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