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