Clean.execute()   F
last analyzed

Complexity

Conditions 9

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 3
c 0
b 0
f 0
cc 9
1
from plugin.core.constants import PLUGIN_VERSION_BASE
2
from plugin.core.helpers.variable import all
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in all.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
3
4
from lxml import etree
5
import shutil
6
import os
7
8
9
class FSMigrator(object):
10
    migrations = []
11
12
    @classmethod
13
    def register(cls, migration):
14
        cls.migrations.append(migration())
15
16
    @classmethod
17
    def run(cls):
18
        for migration in cls.migrations:
19
            Log.Debug('Running migration: %s', migration)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
20
            migration.run()
21
22
23
class Migration(object):
24
    @property
25
    def code_path(self):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
26
        return Core.code_path
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Core'
Loading history...
27
28
    @property
29
    def lib_path(self):
30
        return os.path.join(self.code_path, '..', 'Libraries')
31
32
    @property
33
    def tests_path(self):
34
        return os.path.join(self.code_path, '..', 'Tests')
35
36
    @property
37
    def plex_path(self):
38
        return os.path.abspath(os.path.join(self.code_path, '..', '..', '..', '..'))
39
40
    @property
41
    def preferences_path(self):
42
        return os.path.join(self.plex_path, 'Plug-in Support', 'Preferences', 'com.plexapp.plugins.trakttv.xml')
43
44
    def get_preferences(self):
45
        if not os.path.exists(self.preferences_path):
46
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
47
            return {}
