1# -*- coding: utf-8 -*-
2
3import xml
4from urllib.parse import quote_plus
5
6from plexapi import log, settings, utils
7from plexapi.base import PlexObject
8from plexapi.exceptions import BadRequest
9
10
11@utils.registerPlexObject
12class Media(PlexObject):
13    """ Container object for all MediaPart objects. Provides useful data about the
14        video or audio this media belong to such as video framerate, resolution, etc.
15
16        Attributes:
17            TAG (str): 'Media'
18            aspectRatio (float): The aspect ratio of the media (ex: 2.35).
19            audioChannels (int): The number of audio channels of the media (ex: 6).
20            audioCodec (str): The audio codec of the media (ex: ac3).
21            audioProfile (str): The audio profile of the media (ex: dts).
22            bitrate (int): The bitrate of the media (ex: 1624).
23            container (str): The container of the media (ex: avi).
24            duration (int): The duration of the media in milliseconds (ex: 6990483).
25            height (int): The height of the media in pixels (ex: 256).
26            id (int): The unique ID for this media on the server.
27            has64bitOffsets (bool): True if video has 64 bit offsets.
28            optimizedForStreaming (bool): True if video is optimized for streaming.
29            parts (List<:class:`~plexapi.media.MediaPart`>): List of media part objects.
30            proxyType (int): Equals 42 for optimized versions.
31            target (str): The media version target name.
32            title (str): The title of the media.
33            videoCodec (str): The video codec of the media (ex: ac3).
34            videoFrameRate (str): The video frame rate of the media (ex: 24p).
35            videoProfile (str): The video profile of the media (ex: high).
36            videoResolution (str): The video resolution of the media (ex: sd).
37            width (int): The width of the video in pixels (ex: 608).
38
39            <Photo_only_attributes>: The following attributes are only available for photos.
40
41                * aperture (str): The apeture used to take the photo.
42                * exposure (str): The exposure used to take the photo.
43                * iso (int): The iso used to take the photo.
44                * lens (str): The lens used to take the photo.
45                * make (str): The make of the camera used to take the photo.
46                * model (str): The model of the camera used to take the photo.
47    """
48    TAG = 'Media'
49
50    def _loadData(self, data):
51        """ Load attribute values from Plex XML response. """
52        self._data = data
53        self.aspectRatio = utils.cast(float, data.attrib.get('aspectRatio'))
54        self.audioChannels = utils.cast(int, data.attrib.get('audioChannels'))
55        self.audioCodec = data.attrib.get('audioCodec')
56        self.audioProfile = data.attrib.get('audioProfile')
57        self.bitrate = utils.cast(int, data.attrib.get('bitrate'))
58        self.container = data.attrib.get('container')
59        self.duration = utils.cast(int, data.attrib.get('duration'))
60        self.height = utils.cast(int, data.attrib.get('height'))
61        self.id = utils.cast(int, data.attrib.get('id'))
62        self.has64bitOffsets = utils.cast(bool, data.attrib.get('has64bitOffsets'))
63        self.optimizedForStreaming = utils.cast(bool, data.attrib.get('optimizedForStreaming'))
64        self.parts = self.findItems(data, MediaPart)
65        self.proxyType = utils.cast(int, data.attrib.get('proxyType'))
66        self.target = data.attrib.get('target')
67        self.title = data.attrib.get('title')
68        self.videoCodec = data.attrib.get('videoCodec')
69        self.videoFrameRate = data.attrib.get('videoFrameRate')
70        self.videoProfile = data.attrib.get('videoProfile')
71        self.videoResolution = data.attrib.get('videoResolution')
72        self.width = utils.cast(int, data.attrib.get('width'))
73
74        if self._isChildOf(etag='Photo'):
75            self.aperture = data.attrib.get('aperture')
76            self.exposure = data.attrib.get('exposure')
77            self.iso = utils.cast(int, data.attrib.get('iso'))
78            self.lens = data.attrib.get('lens')
79            self.make = data.attrib.get('make')
80            self.model = data.attrib.get('model')
81
82        parent = self._parent()
83        self._parentKey = parent.key
84
85    @property
86    def isOptimizedVersion(self):
87        """ Returns True if the media is a Plex optimized version. """
88        return self.proxyType == utils.SEARCHTYPES['optimizedVersion']
89
90    def delete(self):
91        part = '%s/media/%s' % (self._parentKey, self.id)
92        try:
93            return self._server.query(part, method=self._server._session.delete)
94        except BadRequest:
95            log.error("Failed to delete %s. This could be because you havn't allowed "
96                      "items to be deleted" % part)
97            raise
98
99
100@utils.registerPlexObject
101class MediaPart(PlexObject):
102    """ Represents a single media part (often a single file) for the media this belongs to.
103
104        Attributes:
105            TAG (str): 'Part'
106            accessible (bool): True if the file is accessible.
107            audioProfile (str): The audio profile of the file.
108            container (str): The container type of the file (ex: avi).
109            decision (str): Unknown.
110            deepAnalysisVersion (int): The Plex deep analysis version for the file.
111            duration (int): The duration of the file in milliseconds.
112            exists (bool): True if the file exists.
113            file (str): The path to this file on disk (ex: /media/Movies/Cars (2006)/Cars (2006).mkv)
114            has64bitOffsets (bool): True if the file has 64 bit offsets.
115            hasThumbnail (bool): True if the file (track) has an embedded thumbnail.
116            id (int): The unique ID for this media part on the server.
117            indexes (str, None): sd if the file has generated preview (BIF) thumbnails.
118            key (str): API URL (ex: /library/parts/46618/1389985872/file.mkv).
119            optimizedForStreaming (bool): True if the file is optimized for streaming.
120            packetLength (int): The packet length of the file.
121            requiredBandwidths (str): The required bandwidths to stream the file.
122            size (int): The size of the file in bytes (ex: 733884416).
123            streams (List<:class:`~plexapi.media.MediaPartStream`>): List of stream objects.
124            syncItemId (int): The unique ID for this media part if it is synced.
125            syncState (str): The sync state for this media part.
126            videoProfile (str): The video profile of the file.
127    """
128    TAG = 'Part'
129
130    def _loadData(self, data):
131        """ Load attribute values from Plex XML response. """
132        self._data = data
133        self.accessible = utils.cast(bool, data.attrib.get('accessible'))
134        self.audioProfile = data.attrib.get('audioProfile')
135        self.container = data.attrib.get('container')
136        self.decision = data.attrib.get('decision')
137        self.deepAnalysisVersion = utils.cast(int, data.attrib.get('deepAnalysisVersion'))
138        self.duration = utils.cast(int, data.attrib.get('duration'))
139        self.exists = utils.cast(bool, data.attrib.get('exists'))
140        self.file = data.attrib.get('file')
141        self.has64bitOffsets = utils.cast(bool, data.attrib.get('has64bitOffsets'))
142        self.hasThumbnail = utils.cast(bool, data.attrib.get('hasThumbnail'))
143        self.id = utils.cast(int, data.attrib.get('id'))
144        self.indexes = data.attrib.get('indexes')
145        self.key = data.attrib.get('key')
146        self.optimizedForStreaming = utils.cast(bool, data.attrib.get('optimizedForStreaming'))
147        self.packetLength = utils.cast(int, data.attrib.get('packetLength'))
148        self.requiredBandwidths = data.attrib.get('requiredBandwidths')
149        self.size = utils.cast(int, data.attrib.get('size'))
150        self.streams = self._buildStreams(data)
151        self.syncItemId = utils.cast(int, data.attrib.get('syncItemId'))
152        self.syncState = data.attrib.get('syncState')
153        self.videoProfile = data.attrib.get('videoProfile')
154
155    def _buildStreams(self, data):
156        streams = []
157        for cls in (VideoStream, AudioStream, SubtitleStream, LyricStream):
158            items = self.findItems(data, cls, streamType=cls.STREAMTYPE)
159            streams.extend(items)
160        return streams
161
162    @property
163    def hasPreviewThumbnails(self):
164        """ Returns True if the media part has generated preview (BIF) thumbnails. """
165        return self.indexes == 'sd'
166
167    def videoStreams(self):
168        """ Returns a list of :class:`~plexapi.media.VideoStream` objects in this MediaPart. """
169        return [stream for stream in self.streams if isinstance(stream, VideoStream)]
170
171    def audioStreams(self):
172        """ Returns a list of :class:`~plexapi.media.AudioStream` objects in this MediaPart. """
173        return [stream for stream in self.streams if isinstance(stream, AudioStream)]
174
175    def subtitleStreams(self):
176        """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
177        return [stream for stream in self.streams if isinstance(stream, SubtitleStream)]
178
179    def lyricStreams(self):
180        """ Returns a list of :class:`~plexapi.media.LyricStream` objects in this MediaPart. """
181        return [stream for stream in self.streams if isinstance(stream, LyricStream)]
182
183    def setDefaultAudioStream(self, stream):
184        """ Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
185
186            Parameters:
187                stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default
188        """
189        if isinstance(stream, AudioStream):
190            key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id)
191        else:
192            key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream)
193        self._server.query(key, method=self._server._session.put)
194
195    def setDefaultSubtitleStream(self, stream):
196        """ Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart.
197
198            Parameters:
199                stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default.
200        """
201        if isinstance(stream, SubtitleStream):
202            key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id)
203        else:
204            key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream)
205        self._server.query(key, method=self._server._session.put)
206
207    def resetDefaultSubtitleStream(self):
208        """ Set default subtitle of this MediaPart to 'none'. """
209        key = "/library/parts/%d?subtitleStreamID=0&allParts=1" % (self.id)
210        self._server.query(key, method=self._server._session.put)
211
212
213class MediaPartStream(PlexObject):
214    """ Base class for media streams. These consist of video, audio, subtitles, and lyrics.
215
216        Attributes:
217            bitrate (int): The bitrate of the stream.
218            codec (str): The codec of the stream (ex: srt, ac3, mpeg4).
219            default (bool): True if this is the default stream.
220            displayTitle (str): The display title of the stream.
221            extendedDisplayTitle (str): The extended display title of the stream.
222            key (str): API URL (/library/streams/<id>)
223            id (int): The unique ID for this stream on the server.
224            index (int): The index of the stream.
225            language (str): The language of the stream (ex: English, ไทย).
226            languageCode (str): The Ascii language code of the stream (ex: eng, tha).
227            requiredBandwidths (str): The required bandwidths to stream the file.
228            selected (bool): True if this stream is selected.
229            streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`,
230                2= :class:`~plexapi.media.AudioStream`, 3= :class:`~plexapi.media.SubtitleStream`).
231            title (str): The title of the stream.
232            type (int): Alias for streamType.
233    """
234
235    def _loadData(self, data):
236        """ Load attribute values from Plex XML response. """
237        self._data = data
238        self.bitrate = utils.cast(int, data.attrib.get('bitrate'))
239        self.codec = data.attrib.get('codec')
240        self.default = utils.cast(bool, data.attrib.get('default'))
241        self.displayTitle = data.attrib.get('displayTitle')
242        self.extendedDisplayTitle = data.attrib.get('extendedDisplayTitle')
243        self.key = data.attrib.get('key')
244        self.id = utils.cast(int, data.attrib.get('id'))
245        self.index = utils.cast(int, data.attrib.get('index', '-1'))
246        self.language = data.attrib.get('language')
247        self.languageCode = data.attrib.get('languageCode')
248        self.requiredBandwidths = data.attrib.get('requiredBandwidths')
249        self.selected = utils.cast(bool, data.attrib.get('selected', '0'))
250        self.streamType = utils.cast(int, data.attrib.get('streamType'))
251        self.title = data.attrib.get('title')
252        self.type = utils.cast(int, data.attrib.get('streamType'))
253
254
255@utils.registerPlexObject
256class VideoStream(MediaPartStream):
257    """ Represents a video stream within a :class:`~plexapi.media.MediaPart`.
258
259        Attributes:
260            TAG (str): 'Stream'
261            STREAMTYPE (int): 1
262            anamorphic (str): If the video is anamorphic.
263            bitDepth (int): The bit depth of the video stream (ex: 8).
264            cabac (int): The context-adaptive binary arithmetic coding.
265            chromaLocation (str): The chroma location of the video stream.
266            chromaSubsampling (str): The chroma subsampling of the video stream (ex: 4:2:0).
267            codecID (str): The codec ID (ex: XVID).
268            codedHeight (int): The coded height of the video stream in pixels.
269            codedWidth (int): The coded width of the video stream in pixels.
270            colorPrimaries (str): The color primaries of the video stream.
271            colorRange (str): The color range of the video stream.
272            colorSpace (str): The color space of the video stream (ex: bt2020).
273            colorTrc (str): The color trc of the video stream.
274            DOVIBLCompatID (int): Dolby Vision base layer compatibility ID.
275            DOVIBLPresent (bool): True if Dolby Vision base layer is present.
276            DOVIELPresent (bool): True if Dolby Vision enhancement layer is present.
277            DOVILevel (int): Dolby Vision level.
278            DOVIPresent (bool): True if Dolby Vision is present.
279            DOVIProfile (int): Dolby Vision profile.
280            DOVIRPUPresent (bool): True if Dolby Vision reference processing unit is present.
281            DOVIVersion (float): The Dolby Vision version.
282            duration (int): The duration of video stream in milliseconds.
283            frameRate (float): The frame rate of the video stream (ex: 23.976).
284            frameRateMode (str): The frame rate mode of the video stream.
285            hasScallingMatrix (bool): True if video stream has a scaling matrix.
286            height (int): The hight of the video stream in pixels (ex: 1080).
287            level (int): The codec encoding level of the video stream (ex: 41).
288            profile (str): The profile of the video stream (ex: asp).
289            pixelAspectRatio (str): The pixel aspect ratio of the video stream.
290            pixelFormat (str): The pixel format of the video stream.
291            refFrames (int): The number of reference frames of the video stream.
292            scanType (str): The scan type of the video stream (ex: progressive).
293            streamIdentifier(int): The stream identifier of the video stream.
294            width (int): The width of the video stream in pixels (ex: 1920).
295    """
296    TAG = 'Stream'
297    STREAMTYPE = 1
298
299    def _loadData(self, data):
300        """ Load attribute values from Plex XML response. """
301        super(VideoStream, self)._loadData(data)
302        self.anamorphic = data.attrib.get('anamorphic')
303        self.bitDepth = utils.cast(int, data.attrib.get('bitDepth'))
304        self.cabac = utils.cast(int, data.attrib.get('cabac'))
305        self.chromaLocation = data.attrib.get('chromaLocation')
306        self.chromaSubsampling = data.attrib.get('chromaSubsampling')
307        self.codecID = data.attrib.get('codecID')
308        self.codedHeight = utils.cast(int, data.attrib.get('codedHeight'))
309        self.codedWidth = utils.cast(int, data.attrib.get('codedWidth'))
310        self.colorPrimaries = data.attrib.get('colorPrimaries')
311        self.colorRange = data.attrib.get('colorRange')
312        self.colorSpace = data.attrib.get('colorSpace')
313        self.colorTrc = data.attrib.get('colorTrc')
314        self.DOVIBLCompatID = utils.cast(int, data.attrib.get('DOVIBLCompatID'))
315        self.DOVIBLPresent = utils.cast(bool, data.attrib.get('DOVIBLPresent'))
316        self.DOVIELPresent = utils.cast(bool, data.attrib.get('DOVIELPresent'))
317        self.DOVILevel = utils.cast(int, data.attrib.get('DOVILevel'))
318        self.DOVIPresent = utils.cast(bool, data.attrib.get('DOVIPresent'))
319        self.DOVIProfile = utils.cast(int, data.attrib.get('DOVIProfile'))
320        self.DOVIRPUPresent = utils.cast(bool, data.attrib.get('DOVIRPUPresent'))
321        self.DOVIVersion = utils.cast(float, data.attrib.get('DOVIVersion'))
322        self.duration = utils.cast(int, data.attrib.get('duration'))
323        self.frameRate = utils.cast(float, data.attrib.get('frameRate'))
324        self.frameRateMode = data.attrib.get('frameRateMode')
325        self.hasScallingMatrix = utils.cast(bool, data.attrib.get('hasScallingMatrix'))
326        self.height = utils.cast(int, data.attrib.get('height'))
327        self.level = utils.cast(int, data.attrib.get('level'))
328        self.profile = data.attrib.get('profile')
329        self.pixelAspectRatio = data.attrib.get('pixelAspectRatio')
330        self.pixelFormat = data.attrib.get('pixelFormat')
331        self.refFrames = utils.cast(int, data.attrib.get('refFrames'))
332        self.scanType = data.attrib.get('scanType')
333        self.streamIdentifier = utils.cast(int, data.attrib.get('streamIdentifier'))
334        self.width = utils.cast(int, data.attrib.get('width'))
335
336
337@utils.registerPlexObject
338class AudioStream(MediaPartStream):
339    """ Represents a audio stream within a :class:`~plexapi.media.MediaPart`.
340
341        Attributes:
342            TAG (str): 'Stream'
343            STREAMTYPE (int): 2
344            audioChannelLayout (str): The audio channel layout of the audio stream (ex: 5.1(side)).
345            bitDepth (int): The bit depth of the audio stream (ex: 16).
346            bitrateMode (str): The bitrate mode of the audio stream (ex: cbr).
347            channels (int): The number of audio channels of the audio stream (ex: 6).
348            duration (int): The duration of audio stream in milliseconds.
349            profile (str): The profile of the audio stream.
350            samplingRate (int): The sampling rate of the audio stream (ex: xxx)
351            streamIdentifier (int): The stream identifier of the audio stream.
352
353            <Track_only_attributes>: The following attributes are only available for tracks.
354
355                * albumGain (float): The gain for the album.
356                * albumPeak (float): The peak for the album.
357                * albumRange (float): The range for the album.
358                * endRamp (str): The end ramp for the track.
359                * gain (float): The gain for the track.
360                * loudness (float): The loudness for the track.
361                * lra (float): The lra for the track.
362                * peak (float): The peak for the track.
363                * startRamp (str): The start ramp for the track.
364    """
365    TAG = 'Stream'
366    STREAMTYPE = 2
367
368    def _loadData(self, data):
369        """ Load attribute values from Plex XML response. """
370        super(AudioStream, self)._loadData(data)
371        self.audioChannelLayout = data.attrib.get('audioChannelLayout')
372        self.bitDepth = utils.cast(int, data.attrib.get('bitDepth'))
373        self.bitrateMode = data.attrib.get('bitrateMode')
374        self.channels = utils.cast(int, data.attrib.get('channels'))
375        self.duration = utils.cast(int, data.attrib.get('duration'))
376        self.profile = data.attrib.get('profile')
377        self.samplingRate = utils.cast(int, data.attrib.get('samplingRate'))
378        self.streamIdentifier = utils.cast(int, data.attrib.get('streamIdentifier'))
379
380        if self._isChildOf(etag='Track'):
381            self.albumGain = utils.cast(float, data.attrib.get('albumGain'))
382            self.albumPeak = utils.cast(float, data.attrib.get('albumPeak'))
383            self.albumRange = utils.cast(float, data.attrib.get('albumRange'))
384            self.endRamp = data.attrib.get('endRamp')
385            self.gain = utils.cast(float, data.attrib.get('gain'))
386            self.loudness = utils.cast(float, data.attrib.get('loudness'))
387            self.lra = utils.cast(float, data.attrib.get('lra'))
388            self.peak = utils.cast(float, data.attrib.get('peak'))
389            self.startRamp = data.attrib.get('startRamp')
390
391
392@utils.registerPlexObject
393class SubtitleStream(MediaPartStream):
394    """ Represents a audio stream within a :class:`~plexapi.media.MediaPart`.
395
396        Attributes:
397            TAG (str): 'Stream'
398            STREAMTYPE (int): 3
399            container (str): The container of the subtitle stream.
400            forced (bool): True if this is a forced subtitle.
401            format (str): The format of the subtitle stream (ex: srt).
402            headerCommpression (str): The header compression of the subtitle stream.
403            transient (str): Unknown.
404    """
405    TAG = 'Stream'
406    STREAMTYPE = 3
407
408    def _loadData(self, data):
409        """ Load attribute values from Plex XML response. """
410        super(SubtitleStream, self)._loadData(data)
411        self.container = data.attrib.get('container')
412        self.forced = utils.cast(bool, data.attrib.get('forced', '0'))
413        self.format = data.attrib.get('format')
414        self.headerCompression = data.attrib.get('headerCompression')
415        self.transient = data.attrib.get('transient')
416
417
418class LyricStream(MediaPartStream):
419    """ Represents a lyric stream within a :class:`~plexapi.media.MediaPart`.
420
421        Attributes:
422            TAG (str): 'Stream'
423            STREAMTYPE (int): 4
424            format (str): The format of the lyric stream (ex: lrc).
425            minLines (int): The minimum number of lines in the (timed) lyric stream.
426            provider (str): The provider of the lyric stream (ex: com.plexapp.agents.lyricfind).
427            timed (bool): True if the lyrics are timed to the track.
428    """
429    TAG = 'Stream'
430    STREAMTYPE = 4
431
432    def _loadData(self, data):
433        """ Load attribute values from Plex XML response. """
434        super(LyricStream, self)._loadData(data)
435        self.format = data.attrib.get('format')
436        self.minLines = utils.cast(int, data.attrib.get('minLines'))
437        self.provider = data.attrib.get('provider')
438        self.timed = utils.cast(bool, data.attrib.get('timed', '0'))
439
440
441@utils.registerPlexObject
442class Session(PlexObject):
443    """ Represents a current session.
444
445        Attributes:
446            TAG (str): 'Session'
447            id (str): The unique identifier for the session.
448            bandwidth (int): The Plex streaming brain reserved bandwidth for the session.
449            location (str): The location of the session (lan, wan, or cellular)
450    """
451    TAG = 'Session'
452
453    def _loadData(self, data):
454        self.id = data.attrib.get('id')
455        self.bandwidth = utils.cast(int, data.attrib.get('bandwidth'))
456        self.location = data.attrib.get('location')
457
458
459@utils.registerPlexObject
460class TranscodeSession(PlexObject):
461    """ Represents a current transcode session.
462
463        Attributes:
464            TAG (str): 'TranscodeSession'
465            audioChannels (int): The number of audio channels of the transcoded media.
466            audioCodec (str): The audio codec of the transcoded media.
467            audioDecision (str): The transcode decision for the audio stream.
468            complete (bool): True if the transcode is complete.
469            container (str): The container of the transcoded media.
470            context (str): The context for the transcode sesson.
471            duration (int): The duration of the transcoded media in milliseconds.
472            height (int): The height of the transcoded media in pixels.
473            key (str): API URL (ex: /transcode/sessions/<id>).
474            maxOffsetAvailable (float): Unknown.
475            minOffsetAvailable (float): Unknown.
476            progress (float): The progress percentage of the transcode.
477            protocol (str): The protocol of the transcode.
478            remaining (int): Unknown.
479            size (int): The size of the transcoded media in bytes.
480            sourceAudioCodec (str): The audio codec of the source media.
481            sourceVideoCodec (str): The video codec of the source media.
482            speed (float): The speed of the transcode.
483            subtitleDecision (str): The transcode decision for the subtitle stream
484            throttled (bool): True if the transcode is throttled.
485            timestamp (int): The epoch timestamp when the transcode started.
486            transcodeHwDecoding (str): The hardware transcoding decoder engine.
487            transcodeHwDecodingTitle (str): The title of the hardware transcoding decoder engine.
488            transcodeHwEncoding (str): The hardware transcoding encoder engine.
489            transcodeHwEncodingTitle (str): The title of the hardware transcoding encoder engine.
490            transcodeHwFullPipeline (str): True if hardware decoding and encoding is being used for the transcode.
491            transcodeHwRequested (str): True if hardware transcoding was requested for the transcode.
492            videoCodec (str): The video codec of the transcoded media.
493            videoDecision (str): The transcode decision for the video stream.
494            width (str): The width of the transcoded media in pixels.
495    """
496    TAG = 'TranscodeSession'
497
498    def _loadData(self, data):
499        """ Load attribute values from Plex XML response. """
500        self._data = data
501        self.audioChannels = utils.cast(int, data.attrib.get('audioChannels'))
502        self.audioCodec = data.attrib.get('audioCodec')
503        self.audioDecision = data.attrib.get('audioDecision')
504        self.complete = utils.cast(bool, data.attrib.get('complete', '0'))
505        self.container = data.attrib.get('container')
506        self.context = data.attrib.get('context')
507        self.duration = utils.cast(int, data.attrib.get('duration'))
508        self.height = utils.cast(int, data.attrib.get('height'))
509        self.key = data.attrib.get('key')
510        self.maxOffsetAvailable = utils.cast(float, data.attrib.get('maxOffsetAvailable'))
511        self.minOffsetAvailable = utils.cast(float, data.attrib.get('minOffsetAvailable'))
512        self.progress = utils.cast(float, data.attrib.get('progress'))
513        self.protocol = data.attrib.get('protocol')
514        self.remaining = utils.cast(int, data.attrib.get('remaining'))
515        self.size = utils.cast(int, data.attrib.get('size'))
516        self.sourceAudioCodec = data.attrib.get('sourceAudioCodec')
517        self.sourceVideoCodec = data.attrib.get('sourceVideoCodec')
518        self.speed = utils.cast(float, data.attrib.get('speed'))
519        self.subtitleDecision = data.attrib.get('subtitleDecision')
520        self.throttled = utils.cast(bool, data.attrib.get('throttled', '0'))
521        self.timestamp = utils.cast(float, data.attrib.get('timeStamp'))
522        self.transcodeHwDecoding = data.attrib.get('transcodeHwDecoding')
523        self.transcodeHwDecodingTitle = data.attrib.get('transcodeHwDecodingTitle')
524        self.transcodeHwEncoding = data.attrib.get('transcodeHwEncoding')
525        self.transcodeHwEncodingTitle = data.attrib.get('transcodeHwEncodingTitle')
526        self.transcodeHwFullPipeline = utils.cast(bool, data.attrib.get('transcodeHwFullPipeline', '0'))
527        self.transcodeHwRequested = utils.cast(bool, data.attrib.get('transcodeHwRequested', '0'))
528        self.videoCodec = data.attrib.get('videoCodec')
529        self.videoDecision = data.attrib.get('videoDecision')
530        self.width = utils.cast(int, data.attrib.get('width'))
531
532
533@utils.registerPlexObject
534class TranscodeJob(PlexObject):
535    """ Represents an Optimizing job.
536        TrancodeJobs are the process for optimizing conversions.
537        Active or paused optimization items. Usually one item as a time."""
538    TAG = 'TranscodeJob'
539
540    def _loadData(self, data):
541        self._data = data
542        self.generatorID = data.attrib.get('generatorID')
543        self.key = data.attrib.get('key')
544        self.progress = data.attrib.get('progress')
545        self.ratingKey = data.attrib.get('ratingKey')
546        self.size = data.attrib.get('size')
547        self.targetTagID = data.attrib.get('targetTagID')
548        self.thumb = data.attrib.get('thumb')
549        self.title = data.attrib.get('title')
550        self.type = data.attrib.get('type')
551
552
553@utils.registerPlexObject
554class Optimized(PlexObject):
555    """ Represents a Optimized item.
556        Optimized items are optimized and queued conversions items."""
557    TAG = 'Item'
558
559    def _loadData(self, data):
560        self._data = data
561        self.id = data.attrib.get('id')
562        self.composite = data.attrib.get('composite')
563        self.title = data.attrib.get('title')
564        self.type = data.attrib.get('type')
565        self.target = data.attrib.get('target')
566        self.targetTagID = data.attrib.get('targetTagID')
567
568    def items(self):
569        """ Returns a list of all :class:`~plexapi.media.Video` objects
570            in this optimized item.
571        """
572        key = '%s/%s/items' % (self._initpath, self.id)
573        return self.fetchItems(key)
574
575    def remove(self):
576        """ Remove an Optimized item"""
577        key = '%s/%s' % (self._initpath, self.id)
578        self._server.query(key, method=self._server._session.delete)
579
580    def rename(self, title):
581        """ Rename an Optimized item"""
582        key = '%s/%s?Item[title]=%s' % (self._initpath, self.id, title)
583        self._server.query(key, method=self._server._session.put)
584
585    def reprocess(self, ratingKey):
586        """ Reprocess a removed Conversion item that is still a listed Optimize item"""
587        key = '%s/%s/%s/enable' % (self._initpath, self.id, ratingKey)
588        self._server.query(key, method=self._server._session.put)
589
590
591@utils.registerPlexObject
592class Conversion(PlexObject):
593    """ Represents a Conversion item.
594        Conversions are items queued for optimization or being actively optimized."""
595    TAG = 'Video'
596
597    def _loadData(self, data):
598        self._data = data
599        self.addedAt = data.attrib.get('addedAt')
600        self.art = data.attrib.get('art')
601        self.chapterSource = data.attrib.get('chapterSource')
602        self.contentRating = data.attrib.get('contentRating')
603        self.duration = data.attrib.get('duration')
604        self.generatorID = data.attrib.get('generatorID')
605        self.generatorType = data.attrib.get('generatorType')
606        self.guid = data.attrib.get('guid')
607        self.key = data.attrib.get('key')
608        self.lastViewedAt = data.attrib.get('lastViewedAt')
609        self.librarySectionID = data.attrib.get('librarySectionID')
610        self.librarySectionKey = data.attrib.get('librarySectionKey')
611        self.librarySectionTitle = data.attrib.get('librarySectionTitle')
612        self.originallyAvailableAt = data.attrib.get('originallyAvailableAt')
613        self.playQueueItemID = data.attrib.get('playQueueItemID')
614        self.playlistID = data.attrib.get('playlistID')
615        self.primaryExtraKey = data.attrib.get('primaryExtraKey')
616        self.rating = data.attrib.get('rating')
617        self.ratingKey = data.attrib.get('ratingKey')
618        self.studio = data.attrib.get('studio')
619        self.summary = data.attrib.get('summary')
620        self.tagline = data.attrib.get('tagline')
621        self.target = data.attrib.get('target')
622        self.thumb = data.attrib.get('thumb')
623        self.title = data.attrib.get('title')
624        self.type = data.attrib.get('type')
625        self.updatedAt = data.attrib.get('updatedAt')
626        self.userID = data.attrib.get('userID')
627        self.username = data.attrib.get('username')
628        self.viewOffset = data.attrib.get('viewOffset')
629        self.year = data.attrib.get('year')
630
631    def remove(self):
632        """ Remove Conversion from queue """
633        key = '/playlists/%s/items/%s/%s/disable' % (self.playlistID, self.generatorID, self.ratingKey)
634        self._server.query(key, method=self._server._session.put)
635
636    def move(self, after):
637        """ Move Conversion items position in queue
638            after (int): Place item after specified playQueueItemID. '-1' is the active conversion.
639
640                Example:
641                    Move 5th conversion Item to active conversion
642                        conversions[4].move('-1')
643
644                    Move 4th conversion Item to 3rd in conversion queue
645                        conversions[3].move(conversions[1].playQueueItemID)
646        """
647
648        key = '%s/items/%s/move?after=%s' % (self._initpath, self.playQueueItemID, after)
649        self._server.query(key, method=self._server._session.put)
650
651
652class MediaTag(PlexObject):
653    """ Base class for media tags used for filtering and searching your library
654        items or navigating the metadata of media items in your library. Tags are
655        the construct used for things such as Country, Director, Genre, etc.
656
657        Attributes:
658            filter (str): The library filter for the tag.
659            id (id): Tag ID (This seems meaningless except to use it as a unique id).
660            key (str): API URL (/library/section/<librarySectionID>/all?<filter>).
661            role (str): The name of the character role for :class:`~plexapi.media.Role` only.
662            tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
663                person for Directors and Roles (ex: Animation, Stephen Graham, etc).
664            thumb (str): URL to thumbnail image for :class:`~plexapi.media.Role` only.
665    """
666
667    def _loadData(self, data):
668        """ Load attribute values from Plex XML response. """
669        self._data = data
670        self.filter = data.attrib.get('filter')
671        self.id = utils.cast(int, data.attrib.get('id'))
672        self.key = data.attrib.get('key')
673        self.role = data.attrib.get('role')
674        self.tag = data.attrib.get('tag')
675        self.thumb = data.attrib.get('thumb')
676
677        parent = self._parent()
678        self._librarySectionID = utils.cast(int, parent._data.attrib.get('librarySectionID'))
679        self._librarySectionKey = parent._data.attrib.get('librarySectionKey')
680        self._librarySectionTitle = parent._data.attrib.get('librarySectionTitle')
681        self._parentType = parent.TYPE
682
683        if self._librarySectionKey and self.filter:
684            self.key = '%s/all?%s&type=%s' % (
685                self._librarySectionKey, self.filter, utils.searchType(self._parentType))
686
687    def items(self):
688        """ Return the list of items within this tag. """
689        if not self.key:
690            raise BadRequest('Key is not defined for this tag: %s. '
691                             'Reload the parent object.' % self.tag)
692        return self.fetchItems(self.key)
693
694
695@utils.registerPlexObject
696class Collection(MediaTag):
697    """ Represents a single Collection media tag.
698
699        Attributes:
700            TAG (str): 'Collection'
701            FILTER (str): 'collection'
702    """
703    TAG = 'Collection'
704    FILTER = 'collection'
705
706    def collection(self):
707        """ Return the :class:`~plexapi.collection.Collection` object for this collection tag.
708        """
709        key = '%s/collections' % self._librarySectionKey
710        return self.fetchItem(key, etag='Directory', index=self.id)
711
712
713@utils.registerPlexObject
714class Country(MediaTag):
715    """ Represents a single Country media tag.
716
717        Attributes:
718            TAG (str): 'Country'
719            FILTER (str): 'country'
720    """
721    TAG = 'Country'
722    FILTER = 'country'
723
724
725@utils.registerPlexObject
726class Director(MediaTag):
727    """ Represents a single Director media tag.
728
729        Attributes:
730            TAG (str): 'Director'
731            FILTER (str): 'director'
732    """
733    TAG = 'Director'
734    FILTER = 'director'
735
736
737@utils.registerPlexObject
738class Format(MediaTag):
739    """ Represents a single Format media tag.
740
741        Attributes:
742            TAG (str): 'Format'
743            FILTER (str): 'format'
744    """
745    TAG = 'Format'
746    FILTER = 'format'
747
748
749@utils.registerPlexObject
750class Genre(MediaTag):
751    """ Represents a single Genre media tag.
752
753        Attributes:
754            TAG (str): 'Genre'
755            FILTER (str): 'genre'
756    """
757    TAG = 'Genre'
758    FILTER = 'genre'
759
760
761@utils.registerPlexObject
762class Label(MediaTag):
763    """ Represents a single Label media tag.
764
765        Attributes:
766            TAG (str): 'Label'
767            FILTER (str): 'label'
768    """
769    TAG = 'Label'
770    FILTER = 'label'
771
772
773@utils.registerPlexObject
774class Mood(MediaTag):
775    """ Represents a single Mood media tag.
776
777        Attributes:
778            TAG (str): 'Mood'
779            FILTER (str): 'mood'
780    """
781    TAG = 'Mood'
782    FILTER = 'mood'
783
784
785@utils.registerPlexObject
786class Producer(MediaTag):
787    """ Represents a single Producer media tag.
788
789        Attributes:
790            TAG (str): 'Producer'
791            FILTER (str): 'producer'
792    """
793    TAG = 'Producer'
794    FILTER = 'producer'
795
796
797@utils.registerPlexObject
798class Role(MediaTag):
799    """ Represents a single Role (actor/actress) media tag.
800
801        Attributes:
802            TAG (str): 'Role'
803            FILTER (str): 'role'
804    """
805    TAG = 'Role'
806    FILTER = 'role'
807
808
809@utils.registerPlexObject
810class Similar(MediaTag):
811    """ Represents a single Similar media tag.
812
813        Attributes:
814            TAG (str): 'Similar'
815            FILTER (str): 'similar'
816    """
817    TAG = 'Similar'
818    FILTER = 'similar'
819
820
821@utils.registerPlexObject
822class Style(MediaTag):
823    """ Represents a single Style media tag.
824
825        Attributes:
826            TAG (str): 'Style'
827            FILTER (str): 'style'
828    """
829    TAG = 'Style'
830    FILTER = 'style'
831
832
833@utils.registerPlexObject
834class Subformat(MediaTag):
835    """ Represents a single Subformat media tag.
836
837        Attributes:
838            TAG (str): 'Subformat'
839            FILTER (str): 'subformat'
840    """
841    TAG = 'Subformat'
842    FILTER = 'subformat'
843
844
845@utils.registerPlexObject
846class Tag(MediaTag):
847    """ Represents a single Tag media tag.
848
849        Attributes:
850            TAG (str): 'Tag'
851            FILTER (str): 'tag'
852    """
853    TAG = 'Tag'
854    FILTER = 'tag'
855
856
857@utils.registerPlexObject
858class Writer(MediaTag):
859    """ Represents a single Writer media tag.
860
861        Attributes:
862            TAG (str): 'Writer'
863            FILTER (str): 'writer'
864    """
865    TAG = 'Writer'
866    FILTER = 'writer'
867
868
869class GuidTag(PlexObject):
870    """ Base class for guid tags used only for Guids, as they contain only a string identifier
871
872        Attributes:
873            id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB).
874    """
875
876    def _loadData(self, data):
877        """ Load attribute values from Plex XML response. """
878        self._data = data
879        self.id = data.attrib.get('id')
880
881
882@utils.registerPlexObject
883class Guid(GuidTag):
884    """ Represents a single Guid media tag.
885
886        Attributes:
887            TAG (str): 'Guid'
888    """
889    TAG = 'Guid'
890
891
892@utils.registerPlexObject
893class Review(PlexObject):
894    """ Represents a single Review for a Movie.
895
896        Attributes:
897            TAG (str): 'Review'
898            filter (str): filter for reviews?
899            id (int): The ID of the review.
900            image (str): The image uri for the review.
901            link (str): The url to the online review.
902            source (str): The source of the review.
903            tag (str): The name of the reviewer.
904            text (str): The text of the review.
905    """
906    TAG = 'Review'
907
908    def _loadData(self, data):
909        self._data = data
910        self.filter = data.attrib.get('filter')
911        self.id = utils.cast(int, data.attrib.get('id', 0))
912        self.image = data.attrib.get('image')
913        self.link = data.attrib.get('link')
914        self.source = data.attrib.get('source')
915        self.tag = data.attrib.get('tag')
916        self.text = data.attrib.get('text')
917
918
919class BaseImage(PlexObject):
920    """ Base class for all Art, Banner, and Poster objects.
921
922        Attributes:
923            TAG (str): 'Photo'
924            key (str): API URL (/library/metadata/<ratingkey>).
925            provider (str): The source of the poster or art.
926            ratingKey (str): Unique key identifying the poster or art.
927            selected (bool): True if the poster or art is currently selected.
928            thumb (str): The URL to retrieve the poster or art thumbnail.
929    """
930    TAG = 'Photo'
931
932    def _loadData(self, data):
933        self._data = data
934        self.key = data.attrib.get('key')
935        self.provider = data.attrib.get('provider')
936        self.ratingKey = data.attrib.get('ratingKey')
937        self.selected = utils.cast(bool, data.attrib.get('selected'))
938        self.thumb = data.attrib.get('thumb')
939
940    def select(self):
941        key = self._initpath[:-1]
942        data = '%s?url=%s' % (key, quote_plus(self.ratingKey))
943        try:
944            self._server.query(data, method=self._server._session.put)
945        except xml.etree.ElementTree.ParseError:
946            pass
947
948
949class Art(BaseImage):
950    """ Represents a single Art object. """
951
952
953class Banner(BaseImage):
954    """ Represents a single Banner object. """
955
956
957class Poster(BaseImage):
958    """ Represents a single Poster object. """
959
960
961@utils.registerPlexObject
962class Chapter(PlexObject):
963    """ Represents a single Writer media tag.
964
965        Attributes:
966            TAG (str): 'Chapter'
967    """
968    TAG = 'Chapter'
969
970    def _loadData(self, data):
971        self._data = data
972        self.id = utils.cast(int, data.attrib.get('id', 0))
973        self.filter = data.attrib.get('filter')  # I couldn't filter on it anyways
974        self.tag = data.attrib.get('tag')
975        self.title = self.tag
976        self.index = utils.cast(int, data.attrib.get('index'))
977        self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
978        self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
979
980
981@utils.registerPlexObject
982class Marker(PlexObject):
983    """ Represents a single Marker media tag.
984
985        Attributes:
986            TAG (str): 'Marker'
987    """
988    TAG = 'Marker'
989
990    def __repr__(self):
991        name = self._clean(self.firstAttr('type'))
992        start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
993        end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end')))
994        offsets = '%s-%s' % (start, end)
995        return '<%s>' % ':'.join([self.__class__.__name__, name, offsets])
996
997    def _loadData(self, data):
998        self._data = data
999        self.id = utils.cast(int, data.attrib.get('id'))
1000        self.type = data.attrib.get('type')
1001        self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
1002        self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
1003
1004
1005@utils.registerPlexObject
1006class Field(PlexObject):
1007    """ Represents a single Field.
1008
1009        Attributes:
1010            TAG (str): 'Field'
1011    """
1012    TAG = 'Field'
1013
1014    def _loadData(self, data):
1015        self._data = data
1016        self.name = data.attrib.get('name')
1017        self.locked = utils.cast(bool, data.attrib.get('locked'))
1018
1019
1020@utils.registerPlexObject
1021class SearchResult(PlexObject):
1022    """ Represents a single SearchResult.
1023
1024        Attributes:
1025            TAG (str): 'SearchResult'
1026    """
1027    TAG = 'SearchResult'
1028
1029    def __repr__(self):
1030        name = self._clean(self.firstAttr('name'))
1031        score = self._clean(self.firstAttr('score'))
1032        return '<%s>' % ':'.join([p for p in [self.__class__.__name__, name, score] if p])
1033
1034    def _loadData(self, data):
1035        self._data = data
1036        self.guid = data.attrib.get('guid')
1037        self.lifespanEnded = data.attrib.get('lifespanEnded')
1038        self.name = data.attrib.get('name')
1039        self.score = utils.cast(int, data.attrib.get('score'))
1040        self.year = data.attrib.get('year')
1041
1042
1043@utils.registerPlexObject
1044class Agent(PlexObject):
1045    """ Represents a single Agent.
1046
1047        Attributes:
1048            TAG (str): 'Agent'
1049    """
1050    TAG = 'Agent'
1051
1052    def __repr__(self):
1053        uid = self._clean(self.firstAttr('shortIdentifier'))
1054        return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid] if p])
1055
1056    def _loadData(self, data):
1057        self._data = data
1058        self.hasAttribution = data.attrib.get('hasAttribution')
1059        self.hasPrefs = data.attrib.get('hasPrefs')
1060        self.identifier = data.attrib.get('identifier')
1061        self.primary = data.attrib.get('primary')
1062        self.shortIdentifier = self.identifier.rsplit('.', 1)[1]
1063        if 'mediaType' in self._initpath:
1064            self.name = data.attrib.get('name')
1065            self.languageCode = []
1066            for code in data:
1067                self.languageCode += [code.attrib.get('code')]
1068        else:
1069            self.mediaTypes = [AgentMediaType(server=self._server, data=d) for d in data]
1070
1071    def _settings(self):
1072        key = '/:/plugins/%s/prefs' % self.identifier
1073        data = self._server.query(key)
1074        return self.findItems(data, cls=settings.Setting)
1075
1076
1077class AgentMediaType(Agent):
1078
1079    def __repr__(self):
1080        uid = self._clean(self.firstAttr('name'))
1081        return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid] if p])
1082
1083    def _loadData(self, data):
1084        self.mediaType = utils.cast(int, data.attrib.get('mediaType'))
1085        self.name = data.attrib.get('name')
1086        self.languageCode = []
1087        for code in data:
1088            self.languageCode += [code.attrib.get('code')]
1089