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