1
|
1 |
|
from plugin.core.backup import BackupManager |
2
|
1 |
|
from plugin.core.database import Database |
3
|
|
|
|
4
|
1 |
|
from stash import ApswArchive |
5
|
1 |
|
from trakt_sync.cache.backends import StashBackend |
|
|
|
|
6
|
1 |
|
from trakt_sync.cache.main import Cache |
7
|
1 |
|
from trakt_sync.differ.core.base import KEY_AGENTS |
8
|
1 |
|
import elapsed |
9
|
1 |
|
import logging |
10
|
1 |
|
import os |
11
|
1 |
|
import trakt.objects |
12
|
|
|
|
13
|
1 |
|
log = logging.getLogger(__name__) |
14
|
|
|
|
15
|
|
|
|
16
|
1 |
|
class SyncStateTrakt(object): |
17
|
1 |
|
def __init__(self, state): |
18
|
1 |
|
self.state = state |
19
|
1 |
|
self.task = state.task |
20
|
|
|
|
21
|
1 |
|
self.cache = None |
22
|
|
|
|
23
|
1 |
|
self.changes = None |
24
|
1 |
|
self.table = Table(self.task) |
25
|
|
|
|
26
|
1 |
|
def load(self): |
27
|
|
|
# Construct cache |
28
|
|
|
self.cache = self._build_cache() |
29
|
|
|
|
30
|
|
|
# Load table handler |
31
|
|
|
self.table.load() |
32
|
|
|
|
33
|
1 |
|
def _build_cache(self): |
34
|
|
|
def storage(name): |
35
|
|
|
return StashBackend( |
36
|
|
|
ApswArchive(Database.cache('trakt'), name), |
37
|
|
|
'lru:///?capacity=500&compact_threshold=1500', |
38
|
|
|
'pickle:///?protocol=2' |
39
|
|
|
) |
40
|
|
|
|
41
|
|
|
cache = Cache(self.task.media, self.task.data, storage) |
42
|
|
|
|
43
|
|
|
# Bind to cache events |
44
|
|
|
cache.events.on([ |
45
|
|
|
'refresh.sync.progress', |
46
|
|
|
'refresh.list.progress' |
47
|
|
|
], self.on_refresh_progress) |
48
|
|
|
|
49
|
|
|
return cache |
50
|
|
|
|
51
|
1 |
|
def on_refresh_progress(self, source, current): |
|
|
|
|
52
|
|
|
# Step refresh progress for `source` |
53
|
|
|
self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).step() |
54
|
|
|
|
55
|
1 |
|
def __getitem__(self, key): |
56
|
|
|
collection = [ |
57
|
|
|
self.task.account.trakt.username, |
58
|
|
|
Cache.Media.get(key[0]), |
59
|
|
|
Cache.Data.get(key[1]) |
60
|
|
|
] |
61
|
|
|
|
62
|
|
|
if len(key) > 2: |
63
|
|
|
# Include extra parameters (list id) |
64
|
|
|
collection.extend(key[2:]) |
65
|
|
|
|
66
|
|
|
return self.cache[collection] |
67
|
|
|
|
68
|
1 |
|
def invalidate(self, *key): |
69
|
|
|
"""Invalidate collection in trakt cache""" |
70
|
|
|
username = self.task.account.trakt.username |
71
|
|
|
|
72
|
|
|
# Invalidate collection |
73
|
|
|
self.cache.invalidate([username] + list(key)) |
74
|
|
|
|
75
|
|
|
log.debug('Invalidated trakt cache %r for account: %r', key, username) |
76
|
|
|
|
77
|
1 |
|
@elapsed.clock |
78
|
|
|
def refresh(self): |
79
|
|
|
# Task checkpoint |
80
|
|
|
self.task.checkpoint() |
81
|
|
|
|
82
|
|
|
# Construct progress groups |
83
|
|
|
def setup_progress_group(source): |
84
|
|
|
# Retrieve steps from cache source |
85
|
|
|
steps = self.cache.source(source).steps() |
86
|
|
|
|
87
|
|
|
# Setup progress group with total steps |
88
|
|
|
self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).add(steps) |
89
|
|
|
|
90
|
|
|
setup_progress_group('list') |
91
|
|
|
setup_progress_group('sync') |
92
|
|
|
|
93
|
|
|
# Refresh cache for account, store changes |
94
|
|
|
self.changes = self.cache.refresh(self.task.account.trakt.username) |
95
|
|
|
|
96
|
|
|
# Resolve changes |
97
|
|
|
self.changes = list(self.changes) |
98
|
|
|
|
99
|
|
|
# Reset current table |
100
|
|
|
self.table.reset() |
101
|
|
|
|
102
|
1 |
|
@elapsed.clock |
103
|
|
|
def build_table(self): |
104
|
|
|
# Build table from cache |
105
|
|
|
self.table.build(self.cache) |
106
|
|
|
|
107
|
1 |
|
@elapsed.clock |
108
|
|
|
def flush(self): |
109
|
|
|
with elapsed.clock(SyncStateTrakt, 'flush:collections'): |
110
|
|
|
# Flush trakt collections to disk |
111
|
|
|
self.cache.collections.flush() |
112
|
|
|
|
113
|
|
|
with elapsed.clock(SyncStateTrakt, 'flush:stores'): |
114
|
|
|
# Flush trakt stores to disk |
115
|
|
|
for key, store in self.cache.stores.items(): |
116
|
|
|
log.debug('[%-38s] Flushing collection...', '/'.join(key)) |
117
|
|
|
|
118
|
|
|
store.flush() |
119
|
|
|
|
120
|
|
|
# Store backup of trakt data |
121
|
|
|
group = os.path.join('trakt', str(self.task.account.id)) |
122
|
|
|
|
123
|
|
|
BackupManager.database.backup(group, Database.cache('trakt'), self.task.id, { |
124
|
|
|
'account': { |
125
|
|
|
'id': self.task.account.id, |
126
|
|
|
'name': self.task.account.name, |
127
|
|
|
|
128
|
|
|
'trakt': { |
129
|
|
|
'username': self.task.account.trakt.username |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
}) |
133
|
|
|
|
134
|
|
|
|
135
|
1 |
|
class Table(object): |
136
|
1 |
|
def __init__(self, task): |
137
|
1 |
|
self.task = task |
138
|
|
|
|
139
|
1 |
|
self.table = None |
140
|
|
|
|
141
|
1 |
|
self.movies = None |
142
|
1 |
|
self.shows = None |
143
|
1 |
|
self.episodes = None |
144
|
|
|
|
145
|
1 |
|
self._data = None |
146
|
1 |
|
self._media = None |
147
|
|
|
|
148
|
1 |
|
def load(self): |
149
|
|
|
# Parse data/media enums into lists |
150
|
|
|
self._data = [ |
151
|
|
|
Cache.Data.get(d) |
152
|
|
|
for d in Cache.Data.parse(self.task.data) |
153
|
|
|
] |
154
|
|
|
|
155
|
|
|
self._media = [ |
156
|
|
|
Cache.Media.get(m) |
157
|
|
|
for m in Cache.Media.parse(self.task.media) |
158
|
|
|
] |
159
|
|
|
|
160
|
1 |
|
def reset(self): |
161
|
|
|
self.table = None |
162
|
|
|
|
163
|
|
|
self.movies = None |
164
|
|
|
self.shows = None |
165
|
|
|
self.episodes = None |
166
|
|
|
|
167
|
1 |
|
def build(self, cache): |
168
|
|
|
# Map item `keys` into a table |
169
|
|
|
self.table = {} |
170
|
|
|
|
171
|
|
|
self.movies = set() |
172
|
|
|
self.shows = set() |
173
|
|
|
self.episodes = {} |
174
|
|
|
|
175
|
|
|
log.debug('Building table...') |
176
|
|
|
|
177
|
|
|
log.debug(' - Data: %s', ', '.join([ |
178
|
|
|
'/'.join(x) if type(x) is tuple else x |
179
|
|
|
for x in self._data |
180
|
|
|
])) |
181
|
|
|
|
182
|
|
|
log.debug(' - Media: %s', ', '.join([ |
183
|
|
|
'/'.join(x) if type(x) is tuple else x |
184
|
|
|
for x in self._media |
185
|
|
|
])) |
186
|
|
|
|
187
|
|
|
# Construct progress group |
188
|
|
|
self.task.progress.group(Table, 'build').add(len(cache.collections)) |
189
|
|
|
|
190
|
|
|
# Map each item in cache collections |
191
|
|
|
for key in cache.collections: |
192
|
|
|
# Increment one step |
193
|
|
|
self.task.progress.group(Table, 'build').step() |
194
|
|
|
|
195
|
|
|
# Parse `key` |
196
|
|
|
if len(key) == 3: |
197
|
|
|
# Sync |
198
|
|
|
username, media, data = key |
199
|
|
|
elif len(key) == 4: |
200
|
|
|
# Lists |
201
|
|
|
username, media, data = tuple(key[0:3]) |
202
|
|
|
else: |
203
|
|
|
log.warn('Unknown key: %r', key) |
204
|
|
|
continue |
205
|
|
|
|
206
|
|
|
if username != self.task.account.trakt.username: |
207
|
|
|
# Collection isn't for the current account |
208
|
|
|
continue |
209
|
|
|
|
210
|
|
|
if media and media not in self._media: |
211
|
|
|
log.debug('[%-38s] Media %r has not been enabled', '/'.join(key), media) |
212
|
|
|
continue |
213
|
|
|
|
214
|
|
|
if data not in self._data: |
215
|
|
|
log.debug('[%-38s] Data %r has not been enabled', '/'.join(key), data) |
216
|
|
|
continue |
217
|
|
|
|
218
|
|
|
# Map store items |
219
|
|
|
if data in [ |
220
|
|
|
Cache.Data.get(Cache.Data.Liked), |
221
|
|
|
Cache.Data.get(Cache.Data.Personal) |
222
|
|
|
]: |
223
|
|
|
self.map_items(key, cache[key]) |
224
|
|
|
else: |
225
|
|
|
self.map_items(key, cache[key], media) |
226
|
|
|
|
227
|
|
|
log.debug( |
228
|
|
|
'Built table with %d keys (movies: %d, shows: %d, episodes: %d)', |
229
|
|
|
len(self.table), |
230
|
|
|
len(self.movies), |
231
|
|
|
len(self.shows), |
232
|
|
|
len(self.episodes) |
233
|
|
|
) |
234
|
|
|
|
235
|
1 |
|
def get_keys(self, key, media): |
|
|
|
|
236
|
|
|
if media == 'movies': |
237
|
|
|
return self.movies |
238
|
|
|
|
239
|
|
|
if media in ['shows', 'seasons', 'episodes']: |
240
|
|
|
return self.shows |
241
|
|
|
|
242
|
|
|
return None |
243
|
|
|
|
244
|
1 |
|
@staticmethod |
245
|
|
|
def get_media(item): |
246
|
|
|
i_type = type(item) |
247
|
|
|
|
248
|
|
|
if i_type is trakt.objects.Movie: |
249
|
|
|
return 'movies' |
250
|
|
|
|
251
|
|
|
if i_type is trakt.objects.Show: |
252
|
|
|
return 'shows' |
253
|
|
|
|
254
|
|
|
if i_type is trakt.objects.Season: |
255
|
|
|
return 'seasons' |
256
|
|
|
|
257
|
|
|
if i_type is trakt.objects.Episode: |
258
|
|
|
return 'episodes' |
259
|
|
|
|
260
|
|
|
log.warn('Unknown item type: %r', i_type) |
261
|
|
|
return None |
262
|
|
|
|
263
|
1 |
|
def map_items(self, key, store, media=None): |
264
|
|
|
# Retrieve key map |
265
|
|
|
if media is not None: |
266
|
|
|
keys = self.get_keys(key, media) |
267
|
|
|
|
268
|
|
|
if keys is None: |
269
|
|
|
log.debug('[%-38s] Collection has been ignored (unknown/unsupported media)', '/'.join(key)) |
270
|
|
|
return |
271
|
|
|
else: |
272
|
|
|
keys = None |
273
|
|
|
|
274
|
|
|
# Map each item in store |
275
|
|
|
log.debug('[%-38s] Building table from collection...', '/'.join(key)) |
276
|
|
|
|
277
|
|
|
for pk, item in store.iteritems(): |
278
|
|
|
# Trim `pk` season/episode values |
279
|
|
|
if len(pk) > 2: |
280
|
|
|
pk = tuple(pk[:2]) |
281
|
|
|
|
282
|
|
|
if pk[0] not in ['imdb', 'tvdb']: |
283
|
|
|
log.info('Ignoring item with an unknown primary agent: %r', pk) |
284
|
|
|
continue |
285
|
|
|
|
286
|
|
|
# Detect media type from `item` |
287
|
|
|
if media is not None: |
288
|
|
|
i_media = media |
289
|
|
|
i_keys = keys |
290
|
|
|
else: |
291
|
|
|
i_media = self.get_media(item) |
292
|
|
|
i_keys = self.get_keys(key, i_media) |
293
|
|
|
|
294
|
|
|
# Store `pk` in `keys |
295
|
|
|
if i_keys is not None: |
296
|
|
|
i_keys.add(pk) |
297
|
|
|
|
298
|
|
|
# Map `item.keys` -> `pk` |
299
|
|
|
for key in item.keys: |
300
|
|
|
# Expand `key` |
301
|
|
|
if type(key) is not tuple or len(key) != 2: |
302
|
|
|
continue |
303
|
|
|
|
304
|
|
|
agent, _ = key |
305
|
|
|
|
306
|
|
|
# Check if agent is supported |
307
|
|
|
if agent not in KEY_AGENTS: |
308
|
|
|
continue |
309
|
|
|
|
310
|
|
|
# Store key in table |
311
|
|
|
if key in self.table: |
312
|
|
|
continue |
313
|
|
|
|
314
|
|
|
self.table[key] = pk |
315
|
|
|
|
316
|
|
|
# Map episodes in show |
317
|
|
|
if i_media == 'episodes': |
318
|
|
|
if type(item) is trakt.objects.Show: |
319
|
|
|
if pk not in self.episodes: |
320
|
|
|
self.episodes[pk] = set() |
321
|
|
|
|
322
|
|
|
for identifier, _ in item.episodes(): |
323
|
|
|
self.episodes[pk].add(identifier) |
324
|
|
|
elif type(item) is trakt.objects.Episode: |
325
|
|
|
# TODO |
326
|
|
|
pass |
327
|
|
|
else: |
328
|
|
|
log.debug('Unknown episode item: %r', item) |
329
|
|
|
|
330
|
|
|
# Task checkpoint |
331
|
|
|
self.task.checkpoint() |
332
|
|
|
|
333
|
1 |
|
def get(self, key, default=None): |
334
|
1 |
|
return self.table.get(key, default) |
335
|
|
|
|
336
|
1 |
|
def __getitem__(self, key): |
337
|
|
|
return self.table[key] |
338
|
|
|
|