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