Passed
Push — develop ( 3d8444...ce4f6d )
by Dean
03:03
created

ControlsMenu()   F

Complexity

Conditions 9

Size

Total Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 118
rs 3.1304
cc 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
from core.helpers import timestamp, pad_title, function_path, redirect
2
from core.localization import localization
3
from core.logger import Logger
4
5
from plugin.core.constants import PLUGIN_PREFIX
6
from plugin.core.filters import Filters
7
from plugin.core.helpers.variable import normalize
8
from plugin.managers.account import AccountManager
9
from plugin.models import Account, SyncResult
10
from plugin.sync import SyncData, SyncMode
11
from plugin.sync.main import Sync, QueueError
12
13
from ago import human
14
from datetime import datetime
15
from plex import Plex
16
17
L, LF = localization('interface.m_sync')
18
19
log = Logger('interface.m_sync')
20
21
22
# NOTE: pad_title(...) is used to force the UI to use 'media-details-list'
23
24
@route(PLUGIN_PREFIX + '/sync/accounts')
25
def AccountsMenu(refresh=None):
26
    oc = ObjectContainer(
27
        title2=L('accounts:title'),
28
        no_cache=True
29
    )
30
31
    # Active sync status
32
    Active.create(
33
        oc,
34
        callback=Callback(AccountsMenu, refresh=timestamp()),
35
    )
36
37
    # Accounts
38
    for account in Accounts.list():
39
        oc.add(DirectoryObject(
40
            key=Callback(ControlsMenu, account_id=account.id),
41
            title=account.name,
42
43
            art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts),
44
            thumb=function_path('Thumb.png', account_id=account.id, refresh=account.refreshed_ts)
45
        ))
46
47
    return oc
48
49
50
@route(PLUGIN_PREFIX + '/sync')
51
def ControlsMenu(account_id=1, title=None, message=None, refresh=None, message_only=False):
52
    account = AccountManager.get(Account.id == account_id)
53
54
    # Build sync controls menu
55
    oc = ObjectContainer(
56
        title2=LF('controls:title', account.name),
57
        no_cache=True,
58
59
        art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
60
    )
61
62
    # Start result message
63
    if title and message:
64
        oc.add(DirectoryObject(
65
            key=Callback(ControlsMenu, account_id=account.id, refresh=timestamp()),
66
            title=pad_title(title),
67
            summary=message
68
        ))
69
70
        if message_only:
71
            return oc
72
73
    # Active sync status
74
    Active.create(
75
        oc,
76
        callback=Callback(ControlsMenu, account_id=account.id, refresh=timestamp()),
77
        account=account
78
    )
79
80
    #
81
    # Full
82
    #
83
84
    oc.add(DirectoryObject(
85
        key=Trigger.callback(Synchronize, account),
86
        title=pad_title(SyncMode.title(SyncMode.Full)),
87
        summary=Status.build(account, SyncMode.Full),
88
89
        thumb=R("icon-sync.png"),
90
        art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
91
    ))
92
93
    #
94
    # Pull
95
    #
96
97
    oc.add(DirectoryObject(
98
        key=Trigger.callback(Pull, account),
99
        title=pad_title('%s from trakt' % SyncMode.title(SyncMode.Pull)),
100
        summary=Status.build(account, SyncMode.Pull),
101
102
        thumb=R("icon-sync_down.png"),
103
        art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
104
    ))
105
106
    oc.add(DirectoryObject(
107
        key=Trigger.callback(FastPull, account),
108
        title=pad_title('%s from trakt' % SyncMode.title(SyncMode.FastPull)),
109
        summary=Status.build(account, SyncMode.FastPull),
110
111
        thumb=R("icon-sync_down.png"),
112
        art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
113
    ))
114
115
    #
116
    # Push
117
    #
118
119
    p_account = account.plex
120
121
    try:
122
        # Retrieve account libraries/sections
123
        with p_account.authorization():
124
            sections = Plex['library'].sections()
125
    except Exception, ex:
