Update to MediaPartStream and inheriting classes (#590)

* moving common child (VideoStream, AudioStream, SubtitleStream) attributes to the parent (MediaPartStream)

* removal of no longer present attribs in video, audio, subtitle streams

* additional attribs to video and mediapart streams

* removal of previously unique subtitleStream attribs. attribs are now common or no longer present.

* docstring cleanup of video, audio, and subtitle stream; pass 1

* remove codecID checks as this attribute is now longer used.

* adding key attrib to parent MediaPartStream.

* add transient to SubtitleStream, found in uploaded subtitles

* removing dialogNorm related assertion as this attrib has been removed.

* update Media class based on PR comments
optimizedVersion to be added, need SEARCHTYPES

* update MediaPartStream class based on PR comments

* update VideoStream class based on PR comments

* update AudioStream class based on PR comments

* update SubtitleStream class based on PR comments

* add LyricStream class based on PR comments

* add streamType int and LyricStream to MediaPartStream.parse method

* add MediaPart.lyricStreams method

* spelling correction

* more movement based on PR comments

* alpha ordering MediaPart

* alpha sort VideoStream

* docstring corrections

* remove assert stream.dialogNorm from audio test as dialogNorm has been removed.

* adding LyricStream to _buildStreams function

* adding changes from @johnnywong16
e346f0b4dc

* moving unused imports down and together

* Address docstring review comments

* Change isChildOf for any kwargs

* Add Media.isOptimizedVersion property

* Add photo and track specific attributes

* Remove dead MediaPartStream.parse method

* Update media doc strings

* Add optimized version doc string

* Cast video stream codedHeight and codedWidth to int

* Add required bandwidths to streams

* Update video tests

* Update audio tests

* Fix video tests

* Fix video tests

* Fix video tests

Co-authored-by: Jason Lawrence <jjlawren@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
This commit is contained in:
blacktwin 2021-01-24 15:21:56 -05:00 committed by GitHub
parent 4da40789ea
commit 4f0910ab79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 316 additions and 138 deletions

View file

@ -110,17 +110,20 @@ class PlexObject(object):
details_key += '?' + urlencode(sorted(includes.items()))
return details_key
def _isChildOf(self, cls):
""" Returns True if this object is a child of the given class.
def _isChildOf(self, **kwargs):
""" Returns True if this object is a child of the given attributes.
This will search the parent objects all the way to the top.
Parameters:
cls: The parent :class:`~plexapi.base.PlexObject` to search for.
**kwargs (dict): The attributes and values to search for in the parent objects.
See all possible `**kwargs*` in :func:`~plexapi.base.PlexObject.fetchItem`.
"""
obj = self
while obj._parent is not None:
if isinstance(obj._parent(), cls):
return True
obj = obj._parent()
if obj._checkAttrs(obj._data, **kwargs):
return True
return False
def fetchItem(self, ekey, cls=None, **kwargs):
""" Load the specified key to find and build the first item with the

View file

@ -12,31 +12,39 @@ from plexapi.utils import cast
@utils.registerPlexObject
class Media(PlexObject):
""" Container object for all MediaPart objects. Provides useful data about the
video this media belong to such as video framerate, resolution, etc.
video or audio this media belong to such as video framerate, resolution, etc.
Attributes:
TAG (str): 'Media'
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
initpath (str): Relative path requested when retrieving specified data.
video (str): Video this media belongs to.
aspectRatio (float): Aspect ratio of the video (ex: 2.35).
audioChannels (int): Number of audio channels for this video (ex: 6).
audioCodec (str): Audio codec used within the video (ex: ac3).
bitrate (int): Bitrate of the video (ex: 1624)
container (str): Container this video is in (ex: avi).
duration (int): Length of the video in milliseconds (ex: 6990483).
height (int): Height of the video in pixels (ex: 256).
id (int): Plex ID of this media item (ex: 46184).
has64bitOffsets (bool): True if video has 64 bit offsets (?).
aspectRatio (float): The aspect ratio of the media (ex: 2.35).
audioChannels (int): The number of audio channels of the media (ex: 6).
audioCodec (str): The audio codec of the media (ex: ac3).
audioProfile (str): The audio profile of the media (ex: dts).
bitrate (int): The bitrate of the media (ex: 1624).
container (str): The container of the media (ex: avi).
duration (int): The duration of the media in milliseconds (ex: 6990483).
height (int): The height of the media in pixels (ex: 256).
id (int): The unique ID for this media on the server.
has64bitOffsets (bool): True if video has 64 bit offsets.
optimizedForStreaming (bool): True if video is optimized for streaming.
target (str): Media version target name.
title (str): Media version title.
videoCodec (str): Video codec used within the video (ex: ac3).
videoFrameRate (str): Video frame rate (ex: 24p).
videoResolution (str): Video resolution (ex: sd).
videoProfile (str): Video profile (ex: high).
width (int): Width of the video in pixels (ex: 608).
parts (list<:class:`~plexapi.media.MediaPart`>): List of MediaParts in this video.
parts (List<:class:`~plexapi.media.MediaPart`>): List of media part objects.
proxyType (int): Equals 42 for optimized versions.
target (str): The media version target name.
title (str): The title of the media.
videoCodec (str): The video codec of the media (ex: ac3).
videoFrameRate (str): The video frame rate of the media (ex: 24p).
videoProfile (str): The video profile of the media (ex: high).
videoResolution (str): The video resolution of the media (ex: sd).
width (int): The width of the video in pixels (ex: 608).
<Photo_only_attributes>: The following attributes are only available for photos.
* aperture (str): The apeture used to take the photo.
* exposure (str): The exposure used to take the photo.
* iso (int): The iso used to take the photo.
* lens (str): The lens used to take the photo.
* make (str): The make of the camera used to take the photo.
* model (str): The model of the camera used to take the photo.
"""
TAG = 'Media'
@ -46,6 +54,7 @@ class Media(PlexObject):
self.aspectRatio = cast(float, data.attrib.get('aspectRatio'))
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
self.audioCodec = data.attrib.get('audioCodec')
self.audioProfile = data.attrib.get('audioProfile')
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.container = data.attrib.get('container')
self.duration = cast(int, data.attrib.get('duration'))
@ -53,6 +62,8 @@ class Media(PlexObject):
self.id = cast(int, data.attrib.get('id'))
self.has64bitOffsets = cast(bool, data.attrib.get('has64bitOffsets'))
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
self.parts = self.findItems(data, MediaPart)
self.proxyType = cast(int, data.attrib.get('proxyType'))
self.target = data.attrib.get('target')
self.title = data.attrib.get('title')
self.videoCodec = data.attrib.get('videoCodec')
@ -60,7 +71,19 @@ class Media(PlexObject):
self.videoProfile = data.attrib.get('videoProfile')
self.videoResolution = data.attrib.get('videoResolution')
self.width = cast(int, data.attrib.get('width'))
self.parts = self.findItems(data, MediaPart)
if self._isChildOf(etag='Photo'):
self.aperture = data.attrib.get('aperture')
self.exposure = data.attrib.get('exposure')
self.iso = cast(int, data.attrib.get('iso'))
self.lens = data.attrib.get('lens')
self.make = data.attrib.get('make')
self.model = data.attrib.get('model')
@property
def isOptimizedVersion(self):
""" Returns True if the media is a Plex optimized version. """
return self.proxyType == utils.SEARCHTYPES['optimizedVersion']
def delete(self):
part = self._initpath + '/media/%s' % self.id
@ -78,60 +101,77 @@ class MediaPart(PlexObject):
Attributes:
TAG (str): 'Part'
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
initpath (str): Relative path requested when retrieving specified data.
media (:class:`~plexapi.media.Media`): Media object this part belongs to.
container (str): Container type of this media part (ex: avi).
duration (int): Length of this media part in milliseconds.
file (str): Path to this file on disk (ex: /media/Movies/Cars.(2006)/Cars.cd2.avi)
id (int): Unique ID of this media part.
indexes (str, None): None or SD.
key (str): Key used to access this media part (ex: /library/parts/46618/1389985872/file.avi).
size (int): Size of this file in bytes (ex: 733884416).
streams (list<:class:`~plexapi.media.MediaPartStream`>): List of streams in this media part.
exists (bool): Determine if file exists
accessible (bool): Determine if file is accessible
accessible (bool): True if the file is accessible.
audioProfile (str): The audio profile of the file.
container (str): The container type of the file (ex: avi).
decision (str): Unknown.
deepAnalysisVersion (int): The Plex deep analysis version for the file.
duration (int): The duration of the file in milliseconds.
exists (bool): True if the file exists.
file (str): The path to this file on disk (ex: /media/Movies/Cars (2006)/Cars (2006).mkv)
has64bitOffsets (bool): True if the file has 64 bit offsets.
hasThumbnail (bool): True if the file (track) has an embedded thumbnail.
id (int): The unique ID for this media part on the server.
indexes (str, None): sd if the file has generated BIF thumbnails.
key (str): API URL (ex: /library/parts/46618/1389985872/file.mkv).
optimizedForStreaming (bool): True if the file is optimized for streaming.
packetLength (int): The packet length of the file.
requiredBandwidths (str): The required bandwidths to stream the file.
size (int): The size of the file in bytes (ex: 733884416).
streams (List<:class:`~plexapi.media.MediaPartStream`>): List of stream objects.
syncItemId (int): The unique ID for this media part if it is synced.
syncState (str): The sync state for this media part.
videoProfile (str): The video profile of the file.
"""
TAG = 'Part'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.accessible = cast(bool, data.attrib.get('accessible'))
self.audioProfile = data.attrib.get('audioProfile')
self.container = data.attrib.get('container')
self.decision = data.attrib.get('decision')
self.deepAnalysisVersion = cast(int, data.attrib.get('deepAnalysisVersion'))
self.duration = cast(int, data.attrib.get('duration'))
self.exists = cast(bool, data.attrib.get('exists'))
self.file = data.attrib.get('file')
self.has64bitOffsets = cast(bool, data.attrib.get('has64bitOffsets'))
self.hasThumbnail = cast(bool, data.attrib.get('hasThumbnail'))
self.id = cast(int, data.attrib.get('id'))
self.indexes = data.attrib.get('indexes')
self.key = data.attrib.get('key')
self.size = cast(int, data.attrib.get('size'))
self.decision = data.attrib.get('decision')
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
self.packetLength = cast(int, data.attrib.get('packetLength'))
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
self.size = cast(int, data.attrib.get('size'))
self.streams = self._buildStreams(data)
self.syncItemId = cast(int, data.attrib.get('syncItemId'))
self.syncState = data.attrib.get('syncState')
self.videoProfile = data.attrib.get('videoProfile')
self.streams = self._buildStreams(data)
self.exists = cast(bool, data.attrib.get('exists'))
self.accessible = cast(bool, data.attrib.get('accessible'))
def _buildStreams(self, data):
streams = []
for elem in data:
for cls in (VideoStream, AudioStream, SubtitleStream):
if elem.attrib.get('streamType') == str(cls.STREAMTYPE):
streams.append(cls(self._server, elem, self._initpath))
for cls in (VideoStream, AudioStream, SubtitleStream, LyricStream):
items = self.findItems(data, cls, streamType=cls.STREAMTYPE)
streams.extend(items)
return streams
def videoStreams(self):
""" Returns a list of :class:`~plexapi.media.VideoStream` objects in this MediaPart. """
return [stream for stream in self.streams if stream.streamType == VideoStream.STREAMTYPE]
return [stream for stream in self.streams if isinstance(stream, VideoStream)]
def audioStreams(self):
""" Returns a list of :class:`~plexapi.media.AudioStream` objects in this MediaPart. """
return [stream for stream in self.streams if stream.streamType == AudioStream.STREAMTYPE]
return [stream for stream in self.streams if isinstance(stream, AudioStream)]
def subtitleStreams(self):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
return [stream for stream in self.streams if isinstance(stream, SubtitleStream)]
def lyricStreams(self):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
return [stream for stream in self.streams if isinstance(stream, LyricStream)]
def setDefaultAudioStream(self, stream):
""" Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
@ -164,69 +204,87 @@ class MediaPart(PlexObject):
class MediaPartStream(PlexObject):
""" Base class for media streams. These consist of video, audio and subtitles.
""" Base class for media streams. These consist of video, audio, subtitles, and lyrics.
Attributes:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
initpath (str): Relative path requested when retrieving specified data.
part (:class:`~plexapi.media.MediaPart`): Media part this stream belongs to.
codec (str): Codec of this stream (ex: srt, ac3, mpeg4).
codecID (str): Codec ID (ex: XVID).
id (int): Unique stream ID on this server.
index (int): Unknown
language (str): Stream language (ex: English, ไทย).
languageCode (str): Ascii code for language (ex: eng, tha).
bitrate (int): The bitrate of the stream.
codec (str): The codec of the stream (ex: srt, ac3, mpeg4).
default (bool): True if this is the default stream.
displayTitle (str): The display title of the stream.
extendedDisplayTitle (str): The extended display title of the stream.
key (str): API URL (/library/streams/<id>)
id (int): The unique ID for this stream on the server.
index (int): The index of the stream.
language (str): The language of the stream (ex: English, ไทย).
languageCode (str): The Ascii language code of the stream (ex: eng, tha).
requiredBandwidths (str): The required bandwidths to stream the file.
selected (bool): True if this stream is selected.
streamType (int): Stream type (1=:class:`~plexapi.media.VideoStream`,
2=:class:`~plexapi.media.AudioStream`, 3=:class:`~plexapi.media.SubtitleStream`).
streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`,
2= :class:`~plexapi.media.AudioStream`, 3= :class:`~plexapi.media.SubtitleStream`).
title (str): The title of the stream.
type (int): Alias for streamType.
"""
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.codec = data.attrib.get('codec')
self.codecID = data.attrib.get('codecID')
self.default = cast(bool, data.attrib.get('default'))
self.displayTitle = data.attrib.get('displayTitle')
self.extendedDisplayTitle = data.attrib.get('extendedDisplayTitle')
self.key = data.attrib.get('key')
self.id = cast(int, data.attrib.get('id'))
self.index = cast(int, data.attrib.get('index', '-1'))
self.language = data.attrib.get('language')
self.languageCode = data.attrib.get('languageCode')
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
self.selected = cast(bool, data.attrib.get('selected', '0'))
self.streamType = cast(int, data.attrib.get('streamType'))
self.title = data.attrib.get('title')
self.type = cast(int, data.attrib.get('streamType'))
@staticmethod
def parse(server, data, initpath): # pragma: no cover seems to be dead code.
""" Factory method returns a new MediaPartStream from xml data. """
STREAMCLS = {1: VideoStream, 2: AudioStream, 3: SubtitleStream}
stype = cast(int, data.attrib.get('streamType'))
cls = STREAMCLS.get(stype, MediaPartStream)
return cls(server, data, initpath)
@utils.registerPlexObject
class VideoStream(MediaPartStream):
""" Respresents a video stream within a :class:`~plexapi.media.MediaPart`.
""" Represents a video stream within a :class:`~plexapi.media.MediaPart`.
Attributes:
TAG (str): 'Stream'
STREAMTYPE (int): 1
bitDepth (int): Bit depth (ex: 8).
bitrate (int): Bitrate (ex: 1169)
cabac (int): Unknown
chromaSubsampling (str): Chroma Subsampling (ex: 4:2:0).
colorSpace (str): Unknown
duration (int): Duration of video stream in milliseconds.
frameRate (float): Frame rate (ex: 23.976)
frameRateMode (str): Unknown
anamorphic (str): If the video is anamorphic.
bitDepth (int): The bit depth of the video stream (ex: 8).
cabac (int): The context-adaptive binary arithmetic coding.
chromaLocation (str): The chroma location of the video stream.
chromaSubsampling (str): The chroma subsampling of the video stream (ex: 4:2:0).
codecID (str): The codec ID (ex: XVID).
codedHeight (int): The coded height of the video stream in pixels.
codedWidth (int): The coded width of the video stream in pixels.
colorPrimaries (str): The color primaries of the video stream.
colorRange (str): The color range of the video stream.
colorSpace (str): The color space of the video stream (ex: bt2020).
colorTrc (str): The color trc of the video stream.
DOVIBLCompatID (int): Dolby Vision base layer compatibility ID.
DOVIBLPresent (bool): True if Dolby Vision base layer is present.
DOVIELPresent (bool): True if Dolby Vision enhancement layer is present.
DOVILevel (int): Dolby Vision level.
DOVIPresent (bool): True if Dolby Vision is present.
DOVIProfile (int): Dolby Vision profile.
DOVIRPUPresent (bool): True if Dolby Vision reference processing unit is present.
DOVIVersion (float): The Dolby Vision version.
duration (int): The duration of video stream in milliseconds.
frameRate (float): The frame rate of the video stream (ex: 23.976).
frameRateMode (str): The frame rate mode of the video stream.
hasScallingMatrix (bool): True if video stream has a scaling matrix.
height (int): Height of video stream.
level (int): Videl stream level (?).
profile (str): Video stream profile (ex: asp).
refFrames (int): Unknown
scanType (str): Video stream scan type (ex: progressive).
title (str): Title of this video stream.
width (int): Width of video stream.
height (int): The hight of the video stream in pixels (ex: 1080).
level (int): The codec encoding level of the video stream (ex: 41).
profile (str): The profile of the video stream (ex: asp).
pixelAspectRatio (str): The pixel aspect ratio of the video stream.
pixelFormat (str): The pixel format of the video stream.
refFrames (int): The number of reference frames of the video stream.
scanType (str): The scan type of the video stream (ex: progressive).
streamIdentifier(int): The stream identifier of the video stream.
width (int): The width of the video stream in pixels (ex: 1920).
"""
TAG = 'Stream'
STREAMTYPE = 1
@ -234,11 +292,26 @@ class VideoStream(MediaPartStream):
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
super(VideoStream, self)._loadData(data)
self.anamorphic = data.attrib.get('anamorphic')
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.cabac = cast(int, data.attrib.get('cabac'))
self.chromaLocation = data.attrib.get('chromaLocation')
self.chromaSubsampling = data.attrib.get('chromaSubsampling')
self.codecID = data.attrib.get('codecID')
self.codedHeight = cast(int, data.attrib.get('codedHeight'))
self.codedWidth = cast(int, data.attrib.get('codedWidth'))
self.colorPrimaries = data.attrib.get('colorPrimaries')
self.colorRange = data.attrib.get('colorRange')
self.colorSpace = data.attrib.get('colorSpace')
self.colorTrc = data.attrib.get('colorTrc')
self.DOVIBLCompatID = cast(int, data.attrib.get('DOVIBLCompatID'))
self.DOVIBLPresent = cast(bool, data.attrib.get('DOVIBLPresent'))
self.DOVIELPresent = cast(bool, data.attrib.get('DOVIELPresent'))
self.DOVILevel = cast(int, data.attrib.get('DOVILevel'))
self.DOVIPresent = cast(bool, data.attrib.get('DOVIPresent'))
self.DOVIProfile = cast(int, data.attrib.get('DOVIProfile'))
self.DOVIRPUPresent = cast(bool, data.attrib.get('DOVIRPUPresent'))
self.DOVIVersion = cast(float, data.attrib.get('DOVIVersion'))
self.duration = cast(int, data.attrib.get('duration'))
self.frameRate = cast(float, data.attrib.get('frameRate'))
self.frameRateMode = data.attrib.get('frameRateMode')
@ -246,28 +319,41 @@ class VideoStream(MediaPartStream):
self.height = cast(int, data.attrib.get('height'))
self.level = cast(int, data.attrib.get('level'))
self.profile = data.attrib.get('profile')
self.pixelAspectRatio = data.attrib.get('pixelAspectRatio')
self.pixelFormat = data.attrib.get('pixelFormat')
self.refFrames = cast(int, data.attrib.get('refFrames'))
self.scanType = data.attrib.get('scanType')
self.title = data.attrib.get('title')
self.streamIdentifier = cast(int, data.attrib.get('streamIdentifier'))
self.width = cast(int, data.attrib.get('width'))
@utils.registerPlexObject
class AudioStream(MediaPartStream):
""" Respresents a audio stream within a :class:`~plexapi.media.MediaPart`.
""" Represents a audio stream within a :class:`~plexapi.media.MediaPart`.
Attributes:
TAG (str): 'Stream'
STREAMTYPE (int): 2
audioChannelLayout (str): Audio channel layout (ex: 5.1(side)).
bitDepth (int): Bit depth (ex: 16).
bitrate (int): Audio bitrate (ex: 448).
bitrateMode (str): Bitrate mode (ex: cbr).
channels (int): number of channels in this stream (ex: 6).
dialogNorm (int): Unknown (ex: -27).
duration (int): Duration of audio stream in milliseconds.
samplingRate (int): Sampling rate (ex: xxx)
title (str): Title of this audio stream.
audioChannelLayout (str): The audio channel layout of the audio stream (ex: 5.1(side)).
bitDepth (int): The bit depth of the audio stream (ex: 16).
bitrateMode (str): The bitrate mode of the audio stream (ex: cbr).
channels (int): The number of audio channels of the audio stream (ex: 6).
duration (int): The duration of audio stream in milliseconds.
profile (str): The profile of the audio stream.
samplingRate (int): The sampling rate of the audio stream (ex: xxx)
streamIdentifier (int): The stream identifier of the audio stream.
<Track_only_attributes>: The following attributes are only available for tracks.
* albumGain (float): The gain for the album.
* albumPeak (float): The peak for the album.
* albumRange (float): The range for the album.
* endRamp (str): The end ramp for the track.
* gain (float): The gain for the track.
* loudness (float): The loudness for the track.
* lra (float): The lra for the track.
* peak (float): The peak for the track.
* startRamp (str): The start ramp for the track.
"""
TAG = 'Stream'
STREAMTYPE = 2
@ -277,26 +363,37 @@ class AudioStream(MediaPartStream):
super(AudioStream, self)._loadData(data)
self.audioChannelLayout = data.attrib.get('audioChannelLayout')
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.bitrateMode = data.attrib.get('bitrateMode')
self.channels = cast(int, data.attrib.get('channels'))
self.dialogNorm = cast(int, data.attrib.get('dialogNorm'))
self.duration = cast(int, data.attrib.get('duration'))
self.profile = data.attrib.get('profile')
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
self.title = data.attrib.get('title')
self.streamIdentifier = cast(int, data.attrib.get('streamIdentifier'))
if self._isChildOf(etag='Track'):
self.albumGain = cast(float, data.attrib.get('albumGain'))
self.albumPeak = cast(float, data.attrib.get('albumPeak'))
self.albumRange = cast(float, data.attrib.get('albumRange'))
self.endRamp = data.attrib.get('endRamp')
self.gain = cast(float, data.attrib.get('gain'))
self.loudness = cast(float, data.attrib.get('loudness'))
self.lra = cast(float, data.attrib.get('lra'))
self.peak = cast(float, data.attrib.get('peak'))
self.startRamp = data.attrib.get('startRamp')
@utils.registerPlexObject
class SubtitleStream(MediaPartStream):
""" Respresents a audio stream within a :class:`~plexapi.media.MediaPart`.
""" Represents a audio stream within a :class:`~plexapi.media.MediaPart`.
Attributes:
TAG (str): 'Stream'
STREAMTYPE (int): 3
forced (bool): True if this is a forced subtitle
format (str): Subtitle format (ex: srt).
key (str): Key of this subtitle stream (ex: /library/streams/212284).
title (str): Title of this subtitle stream.
container (str): The container of the subtitle stream.
forced (bool): True if this is a forced subtitle.
format (str): The format of the subtitle stream (ex: srt).
headerCommpression (str): The header compression of the subtitle stream.
transient (str): Unknown.
"""
TAG = 'Stream'
STREAMTYPE = 3
@ -304,10 +401,34 @@ class SubtitleStream(MediaPartStream):
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
super(SubtitleStream, self)._loadData(data)
self.container = data.attrib.get('container')
self.forced = cast(bool, data.attrib.get('forced', '0'))
self.format = data.attrib.get('format')
self.key = data.attrib.get('key')
self.title = data.attrib.get('title')
self.headerCompression = data.attrib.get('headerCompression')
self.transient = data.attrib.get('transient')
class LyricStream(MediaPartStream):
""" Represents a lyric stream within a :class:`~plexapi.media.MediaPart`.
Attributes:
TAG (str): 'Stream'
STREAMTYPE (int): 4
format (str): The format of the lyric stream (ex: lrc).
minLines (int): The minimum number of lines in the (timed) lyric stream.
provider (str): The provider of the lyric stream (ex: com.plexapp.agents.lyricfind).
timed (bool): True if the lyrics are timed to the track.
"""
TAG = 'Stream'
STREAMTYPE = 4
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
super(LyricStream, self)._loadData(data)
self.format = data.attrib.get('format')
self.minLines = cast(int, data.attrib.get('minLines'))
self.provider = data.attrib.get('provider')
self.timed = cast(bool, data.attrib.get('timed', '0'))
@utils.registerPlexObject
@ -323,12 +444,7 @@ class Session(PlexObject):
@utils.registerPlexObject
class TranscodeSession(PlexObject):
""" Represents a current transcode session.
Attributes:
TAG (str): 'TranscodeSession'
TODO: Document this.
"""
""" Represents a current transcode session. """
TAG = 'TranscodeSession'
def _loadData(self, data):
@ -357,7 +473,7 @@ class TranscodeSession(PlexObject):
class TranscodeJob(PlexObject):
""" Represents an Optimizing job.
TrancodeJobs are the process for optimizing conversions.
Active or paused optimization items. Usually one item as a time"""
Active or paused optimization items. Usually one item as a time."""
TAG = 'TranscodeJob'
def _loadData(self, data):

View file

@ -3,14 +3,9 @@ from urllib.parse import urlencode
from xml.etree import ElementTree
import requests
# Need these imports to populate utils.PLEXOBJECTS
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE, log,
logfilter)
from plexapi import media as _media # noqa: F401
from plexapi import photo as _photo # noqa: F401
from plexapi import playlist as _playlist # noqa: F401
from plexapi import utils
from plexapi import video as _video # noqa: F401
from plexapi.alert import AlertListener
from plexapi.base import PlexObject
from plexapi.client import PlexClient
@ -23,7 +18,12 @@ from plexapi.settings import Settings
from plexapi.utils import cast
from requests.status_codes import _codes as codes
# Need these imports to populate utils.PLEXOBJECTS
from plexapi import audio as _audio # noqa: F401; noqa: F401
from plexapi import media as _media # noqa: F401
from plexapi import photo as _photo # noqa: F401
from plexapi import playlist as _playlist # noqa: F401
from plexapi import video as _video # noqa: F401
class PlexServer(PlexObject):

View file

@ -27,7 +27,7 @@ warnings.simplefilter('default', category=DeprecationWarning)
# Library Types - Populated at runtime
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'trailer': 5, 'comic': 6, 'person': 7,
'artist': 8, 'album': 9, 'track': 10, 'picture': 11, 'clip': 12, 'photo': 13, 'photoalbum': 14,
'playlist': 15, 'playlistFolder': 16, 'collection': 18, 'userPlaylistItem': 1001}
'playlist': 15, 'playlistFolder': 16, 'collection': 18, 'optimizedVersion': 42, 'userPlaylistItem': 1001}
PLEXOBJECTS = {}

View file

@ -294,8 +294,6 @@ def test_audio_Track_attrs(album):
assert stream.bitrateMode is None
assert stream.channels == 2
assert stream.codec == "mp3"
assert stream.codecID is None
assert stream.dialogNorm is None
assert stream.duration is None
assert utils.is_int(stream.id)
assert stream.index == 0
@ -309,6 +307,15 @@ def test_audio_Track_attrs(album):
assert stream.streamType == 2
assert stream.title is None
assert stream.type == 2
assert stream.albumGain is None
assert stream.albumPeak is None
assert stream.albumRange is None
assert stream.endRamp is None
assert stream.gain is None
assert stream.loudness is None
assert stream.lra is None
assert stream.peak is None
assert stream.startRamp is None
def test_audio_Track_album(album):

View file

@ -53,12 +53,17 @@ def test_video_Movie_addCollection(movie):
def test_video_Movie_getStreamURL(movie, account):
key = movie.ratingKey
assert movie.getStreamURL() == "{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}".format(
assert movie.getStreamURL() == (
"{0}/video/:/transcode/universal/start.m3u8?"
"X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&"
"offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}").format(
utils.SERVER_BASEURL, key, account.authenticationToken
) # noqa
assert movie.getStreamURL(
videoResolution="800x600"
) == "{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}".format(
) == ("{0}/video/:/transcode/universal/start.m3u8?"
"X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&"
"offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}").format(
utils.SERVER_BASEURL, key, account.authenticationToken
) # noqa
@ -165,6 +170,7 @@ def test_video_Movie_attrs(movies):
assert sorted([i.tag for i in movie.genres]) == [
"Animation",
"Comedy",
"Drama",
"Fantasy",
"Musical",
"Romance",
@ -216,50 +222,82 @@ def test_video_Movie_attrs(movies):
assert audio.bitrateMode is None
assert audio.channels in utils.AUDIOCHANNELS
assert audio.codec in utils.CODECS
assert audio.codecID is None
assert audio.dialogNorm is None
assert audio.default is True
assert audio.displayTitle == "Unknown (AAC Stereo)"
assert audio.duration is None
assert audio.extendedDisplayTitle == "Unknown (AAC Stereo)"
assert audio.id >= 1
assert audio.index == 1
assert utils.is_metadata(audio._initpath)
assert audio.language is None
assert audio.languageCode is None
assert audio.profile == "lc"
assert audio.requiredBandwidths is None or audio.requiredBandwidths
assert audio.samplingRate == 44100
assert audio.selected is True
assert audio._server._baseurl == utils.SERVER_BASEURL
assert audio.streamIdentifier == 2
assert audio.streamType == 2
assert audio._server._baseurl == utils.SERVER_BASEURL
assert audio.title is None
assert audio.type == 2
with pytest.raises(AttributeError):
assert audio.albumGain is None # Check track only attributes are not available
# Media
media = movie.media[0]
assert media.aspectRatio >= 1.3
assert media.audioChannels in utils.AUDIOCHANNELS
assert media.audioCodec in utils.CODECS
assert media.audioProfile == "lc"
assert utils.is_int(media.bitrate)
assert media.container in utils.CONTAINERS
assert utils.is_int(media.duration, gte=160000)
assert utils.is_int(media.height)
assert utils.is_int(media.id)
assert utils.is_metadata(media._initpath)
assert media.has64bitOffsets is False
assert media.optimizedForStreaming in [None, False, True]
assert media.proxyType is None
assert media._server._baseurl == utils.SERVER_BASEURL
assert media.target is None
assert media.title is None
assert media.videoCodec in utils.CODECS
assert media.videoFrameRate in utils.FRAMERATES
assert media.videoProfile == "main"
assert media.videoResolution in utils.RESOLUTIONS
assert utils.is_int(media.width, gte=200)
with pytest.raises(AttributeError):
assert media.aperture is None # Check photo only attributes are not available
# Video
video = movie.media[0].parts[0].videoStreams()[0]
assert video.anamorphic is None
assert video.bitDepth in (
8,
None,
) # Different versions of Plex Server return different values
assert utils.is_int(video.bitrate)
assert video.cabac is None
assert video.chromaLocation == "left"
assert video.chromaSubsampling in ("4:2:0", None)
assert video.codec in utils.CODECS
assert video.codecID is None
assert utils.is_int(video.codedHeight, gte=1080)
assert utils.is_int(video.codedWidth, gte=1920)
assert video.colorPrimaries is None
assert video.colorRange is None
assert video.colorSpace is None
assert video.colorTrc is None
assert video.default is True
assert video.displayTitle == "1080p (H.264)"
assert video.DOVIBLCompatID is None
assert video.DOVIBLPresent is None
assert video.DOVIELPresent is None
assert video.DOVILevel is None
assert video.DOVIPresent is None
assert video.DOVIProfile is None
assert video.DOVIRPUPresent is None
assert video.DOVIVersion is None
assert video.duration is None
assert video.extendedDisplayTitle == "1080p (H.264)"
assert utils.is_float(video.frameRate, gte=20.0)
assert video.frameRateMode is None
assert video.hasScallingMatrix is None
@ -271,9 +309,14 @@ def test_video_Movie_attrs(movies):
assert video.languageCode is None
assert utils.is_int(video.level)
assert video.profile in utils.PROFILES
assert video.pixelAspectRatio is None
assert video.pixelFormat is None
assert utils.is_int(video.refFrames)
assert video.requiredBandwidths is None or video.requiredBandwidths
assert video.scanType in ("progressive", None)
assert video.selected is False
assert video.streamType == 1
assert video.streamIdentifier == 1
assert video._server._baseurl == utils.SERVER_BASEURL
assert utils.is_int(video.streamType)
assert video.title is None
@ -281,16 +324,28 @@ def test_video_Movie_attrs(movies):
assert utils.is_int(video.width, gte=400)
# Part
part = media.parts[0]
assert part.accessible
assert part.audioProfile == "lc"
assert part.container in utils.CONTAINERS
assert part.decision is None
assert part.deepAnalysisVersion is None or utils.is_int(part.deepAnalysisVersion)
assert utils.is_int(part.duration, 160000)
assert part.exists
assert len(part.file) >= 10
assert part.has64bitOffsets is False
assert part.hasThumbnail is None
assert utils.is_int(part.id)
assert part.indexes is None
assert utils.is_metadata(part._initpath)
assert len(part.key) >= 10
assert part._server._baseurl == utils.SERVER_BASEURL
assert part.optimizedForStreaming is True
assert part.packetLength is None
assert part.requiredBandwidths is None or part.requiredBandwidths
assert utils.is_int(part.size, gte=1000000)
assert part.exists
assert part.accessible
assert part.syncItemId is None
assert part.syncState is None
assert part._server._baseurl == utils.SERVER_BASEURL
assert part.videoProfile == "main"
# Stream 1
stream1 = part.streams[0]
assert stream1.bitDepth in (8, None)
@ -298,7 +353,6 @@ def test_video_Movie_attrs(movies):
assert stream1.cabac is None
assert stream1.chromaSubsampling in ("4:2:0", None)
assert stream1.codec in utils.CODECS
assert stream1.codecID is None
assert stream1.colorSpace is None
assert stream1.duration is None
assert utils.is_float(stream1.frameRate, gte=20.0)
@ -329,8 +383,6 @@ def test_video_Movie_attrs(movies):
assert stream2.bitrateMode is None
assert stream2.channels in utils.AUDIOCHANNELS
assert stream2.codec in utils.CODECS
assert stream2.codecID is None
assert stream2.dialogNorm is None
assert stream2.duration is None
assert utils.is_int(stream2.id)
assert utils.is_int(stream2.index)
@ -898,7 +950,7 @@ def test_video_exists_accessible(movie, episode):
def test_video_edits_locked(movie, episode):
edits = {'titleSort.value':'New Title Sort', 'titleSort.locked': 1}
edits = {'titleSort.value': 'New Title Sort', 'titleSort.locked': 1}
movieTitleSort = movie.titleSort
movie.edit(**edits)
movie.reload()