forked from pushingkarmaorg/python-plexapi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.py
More file actions
312 lines (257 loc) · 13.4 KB
/
sync.py
File metadata and controls
312 lines (257 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# -*- coding: utf-8 -*-
"""
You can work with Mobile Sync on other devices straight away, but if you'd like to use your app as a `sync-target` (when
you can set items to be synced to your app) you need to init some variables.
.. code-block:: python
def init_sync():
import plexapi
plexapi.X_PLEX_PROVIDES = 'sync-target'
plexapi.BASE_HEADERS['X-Plex-Sync-Version'] = '2'
plexapi.BASE_HEADERS['X-Plex-Provides'] = plexapi.X_PLEX_PROVIDES
# mimic iPhone SE
plexapi.X_PLEX_PLATFORM = 'iOS'
plexapi.X_PLEX_PLATFORM_VERSION = '11.4.1'
plexapi.X_PLEX_DEVICE = 'iPhone'
plexapi.BASE_HEADERS['X-Plex-Platform'] = plexapi.X_PLEX_PLATFORM
plexapi.BASE_HEADERS['X-Plex-Platform-Version'] = plexapi.X_PLEX_PLATFORM_VERSION
plexapi.BASE_HEADERS['X-Plex-Device'] = plexapi.X_PLEX_DEVICE
You have to fake platform/device/model because transcoding profiles are hardcoded in Plex, and you obviously have
to explicitly specify that your app supports `sync-target`.
"""
import requests
import plexapi
from plexapi.base import PlexObject
from plexapi.exceptions import NotFound, BadRequest
class SyncItem(PlexObject):
"""
Represents single sync item, for specified server and client. When you saying in the UI to sync "this" to "that"
you're basically creating a sync item.
Attributes:
id (int): unique id of the item.
clientIdentifier (str): an identifier of Plex Client device, to which the item is belongs.
machineIdentifier (str): the id of server which holds all this content.
version (int): current version of the item. Each time you modify the item (e.g. by changing amount if media to
sync) the new version is created.
rootTitle (str): the title of library/media from which the sync item was created. E.g.:
* when you create an item for an episode 3 of season 3 of show Example, the value would be `Title of
Episode 3`
* when you create an item for a season 3 of show Example, the value would be `Season 3`
* when you set to sync all your movies in library named "My Movies" to value would be `My Movies`.
title (str): the title which you've set when created the sync item.
metadataType (str): the type of media which hides inside, can be `episode`, `movie`, etc.
contentType (str): basic type of the content: `video` or `audio`.
status (:class:`~plexapi.sync.Status`): current status of the sync.
mediaSettings (:class:`~plexapi.sync.MediaSettings`): media transcoding settings used for the item.
policy (:class:`~plexapi.sync.Policy`): the policy of which media to sync.
location (str): plex-style library url with all required filters / sorting.
"""
TAG = 'SyncItem'
def __init__(self, server, data, initpath=None, clientIdentifier=None):
super(SyncItem, self).__init__(server, data, initpath)
self.clientIdentifier = clientIdentifier
def _loadData(self, data):
self._data = data
self.id = plexapi.utils.cast(int, data.attrib.get('id'))
self.version = plexapi.utils.cast(int, data.attrib.get('version'))
self.rootTitle = data.attrib.get('rootTitle')
self.title = data.attrib.get('title')
self.metadataType = data.attrib.get('metadataType')
self.contentType = data.attrib.get('contentType')
self.machineIdentifier = data.find('Server').get('machineIdentifier')
self.status = Status(**data.find('Status').attrib)
self.mediaSettings = MediaSettings(**data.find('MediaSettings').attrib)
self.policy = Policy(**data.find('Policy').attrib)
self.location = data.find('Location').attrib.get('uri', '')
def server(self):
""" Returns :class:`plexapi.myplex.MyPlexResource` with server of current item. """
server = [s for s in self._server.resources() if s.clientIdentifier == self.machineIdentifier]
if len(server) == 0:
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
return server[0]
def getMedia(self):
""" Returns list of :class:`~plexapi.base.Playable` which belong to this sync item. """
server = self.server().connect()
key = '/sync/items/%s' % self.id
return server.fetchItems(key)
def markDownloaded(self, media):
""" Mark the file as downloaded (by the nature of Plex it will be marked as downloaded within
any SyncItem where it presented).
Parameters:
media (base.Playable): the media to be marked as downloaded.
"""
url = '/sync/%s/item/%s/downloaded' % (self.clientIdentifier, media.ratingKey)
media._server.query(url, method=requests.put)
def delete(self):
""" Removes current SyncItem """
url = SyncList.key.format(clientId=self.clientIdentifier)
url += '/' + str(self.id)
self._server.query(url, self._server._session.delete)
class SyncList(PlexObject):
""" Represents a Mobile Sync state, specific for single client, within one SyncList may be presented
items from different servers.
Attributes:
clientId (str): an identifier of the client.
items (List<:class:`~plexapi.sync.SyncItem`>): list of registered items to sync.
"""
key = 'https://plex.tv/devices/{clientId}/sync_items'
TAG = 'SyncList'
def _loadData(self, data):
self._data = data
self.clientId = data.attrib.get('clientIdentifier')
self.items = []
syncItems = data.find('SyncItems')
if syncItems:
for sync_item in syncItems.iter('SyncItem'):
item = SyncItem(self._server, sync_item, clientIdentifier=self.clientId)
self.items.append(item)
class Status(object):
""" Represents a current status of specific :class:`~plexapi.sync.SyncItem`.
Attributes:
failureCode: unknown, never got one yet.
failure: unknown.
state (str): server-side status of the item, can be `completed`, `pending`, empty, and probably something
else.
itemsCount (int): total items count.
itemsCompleteCount (int): count of transcoded and/or downloaded items.
itemsDownloadedCount (int): count of downloaded items.
itemsReadyCount (int): count of transcoded items, which can be downloaded.
totalSize (int): total size in bytes of complete items.
itemsSuccessfulCount (int): unknown, in my experience it always was equal to `itemsCompleteCount`.
"""
def __init__(self, itemsCount, itemsCompleteCount, state, totalSize, itemsDownloadedCount, itemsReadyCount,
itemsSuccessfulCount, failureCode, failure):
self.itemsDownloadedCount = plexapi.utils.cast(int, itemsDownloadedCount)
self.totalSize = plexapi.utils.cast(int, totalSize)
self.itemsReadyCount = plexapi.utils.cast(int, itemsReadyCount)
self.failureCode = failureCode
self.failure = failure
self.itemsSuccessfulCount = plexapi.utils.cast(int, itemsSuccessfulCount)
self.state = state
self.itemsCompleteCount = plexapi.utils.cast(int, itemsCompleteCount)
self.itemsCount = plexapi.utils.cast(int, itemsCount)
def __repr__(self):
return '<%s>:%s' % (self.__class__.__name__, dict(
itemsCount=self.itemsCount,
itemsCompleteCount=self.itemsCompleteCount,
itemsDownloadedCount=self.itemsDownloadedCount,
itemsReadyCount=self.itemsReadyCount,
itemsSuccessfulCount=self.itemsSuccessfulCount
))
class MediaSettings(object):
""" Transcoding settings used for all media within :class:`~plexapi.sync.SyncItem`.
Attributes:
audioBoost (int): unknown.
maxVideoBitrate (int|str): maximum bitrate for video, may be empty string.
musicBitrate (int|str): maximum bitrate for music, may be an empty string.
photoQuality (int): photo quality on scale 0 to 100.
photoResolution (str): maximum photo resolution, formatted as WxH (e.g. `1920x1080`).
videoResolution (str): maximum video resolution, formatted as WxH (e.g. `1280x720`, may be empty).
subtitleSize (int|str): unknown, usually equals to 0, may be empty string.
videoQuality (int): video quality on scale 0 to 100.
"""
def __init__(self, maxVideoBitrate=4000, videoQuality=100, videoResolution='1280x720', audioBoost=100,
musicBitrate=192, photoQuality=74, photoResolution='1920x1080', subtitleSize=''):
self.audioBoost = plexapi.utils.cast(int, audioBoost)
self.maxVideoBitrate = plexapi.utils.cast(int, maxVideoBitrate) if maxVideoBitrate != '' else ''
self.musicBitrate = plexapi.utils.cast(int, musicBitrate) if musicBitrate != '' else ''
self.photoQuality = plexapi.utils.cast(int, photoQuality) if photoQuality != '' else ''
self.photoResolution = photoResolution
self.videoResolution = videoResolution
self.subtitleSize = subtitleSize
self.videoQuality = plexapi.utils.cast(int, videoQuality) if videoQuality != '' else ''
@staticmethod
def createVideo(videoQuality):
""" Returns a :class:`~plexapi.sync.MediaSettings` object, based on provided video quality value.
Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in this module.
Raises:
:class:`plexapi.exceptions.BadRequest`: when provided unknown video quality.
"""
if videoQuality == VIDEO_QUALITY_ORIGINAL:
return MediaSettings('', '', '')
elif videoQuality < len(VIDEO_QUALITIES['bitrate']):
return MediaSettings(VIDEO_QUALITIES['bitrate'][videoQuality],
VIDEO_QUALITIES['videoQuality'][videoQuality],
VIDEO_QUALITIES['videoResolution'][videoQuality])
else:
raise BadRequest('Unexpected video quality')
@staticmethod
def createMusic(bitrate):
""" Returns a :class:`~plexapi.sync.MediaSettings` object, based on provided music quality value
Parameters:
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
module
"""
return MediaSettings(musicBitrate=bitrate)
@staticmethod
def createPhoto(resolution):
""" Returns a :class:`~plexapi.sync.MediaSettings` object, based on provided photo quality value.
Parameters:
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
module.
Raises:
:class:`plexapi.exceptions.BadRequest` when provided unknown video quality.
"""
if resolution in PHOTO_QUALITIES:
return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution)
else:
raise BadRequest('Unexpected photo quality')
class Policy(object):
""" Policy of syncing the media (how many items to sync and process watched media or not).
Attributes:
scope (str): type of limitation policy, can be `count` or `all`.
value (int): amount of media to sync, valid only when `scope=count`.
unwatched (bool): True means disallow to sync watched media.
"""
def __init__(self, scope, unwatched, value=0):
self.scope = scope
self.unwatched = plexapi.utils.cast(bool, unwatched)
self.value = plexapi.utils.cast(int, value)
@staticmethod
def create(limit=None, unwatched=False):
""" Creates a :class:`~plexapi.sync.Policy` object for provided options and automatically sets proper `scope`
value.
Parameters:
limit (int): limit items by count.
unwatched (bool): if True then watched items wouldn't be synced.
Returns:
:class:`~plexapi.sync.Policy`.
"""
scope = 'all'
if limit is None:
limit = 0
else:
scope = 'count'
return Policy(scope, unwatched, limit)
VIDEO_QUALITIES = {
'bitrate': [64, 96, 208, 320, 720, 1500, 2e3, 3e3, 4e3, 8e3, 1e4, 12e3, 2e4],
'videoResolution': ['220x128', '220x128', '284x160', '420x240', '576x320', '720x480', '1280x720', '1280x720',
'1280x720', '1920x1080', '1920x1080', '1920x1080', '1920x1080'],
'videoQuality': [10, 20, 30, 30, 40, 60, 60, 75, 100, 60, 75, 90, 100],
}
VIDEO_QUALITY_0_2_MBPS = 2
VIDEO_QUALITY_0_3_MBPS = 3
VIDEO_QUALITY_0_7_MBPS = 4
VIDEO_QUALITY_1_5_MBPS_480p = 5
VIDEO_QUALITY_2_MBPS_720p = 6
VIDEO_QUALITY_3_MBPS_720p = 7
VIDEO_QUALITY_4_MBPS_720p = 8
VIDEO_QUALITY_8_MBPS_1080p = 9
VIDEO_QUALITY_10_MBPS_1080p = 10
VIDEO_QUALITY_12_MBPS_1080p = 11
VIDEO_QUALITY_20_MBPS_1080p = 12
VIDEO_QUALITY_ORIGINAL = -1
AUDIO_BITRATE_96_KBPS = 96
AUDIO_BITRATE_128_KBPS = 128
AUDIO_BITRATE_192_KBPS = 192
AUDIO_BITRATE_320_KBPS = 320
PHOTO_QUALITIES = {
'720x480': 24,
'1280x720': 49,
'1920x1080': 74,
'3840x2160': 99,
}
PHOTO_QUALITY_HIGHEST = PHOTO_QUALITY_2160p = '3840x2160'
PHOTO_QUALITY_HIGH = PHOTO_QUALITY_1080p = '1920x1080'
PHOTO_QUALITY_MEDIUM = PHOTO_QUALITY_720p = '1280x720'
PHOTO_QUALITY_LOW = PHOTO_QUALITY_480p = '720x480'