126
        # Build message
127
        if p_account is None:
128
            message = "Plex account hasn't been authenticated"
129
        else:
130
            message = str(ex.message or ex)
131
132
        # Redirect to error message
133
        log.warn('Unable to retrieve account libraries/sections: %s', message, exc_info=True)
134
135
        return redirect('/sync',
136
            account_id=account_id,
137
            title='Error',
138
            message=message,
139
            message_only=True
140
        )
141
142
    section_keys = []
143
144
    f_allow, f_deny = Filters.get('filter_sections')
145
146
    for section in sections.filter(['show', 'movie'], titles=f_allow):
147
        oc.add(DirectoryObject(
148
            key=Trigger.callback(Push, account, section),
149
            title=pad_title('%s "%s" to trakt' % (SyncMode.title(SyncMode.Push), section.title)),
150
            summary=Status.build(account, SyncMode.Push, section.key),
151
152
            thumb=R("icon-sync_up.png"),
153
            art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
154
        ))
155
        section_keys.append(section.key)
156
157
    if len(section_keys) > 1:
158
        oc.add(DirectoryObject(
159
            key=Trigger.callback(Push, account),
160
            title=pad_title('%s all to trakt' % SyncMode.title(SyncMode.Push)),
161
            summary=Status.build(account, SyncMode.Push),
162
163
            thumb=R("icon-sync_up.png"),
164
            art=function_path('Cover.png', account_id=account.id, refresh=account.refreshed_ts)
165
        ))
166
167
    return oc
168
169
170
@route(PLUGIN_PREFIX + '/sync/synchronize')
171
def Synchronize(account_id=1, **kwargs):
172
    return Trigger.run(int(account_id), SyncMode.Full, **kwargs)
173
174
175
@route(PLUGIN_PREFIX + '/sync/fast_pull')
176
def FastPull(account_id=1, **kwargs):
177
    return Trigger.run(int(account_id), SyncMode.FastPull, **kwargs)
178
179
180
@route(PLUGIN_PREFIX + '/sync/push')
181
def Push(account_id=1, section=None, **kwargs):
182
    return Trigger.run(int(account_id), SyncMode.Push, section=section, **kwargs)
183
184
185
@route(PLUGIN_PREFIX + '/sync/pull')
186
def Pull(account_id=1, **kwargs):
187
    return Trigger.run(int(account_id), SyncMode.Pull, **kwargs)
188
189
190
@route(PLUGIN_PREFIX + '/sync/cancel')
191
def Cancel(account_id, id):
192
    id = int(id)
193
194
    # Cancel sync task
195
    if not Sync.cancel(id):
196
        # Unable to cancel task
197
        return redirect('/sync', account_id=account_id, title='Error', message='Unable to cancel current sync')
198
199
    # Success
200
    return redirect('/sync', account_id=account_id)
201
202
203
class Accounts(object):
204
    @classmethod
205
    def count(cls):
206
        return cls.list().count()
207
208
    @classmethod
209
    def list(cls):
210
        return AccountManager.get.all().where(
211
            Account.id != 0,
212
            Account.deleted == False
213
        )
214
215
216
class Active(object):
217
    @classmethod
218
    def create(cls, oc, callback, account=None):
219
        current = Sync.current
220
221
        if not current:
222
            # No task running
223
            return
224
225
        if account and current.account.id != account.id:
226
            # Only display status if `current` task matches provided `account`
227
            return
228
229
        # Create objects
230
        title = cls.build_title(current, account)
231
232
        oc.add(cls.build_status(current, title, callback))
233
        oc.add(cls.build_cancel(current, title))
234
235
    @staticmethod
236
    def build_title(current, account):
237
        # <mode>
238
        title = normalize(SyncMode.title(current.mode))
239
240
        # Task Progress
241
        percent = current.progress.percent
242
243
        if percent is not None:
244
            title += ' (%2d%%)' % percent
245
246
        # Account Name (only display outside of account-specific menus)