48
49
        data = Core.storage.load(self.preferences_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Core'
Loading history...
50
        doc = etree.fromstring(data)
51
52
        return dict([(elem.tag, elem.text) for elem in doc])
53
54
    def set_preferences(self, changes):
55
        if not os.path.exists(self.preferences_path):
56
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
57
            return False
58
59
        data = Core.storage.load(self.preferences_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Core'
Loading history...
60
        doc = etree.fromstring(data)
61
62
        for key, value in changes.items():
63
            elem = doc.find(key)
64
65
            # Ensure node exists
66
            if elem is None:
67
                elem = etree.SubElement(doc, key)
68
69
            # Update node value, ensure it is a string
70
            elem.text = str(value)
71
72
            Log.Debug('Updated preference with key "%s" to value %s', key, repr(value))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
73
74
        # Write back new preferences
75
        Core.storage.save(self.preferences_path, etree.tostring(doc, pretty_print=True))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Core'
Loading history...
76
77
    @staticmethod
78
    def delete_file(path, conditions=None):
79
        if not all([c(path) for c in conditions]):
80
            return False
81
82
        try:
83
            os.remove(path)
84
            return True
85
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
86
            Log.Warn('Unable to remove file %r - %s', path, ex, exc_info=True)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
87
88
        return False
89
90
    @staticmethod
91
    def delete_directory(path, conditions=None):
92
        if not all([c(path) for c in conditions]):
93
            return False
94
95
        try:
96
            shutil.rmtree(path)
97
            return True
98
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
99
            Log.Warn('Unable to remove directory %r - %s', path, ex, exc_info=True)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
100
101
        return False
102
103
104
class Clean(Migration):
105
    tasks_code = [
106
        (
107
            'delete_file', [
108
                # /core
109
                'core/action.py',
110
                'core/cache.py',
111
                'core/configuration.py',
112
                'core/environment.py',
113
                'core/eventing.py',
114
                'core/localization.py',
115
                'core/logging_handler.py',
116
                'core/logging_reporter.py',
117
                'core/method_manager.py',
118
                'core/migrator.py',
119
                'core/model.py',
120
                'core/network.py',
121
                'core/numeric.py',
122
                'core/plugin.py',
123
                'core/task.py',
124
                'core/trakt.py',
125
                'core/trakt_objects.py',
126
                'core/update_checker.py',
127
128
                # /interface
129
                'interface/main_menu.py',
130
                'interface/sync_menu.py',
131
132
                # /
133
                'libraries.py',
134
                'sync.py'
135
            ], os.path.isfile
136
        ),
137
        (
138
            'delete_directory', [
139
                'data',
140
                'plex',
141
                'pts',
142
                'sync'
143
            ], os.path.isdir
144
        )
145
    ]
146
147
    tasks_lib = [
148
        (
149
            'delete_file', [
150
                # plugin
151
                'Shared/plugin/api/account.py',
152
                'Shared/plugin/core/event.py',
153
                'Shared/plugin/core/helpers/database.py',
154
                'Shared/plugin/core/io.py',
155
                'Shared/plugin/core/jsonw.py',
156
                'Shared/plugin/core/libraries/main.py',
157
                'Shared/plugin/core/libraries/tests/pyopenssl_.py',
158
                'Shared/plugin/core/logger/handlers/error_reporter.py',
159
                'Shared/plugin/core/session_status.py',
160
                'Shared/plugin/models/core/exceptions.py',
161
                'Shared/plugin/modules/base.py',
162
                'Shared/plugin/modules/manager.py',
163
                'Shared/plugin/preferences/options/core/base.py',
164
                'Shared/plugin/sync/modes/core/base.py',
165
                'Shared/plugin/sync/modes/fast_pull.py',
166
                'Shared/plugin/sync/modes/pull.py',
167
                'Shared/plugin/sync/modes/push.py',
168
169
                # native
170
                'FreeBSD/i386/apsw.so',
171
                'FreeBSD/i386/llist.so',
172
173
                'FreeBSD/i386/ucs2/apsw.dependencies',
174
                'FreeBSD/i386/ucs2/apsw.file',
175
                'FreeBSD/i386/ucs2/llist.dependencies',
176
                'FreeBSD/i386/ucs2/llist.file',
177
                'FreeBSD/i386/ucs4/apsw.dependencies',
178
                'FreeBSD/i386/ucs4/apsw.file',
179
                'FreeBSD/i386/ucs4/llist.dependencies',
180
                'FreeBSD/i386/ucs4/llist.file',
181
182
                'FreeBSD/x86_64/ucs2/apsw.dependencies',
183
                'FreeBSD/x86_64/ucs2/apsw.file',
184
                'FreeBSD/x86_64/ucs2/llist.dependencies',
185
                'FreeBSD/x86_64/ucs2/llist.file',
186
                'FreeBSD/x86_64/ucs4/apsw.dependencies',
187
                'FreeBSD/x86_64/ucs4/apsw.file',
188
                'FreeBSD/x86_64/ucs4/llist.dependencies',
189
                'FreeBSD/x86_64/ucs4/llist.file',
190
191
                'Windows/i386/apsw.pyd',
192
                'Windows/i386/llist.pyd',
193
194
                'Linux/i386/apsw.so',
195
                'Linux/i386/llist.so',
196
                'Linux/x86_64/apsw.so',
197
                'Linux/x86_64/llist.so',
198
199
                'Linux/armv6_hf/ucs4/apsw.dependencies',
200
                'Linux/armv6_hf/ucs4/apsw.file',
201
                'Linux/armv6_hf/ucs4/apsw.header',
202
                'Linux/armv6_hf/ucs4/llist.dependencies',
203
                'Linux/armv6_hf/ucs4/llist.file',
204
                'Linux/armv6_hf/ucs4/llist.header',
205
206
                'Linux/armv7_hf/ucs4/apsw.dependencies',
207
                'Linux/armv7_hf/ucs4/apsw.file',
208
                'Linux/armv7_hf/ucs4/apsw.header',
209
                'Linux/armv7_hf/ucs4/llist.dependencies',
210
                'Linux/armv7_hf/ucs4/llist.file',
211
                'Linux/armv7_hf/ucs4/llist.header',
212
213
                'Linux/i386/ucs2/apsw.dependencies',
214
                'Linux/i386/ucs2/apsw.file',
215
                'Linux/i386/ucs2/llist.dependencies',
216
                'Linux/i386/ucs2/llist.file',
217
                'Linux/i386/ucs4/apsw.dependencies',
218
                'Linux/i386/ucs4/apsw.file',
219
                'Linux/i386/ucs4/llist.dependencies',
220
                'Linux/i386/ucs4/llist.file',
221
222
                'Linux/x86_64/ucs2/apsw.dependencies',
223
                'Linux/x86_64/ucs2/apsw.file',
224
                'Linux/x86_64/ucs2/llist.dependencies',
225
                'Linux/x86_64/ucs2/llist.file',
226
                'Linux/x86_64/ucs4/apsw.dependencies',
227
                'Linux/x86_64/ucs4/apsw.file',
228
                'Linux/x86_64/ucs4/llist.dependencies',
229
                'Linux/x86_64/ucs4/llist.file',
230
231
                'MacOSX/i386/ucs2/apsw.dependencies',
232
                'MacOSX/i386/ucs2/apsw.file',
233
                'MacOSX/i386/ucs2/llist.dependencies',
234
                'MacOSX/i386/ucs2/llist.file',
235
                'MacOSX/i386/ucs4/apsw.dependencies',
236
                'MacOSX/i386/ucs4/apsw.file',
237
                'MacOSX/i386/ucs4/llist.dependencies',
238
                'MacOSX/i386/ucs4/llist.file',
239
240
                'MacOSX/x86_64/ucs2/apsw.dependencies',
241
                'MacOSX/x86_64/ucs2/apsw.file',
242
                'MacOSX/x86_64/ucs2/llist.dependencies',
243
                'MacOSX/x86_64/ucs2/llist.file',
244
                'MacOSX/x86_64/ucs4/apsw.dependencies',
245
                'MacOSX/x86_64/ucs4/apsw.file',
246
                'MacOSX/x86_64/ucs4/llist.dependencies',
247
                'MacOSX/x86_64/ucs4/llist.file',
248
249
                'Windows/i386/ucs2/apsw.pyd',
250
                'Windows/i386/ucs2/llist.pyd',
251
252
                # asio
253
                'Shared/asio.py',
254
                'Shared/asio_base.py',
255
                'Shared/asio_posix.py',
256
                'Shared/asio_windows.py',
257
                'Shared/asio_windows_interop.py',
258
259
                # concurrent
260
                'Shared/concurrent/futures/_compat.py',
261
262
                # msgpack
263
                'Shared/msgpack/_packer.pyx',
264
                'Shared/msgpack/_unpacker.pyx',
265
                'Shared/msgpack/pack.h',
266
                'Shared/msgpack/pack_template.h',
267
                'Shared/msgpack/sysdep.h',
268
                'Shared/msgpack/unpack.h',
269
                'Shared/msgpack/unpack_define.h',
270
                'Shared/msgpack/unpack_template.h',
271
272
                # playhouse
273
                'Shared/playhouse/pskel',
274
275
                # plex.py
276
                'Shared/plex/core/compat.py',
277
                'Shared/plex/core/event.py',
278
                'Shared/plex/interfaces/library.py',
279
                'Shared/plex/interfaces/plugin.py',
280
281
                # plex.metadata.py
282
                'Shared/plex_metadata/core/cache.py',
283
284
                # raven
285
                'Shared/raven/transport/aiohttp.py',
286
                'Shared/raven/transport/udp.py',
287
                'Shared/raven/utils/six.py',
288
289
                # requests
290
                'Shared/requests/packages/urllib3/util.py',
291
                'Shared/requests/packages/README.rst',
292
293
                # trakt.py
294
                'Shared/trakt/core/context.py',
295
                'Shared/trakt/interfaces/base/media.py',
296
                'Shared/trakt/interfaces/account.py',
297
                'Shared/trakt/interfaces/rate.py',
298
                'Shared/trakt/interfaces/sync/base.py',
299
                'Shared/trakt/media_mapper.py',
300
                'Shared/trakt/objects.py',
301
                'Shared/trakt/objects/list.py',
302
                'Shared/trakt/request.py',
303
304
                # tzlocal
305
                'Shared/tzlocal/tests.py',
306
307
                # websocket
308
                'Shared/websocket.py'
309
            ], os.path.isfile
310
        ),
311
        (
312
            'delete_directory', [
313
                # plugin
314
                'Shared/plugin/core/collections',
315
                'Shared/plugin/data',
316
                'Shared/plugin/modules/backup',
317
                'Shared/plugin/raven',
318
319
                # native
320
                'MacOSX/universal',
321
322
                # pytz
323
                'Shared/pytz/tests',
324
325
                # raven
326
                'Shared/raven',
327
328
                # shove
329
                'Shared/shove',
330
331
                # stuf
332
                'Shared/stuf',
333
334
                # trakt.py
335
                'Shared/trakt/interfaces/movie',
336
                'Shared/trakt/interfaces/show',
337
                'Shared/trakt/interfaces/user',
338
339
                # tzlocal
340
                'Shared/tzlocal/test_data'
341
            ], os.path.isdir
342
        )
343
    ]
344
345
    tasks_tests = [
346
        (
347
            'delete_file', [
348
            ], os.path.isfile
349
        ),
350
        (
351
            'delete_directory', [
352
                'tests/core/mock',
353
                'tests/scrobbler/engine_tests.py',
354
            ], os.path.isdir
355
        )
356
    ]
357
358
    def run(self):
359
        if PLUGIN_VERSION_BASE >= (0, 8):
360
            self.upgrade()
361
362
    def upgrade(self):
363
        self.execute(self.tasks_code, 'upgrade', self.code_path)
364
        self.execute(self.tasks_lib, 'upgrade', self.lib_path)
365
        self.execute(self.tasks_tests, 'upgrade', self.tests_path)
366
367
    def execute(self, tasks, name, base_path):
368
        for action, paths, conditions in tasks:
369
            if type(paths) is not list:
370
                paths = [paths]
371
372
            if type(conditions) is not list:
373
                conditions = [conditions]
374
375
            if not hasattr(self, action):
376
                Log.Error('Unknown migration action "%s"', action)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
377
                continue
378
379
            m = getattr(self, action)
380
381
            for path in paths:
382
                path = os.path.join(base_path, path)
383
                path = os.path.abspath(path)
384
385
                # Remove file
386
                if m(path, conditions):
387
                    Log.Info('(%s) %s: "%s"', name, action, path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
388
389
                # Remove .pyc files as-well
390
                if path.endswith('.py') and m(path + 'c', conditions):
391
                    Log.Info('(%s) %s: "%s"', name, action, path + 'c')
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
392
393
394
class ForceLegacy(Migration):
395
    """Migrates the 'force_legacy' option to the 'activity_mode' option."""
396
397
    def run(self):
398
        self.upgrade()
399
400
    def upgrade(self):
401
        if not os.path.exists(self.preferences_path):
402
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
403
            return
404
405
        preferences = self.get_preferences()
406
407
        # Read 'force_legacy' option from raw preferences
408
        force_legacy = preferences.get('force_legacy')
409
410
        if force_legacy is None:
411
            return
412
413
        force_legacy = force_legacy.lower() == "true"
414
415
        if not force_legacy:
416
            return
417
418
        # Read 'activity_mode' option from raw preferences
419
        activity_mode = preferences.get('activity_mode')
420
421
        # Activity mode has already been set, not changing it
422
        if activity_mode is not None:
423
            return
424
425
        self.set_preferences({
426
            'activity_mode': '1'
427
        })
428
429
430
class SelectiveSync(Migration):
431
    """Migrates the syncing task bool options to selective synchronize/push/pull enums"""
432
433
    option_keys = [
434
        'sync_watched',
435
        'sync_ratings',
436
        'sync_collection'
437
    ]
438
439
    value_map = {
440
        'false': '0',
441
        'true': '1',
442
    }
443
444
    def run(self):
445
        self.upgrade()
446
447
    def upgrade(self):
448
        preferences = self.get_preferences()
449
450
        # Filter to only relative preferences
451
        preferences = dict([
452
            (key, value)
453
            for key, value in preferences.items()
454
            if key in self.option_keys
455
        ])
456
457
        changes = {}
458
459
        for key, value in preferences.items():
460
            if value not in self.value_map:
461
                continue
462
463
            changes[key] = self.value_map[value]
464
465
        if not changes:
466
            return
467
468
        Log.Debug('Updating preferences with changes: %s', changes)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'Log'
Loading history...
469
        self.set_preferences(changes)
470
471
472
FSMigrator.register(Clean)
473
FSMigrator.register(ForceLegacy)
474
FSMigrator.register(SelectiveSync)
475