1
|
1 |
|
from plugin.sync.core.enums import SyncData, SyncMedia, SyncMode |
2
|
1 |
|
from plugin.sync.handlers.collection.base import CollectionHandler |
3
|
1 |
|
from plugin.sync.handlers.core import DataHandler, PushHandler, bind |
4
|
|
|
|
5
|
1 |
|
import logging |
6
|
1 |
|
import re |
7
|
|
|
|
8
|
1 |
|
log = logging.getLogger(__name__) |
9
|
|
|
|
10
|
1 |
|
AUDIO_CODECS = { |
11
|
|
|
'lpcm': 'pcm', |
12
|
|
|
'mp3': None, |
13
|
|
|
'aac': None, |
14
|
|
|
'ogg': 'vorbis', |
15
|
|
|
'wma': None, |
16
|
|
|
|
17
|
|
|
'dts': '(dca|dta)', |
18
|
|
|
'dts_ma': 'dtsma', |
19
|
|
|
|
20
|
|
|
'dolby_prologic': 'dolby.?pro', |
21
|
|
|
'dolby_digital': 'ac.?3', |
22
|
|
|
'dolby_truehd': 'truehd' |
23
|
|
|
} |
24
|
|
|
|
25
|
|
|
# compile patterns in `AUDIO_CODECS` |
26
|
1 |
|
for k, v in AUDIO_CODECS.items(): |
27
|
1 |
|
if v is None: |
28
|
1 |
|
continue |
29
|
|
|
|
30
|
1 |
|
try: |
31
|
1 |
|
AUDIO_CODECS[k] = re.compile(v, re.IGNORECASE) |
32
|
|
|
except Exception: |
|
|
|
|
33
|
|
|
log.warn('Unable to compile regex pattern: %r', v, exc_info=True) |
34
|
|
|
|
35
|
|
|
|
36
|
1 |
|
class Base(PushHandler, CollectionHandler): |
|
|
|
|
37
|
1 |
|
@staticmethod |
38
|
|
|
def get_audio_channels(channels): |
39
|
1 |
|
if channels is None: |
40
|
1 |
|
return None |
41
|
|
|
|
42
|
1 |
|
if channels < 3: |
43
|
1 |
|
return '%.01f' % channels |
44
|
|
|
|
45
|
1 |
|
return '%.01f' % (channels - 0.9) |
46
|
|
|
|
47
|
1 |
|
@staticmethod |
48
|
|
|
def get_audio_codec(codec): |
49
|
1 |
|
if codec is None: |
50
|
1 |
|
return None |
51
|
|
|
|
52
|
1 |
|
for key, regex in AUDIO_CODECS.items(): |
53
|
1 |
|
if key == codec: |
54
|
1 |
|
return key |
55
|
|
|
|
56
|
1 |
|
if regex and regex.match(codec): |
57
|
1 |
|
return key |
58
|
|
|
|
59
|
1 |
|
return None |
60
|
|
|
|
61
|
1 |
|
@staticmethod |
62
|
|
|
def get_resolution(height, interlaced): |
63
|
|
|
# 4k |
64
|
1 |
|
if height > 1100: |
65
|
1 |
|
return 'uhd_4k' |
66
|
|
|
|
67
|
|
|
# 1080 |
68
|
1 |
|
if height > 720: |
69
|
1 |
|
if interlaced: |
70
|
1 |
|
return 'hd_1080i' |
71
|
|
|
else: |
72
|
1 |
|
return 'hd_1080p' |
73
|
|
|
|
74
|
|
|
# 720 |
75
|
1 |
|
if height > 576: |
76
|
1 |
|
return 'hd_720p' |
77
|
|
|
|
78
|
|
|
# 576 |
79
|
1 |
|
if height > 480: |
80
|
1 |
|
if interlaced: |
81
|
1 |
|
return 'sd_576i' |
82
|
|
|
else: |
83
|
1 |
|
return 'sd_576p' |
84
|
|
|
|
85
|
|
|
# 480 |
86
|
1 |
|
if interlaced: |
87
|
1 |
|
return 'sd_480i' |
88
|
|
|
|
89
|
1 |
|
return 'sd_480p' |
90
|
|
|
|
91
|
1 |
|
@classmethod |
92
|
|
|
def build_metadata(cls, p_item): |
93
|
1 |
|
p_media = p_item.get('media', {}) |
94
|
|
|
|
95
|
1 |
|
data = { |
96
|
|
|
'media_type': 'digital' |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
# Set attributes |
100
|
1 |
|
if 'audio_codec' in p_media: |
101
|
1 |
|
data['audio'] = cls.get_audio_codec(p_media['audio_codec']) |
102
|
|
|
|
103
|
1 |
|
if 'audio_channels' in p_media: |
104
|
1 |
|
data['audio_channels'] = cls.get_audio_channels(p_media['audio_channels']) |
105
|
|
|
|
106
|
1 |
|
if 'height' in p_media and 'interlaced' in p_media: |
107
|
1 |
|
data['resolution'] = cls.get_resolution(p_media['height'], p_media['interlaced']) |
108
|
|
|
|
109
|
|
|
# Remove any invalid/missing attributes |
110
|
1 |
|
for key in data.keys(): |
111
|
1 |
|
if data.get(key) is None: |
112
|
1 |
|
del data[key] |
113
|
|
|
|
114
|
1 |
|
return data |
115
|
|
|
|
116
|
|
|
|
117
|
1 |
|
class Movies(Base): |
|
|
|
|
118
|
1 |
|
media = SyncMedia.Movies |
119
|
|
|
|
120
|
1 |
|
@bind('added', [SyncMode.Full, SyncMode.Push]) |
121
|
|
|
def on_added(self, key, guid, p_item, p_value, t_value, **kwargs): |
122
|
1 |
|
log.debug('Movies.on_added(%r, ...)', key) |
123
|
|
|
|
124
|
1 |
|
if t_value: |
125
|
1 |
|
return |
126
|
|
|
|
127
|
1 |
|
self.store_movie('add', guid, |
128
|
|
|
p_item, |
129
|
|
|
collected_at=p_value, |
130
|
|
|
**self.build_metadata(p_item) |
131
|
|
|
) |
132
|
|
|
|
133
|
1 |
|
@bind('removed', [SyncMode.Full]) |
134
|
|
|
def on_removed(self, guid, **kwargs): |
135
|
|
|
if not self.configuration['sync.collection.clean'] or self.current.kwargs.get('section'): |
136
|
|
|
# Collection cleaning hasn't been enabled |
137
|
|
|
return |
138
|
|
|
|
139
|
|
|
log.debug('Movies.on_removed(%r) - %r', guid, kwargs) |
140
|
|
|
|
141
|
|
|
# Store action in artifacts |
142
|
|
|
self.store_movie('remove', guid) |
143
|
|
|
|
144
|
|
|
|
145
|
1 |
|
class Episodes(Base): |
|
|
|
|
146
|
1 |
|
media = SyncMedia.Episodes |
147
|
|
|
|
148
|
1 |
|
@bind('added', [SyncMode.Full, SyncMode.Push]) |
149
|
|
|
def on_added(self, key, guid, identifier, p_show, p_item, p_value, t_value, **kwargs): |
150
|
1 |
|
log.debug('Episodes.on_added(%r, ...)', key) |
151
|
|
|
|
152
|
1 |
|
if t_value: |
153
|
1 |
|
return |
154
|
|
|
|
155
|
1 |
|
self.store_episode('add', guid, |
156
|
|
|
identifier, p_show, |
157
|
|
|
collected_at=p_value, |
158
|
|
|
**self.build_metadata(p_item) |
159
|
|
|
) |
160
|
|
|
|
161
|
1 |
|
@bind('removed', [SyncMode.Full]) |
162
|
|
|
def on_removed(self, guid, identifier, **kwargs): |
163
|
|
|
if not self.configuration['sync.collection.clean'] or self.current.kwargs.get('section'): |
164
|
|
|
# Collection cleaning hasn't been enabled |
165
|
|
|
return |
166
|
|
|
|
167
|
|
|
log.debug('Episodes.on_removed(%r) - %r', guid, kwargs) |
168
|
|
|
|
169
|
|
|
# Store action in artifacts |
170
|
|
|
self.store_episode('remove', guid, identifier) |
171
|
|
|
|
172
|
|
|
|
173
|
1 |
|
class Push(DataHandler): |
174
|
1 |
|
data = SyncData.Collection |
175
|
1 |
|
mode = SyncMode.Push |
176
|
|
|
|
177
|
1 |
|
children = [ |
178
|
|
|
Movies, |
179
|
|
|
Episodes |
180
|
|
|
] |
181
|
|
|
|
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.