247
        if account is None:
248
            title += ' (%s)' % current.account.name
249
250
        return title
251
252
    #
253
    # Status
254
    #
255
256
    @classmethod
257
    def build_status(cls, current, title, callback=None):
258
        return DirectoryObject(
259
            key=callback,
260
            title=pad_title('%s - Status' % title),
261
            summary=cls.build_status_summary(current)
262
        )
263
264
    @staticmethod
265
    def build_status_summary(current):
266
        summary = 'Working'
267
268
        # Estimated time remaining
269
        remaining_seconds = current.progress.remaining_seconds
270
271
        if remaining_seconds is not None:
272
            summary += ', %.02f seconds remaining' % remaining_seconds
273
274
        return summary
275
276
    #
277
    # Cancel
278
    #
279
280
    @classmethod
281
    def build_cancel(cls, current, title):
282
        return DirectoryObject(
283
            key=Callback(Cancel, account_id=current.account.id, id=current.id),
284
            title=pad_title('%s - Cancel' % title)
285
        )
286
287
288
class Status(object):
289
    @classmethod
290
    def build(cls, account, mode, section=None):
291
        status = SyncResult.get_latest(account, mode, section).first()
292
293
        if status is None or status.latest is None:
294
            return 'Not run yet.'
295
296
        # Build status fragments
297
        fragments = []
298
299
        if status.latest.ended_at:
300
            # Build "Last run [...] ago" fragment
301
            fragments.append(cls.build_since(status))
302
303
            if status.latest.started_at:
304
                # Build "taking [...] seconds" fragment
305
                fragments.append(cls.build_elapsed(status))
306
307
        # Build result fragment (success, errors)
308
        fragments.append(cls.build_result(status))
309
310
        # Merge fragments
311
        if len(fragments):
312
            return ', '.join(fragments) + '.'
313
314
        return 'Not run yet.'
315
316
    @staticmethod
317
    def build_elapsed(status):
318
        elapsed = status.latest.ended_at - status.latest.started_at
319
320
        if elapsed.seconds < 1:
321
            return 'taking less than a second'
322
323
        return 'taking %s' % human(
324
            elapsed,
325
            precision=1,
326
            past_tense='%s'
327
        )
328
329
    @staticmethod
330
    def build_result(status):
331
        if status.latest.success:
332
            return 'was successful'
333
334
        message = 'failed'
335
336
        # Resolve errors
337
        errors = list(status.latest.get_errors())
338
339
        if len(errors) > 1:
340
            # Multiple errors
341
            message += ' (%d errors, %s)' % (len(errors), errors[0].summary)
342
        elif len(errors) == 1:
343
            # Single error
344
            message += ' (%s)' % errors[0].summary
345
346
        return message
347
348
    @staticmethod
349
    def build_since(status):
350
        since = datetime.utcnow() - status.latest.ended_at
351
352
        if since.seconds < 1:
353
            return 'Last run just a moment ago'
354
355
        return 'Last run %s' % human(since, precision=1)
356
357
358
class Trigger(object):
359
    triggered = {}
360
361
    @classmethod
362
    def callback(cls, func, account, section=None):
363
        if section:
364
            return Callback(func, account_id=account.id, section=section.key, t=timestamp())
365
366
        return Callback(func, account_id=account.id, t=timestamp())
367
368
    @classmethod
369
    def run(cls, account_id, mode, t, **kwargs):
370
        # Check for duplicate trigger
371
        key = (account_id, mode, t)
372
373
        if key in cls.triggered:
374
            log.info('Ignored duplicate sync trigger action')
375
            return redirect('/sync', account_id=account_id)
376
377
        # Mark triggered
378
        cls.triggered[key] = True
379
380
        # Trigger sync
381
        try:
382
            Sync.queue(account_id, mode, **kwargs)
383
        except QueueError, ex:
384
            return redirect('/sync', account_id=account_id, title=ex.title, message=ex.message)
385
386
        return redirect('/sync', account_id=account_id)
387