mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 14:14:19 +00:00
Merge branch 'master' into feature/posters
This commit is contained in:
commit
1445be25eb
10 changed files with 550 additions and 157 deletions
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi import library, media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest
|
||||
|
||||
|
@ -155,6 +155,15 @@ class Artist(Audio):
|
|||
for album in self.albums():
|
||||
yield album
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
directory = data.find('Directory')
|
||||
if directory:
|
||||
related = directory.find('Related')
|
||||
if related:
|
||||
return self.findItems(related, library.Hub)
|
||||
|
||||
def album(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ class PlexObject(object):
|
|||
if cls is not None:
|
||||
return cls(self._server, elem, initpath, parent=self)
|
||||
# cls is not specified, try looking it up in PLEXOBJECTS
|
||||
etype = elem.attrib.get('type', elem.attrib.get('streamType'))
|
||||
etype = elem.attrib.get('streamType', elem.attrib.get('tagType', elem.attrib.get('type')))
|
||||
ehash = '%s.%s' % (elem.tag, etype) if etype else elem.tag
|
||||
ecls = utils.PLEXOBJECTS.get(ehash, utils.PLEXOBJECTS.get(elem.tag))
|
||||
# log.debug('Building %s as %s', elem.tag, ecls.__name__)
|
||||
|
@ -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
|
||||
|
|
|
@ -1210,27 +1210,146 @@ class Hub(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'Hub'
|
||||
hubIdentifier (str): Unknown.
|
||||
size (int): Number of items found.
|
||||
title (str): Title of this Hub.
|
||||
type (str): Type of items in the Hub.
|
||||
items (str): List of items in the Hub.
|
||||
context (str): The context of the hub.
|
||||
hubKey (str): API URL for these specific hub items.
|
||||
hubIdentifier (str): The identifier of the hub.
|
||||
key (str): API URL for the hub.
|
||||
more (bool): True if there are more items to load (call reload() to fetch all items).
|
||||
size (int): The number of items in the hub.
|
||||
style (str): The style of the hub.
|
||||
title (str): The title of the hub.
|
||||
type (str): The type of items in the hub.
|
||||
"""
|
||||
TAG = 'Hub'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.context = data.attrib.get('context')
|
||||
self.hubKey = data.attrib.get('hubKey')
|
||||
self.hubIdentifier = data.attrib.get('hubIdentifier')
|
||||
self.items = self.findItems(data)
|
||||
self.key = data.attrib.get('key')
|
||||
self.more = utils.cast(bool, data.attrib.get('more'))
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.style = data.attrib.get('style')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.key = data.attrib.get('key')
|
||||
self.items = self.findItems(data)
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def reload(self):
|
||||
""" Reloads the hub to fetch all items in the hub. """
|
||||
if self.more and self.key:
|
||||
self.items = self.fetchItems(self.key)
|
||||
self.more = False
|
||||
self.size = len(self.items)
|
||||
|
||||
|
||||
class HubMediaTag(PlexObject):
|
||||
""" Base class of hub media tag search results.
|
||||
|
||||
Attributes:
|
||||
count (int): The number of items where this tag is found.
|
||||
filter (str): The URL filter for the tag.
|
||||
id (int): The id of the tag.
|
||||
key (str): API URL (/library/section/<librarySectionID>/all?<filter>).
|
||||
librarySectionID (int): The library section ID where the tag is found.
|
||||
librarySectionKey (str): API URL for the library section (/library/section/<librarySectionID>)
|
||||
librarySectionTitle (str): The library title where the tag is found.
|
||||
librarySectionType (int): The library type where the tag is found.
|
||||
reason (str): The reason for the search result.
|
||||
reasonID (int): The reason ID for the search result.
|
||||
reasonTitle (str): The reason title for the search result.
|
||||
type (str): The type of search result (tag).
|
||||
tag (str): The title of the tag.
|
||||
tagType (int): The type ID of the tag.
|
||||
tagValue (int): The value of the tag.
|
||||
thumb (str): The URL for the thumbnail of the tag (if available).
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.count = utils.cast(int, data.attrib.get('count'))
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.librarySectionType = utils.cast(int, data.attrib.get('librarySectionType'))
|
||||
self.reason = data.attrib.get('reason')
|
||||
self.reasonID = utils.cast(int, data.attrib.get('reasonID'))
|
||||
self.reasonTitle = data.attrib.get('reasonTitle')
|
||||
self.type = data.attrib.get('type')
|
||||
self.tag = data.attrib.get('tag')
|
||||
self.tagType = utils.cast(int, data.attrib.get('tagType'))
|
||||
self.tagValue = utils.cast(int, data.attrib.get('tagValue'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(HubMediaTag):
|
||||
""" Represents a single Tag hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 0
|
||||
"""
|
||||
TAGTYPE = 0
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Genre(HubMediaTag):
|
||||
""" Represents a single Genre hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 1
|
||||
"""
|
||||
TAGTYPE = 1
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Director(HubMediaTag):
|
||||
""" Represents a single Director hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 4
|
||||
"""
|
||||
TAGTYPE = 4
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Actor(HubMediaTag):
|
||||
""" Represents a single Actor hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 6
|
||||
"""
|
||||
TAGTYPE = 6
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class AutoTag(HubMediaTag):
|
||||
""" Represents a single AutoTag hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 207
|
||||
"""
|
||||
TAGTYPE = 207
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Place(HubMediaTag):
|
||||
""" Represents a single Place hub search media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 400
|
||||
"""
|
||||
TAGTYPE = 400
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Station(PlexObject):
|
||||
|
|
342
plexapi/media.py
342
plexapi/media.py
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
@ -522,17 +522,24 @@ class PlexServer(PlexObject):
|
|||
Parameters:
|
||||
query (str): Query to use when searching your library.
|
||||
mediatype (str): Optionally limit your search to the specified media type.
|
||||
actor, album, artist, autotag, collection, director, episode, game, genre,
|
||||
movie, photo, photoalbum, place, playlist, shared, show, tag, track
|
||||
limit (int): Optionally limit to the specified number of results per Hub.
|
||||
"""
|
||||
results = []
|
||||
params = {'query': query}
|
||||
if mediatype:
|
||||
params['section'] = utils.SEARCHTYPES[mediatype]
|
||||
params = {
|
||||
'query': query,
|
||||
'includeCollections': 1,
|
||||
'includeExternalMedia': 1}
|
||||
if limit:
|
||||
params['limit'] = limit
|
||||
key = '/hubs/search?%s' % urlencode(params)
|
||||
for hub in self.fetchItems(key, Hub):
|
||||
results += hub.items
|
||||
if mediatype:
|
||||
if hub.type == mediatype:
|
||||
return hub.items
|
||||
else:
|
||||
results += hub.items
|
||||
return results
|
||||
|
||||
def sessions(self):
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ def registerPlexObject(cls):
|
|||
define a few helper functions to dynamically convery the XML into objects. See
|
||||
buildItem() below for an example.
|
||||
"""
|
||||
etype = getattr(cls, 'STREAMTYPE', cls.TYPE)
|
||||
etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE))
|
||||
ehash = '%s.%s' % (cls.TAG, etype) if etype else cls.TAG
|
||||
if ehash in PLEXOBJECTS:
|
||||
raise Exception('Ambiguous PlexObject definition %s(tag=%s, type=%s) with %s' %
|
||||
|
|
|
@ -346,6 +346,15 @@ class Movie(Playable, Video):
|
|||
# This is just for compat.
|
||||
return self.title
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
video = data.find('Video')
|
||||
if video:
|
||||
related = video.find('Related')
|
||||
if related:
|
||||
return self.findItems(related, library.Hub)
|
||||
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Download video files to specified directory.
|
||||
|
||||
|
@ -479,15 +488,21 @@ class Show(Video):
|
|||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
for item in data.iter('Related'):
|
||||
return self.findItems(item, library.Hub)
|
||||
directory = data.find('Directory')
|
||||
if directory:
|
||||
related = directory.find('Related')
|
||||
if related:
|
||||
return self.findItems(related, library.Hub)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns shows On Deck :class:`~plexapi.video.Video` object.
|
||||
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||
If show is unwatched, return will likely be the first episode.
|
||||
"""
|
||||
data = self._server.query(self._details_key)
|
||||
return self.findItems([item for item in data.iter('OnDeck')][0])[0]
|
||||
episode = next(data.iter('OnDeck'), None)
|
||||
if episode:
|
||||
return self.findItems(episode)[0]
|
||||
return None
|
||||
|
||||
def season(self, title=None, season=None):
|
||||
""" Returns the season with the specified title or number.
|
||||
|
@ -650,6 +665,16 @@ class Season(Video):
|
|||
""" Alias to :func:`~plexapi.video.Season.episode`. """
|
||||
return self.episode(title, episode)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns season's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||
Will only return a match if the show's On Deck episode is in this season.
|
||||
"""
|
||||
data = self._server.query(self._details_key)
|
||||
episode = next(data.iter('OnDeck'), None)
|
||||
if episode:
|
||||
return self.findItems(episode)[0]
|
||||
return None
|
||||
|
||||
def show(self):
|
||||
""" Return the season's :class:`~plexapi.video.Show`. """
|
||||
return self.fetchItem(self.parentRatingKey)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -109,7 +109,38 @@ def test_server_search(plex, movie):
|
|||
title = movie.title
|
||||
# this search seem to fail on my computer but not at travis, wtf.
|
||||
assert plex.search(title)
|
||||
assert plex.search(title, mediatype="movie")
|
||||
results = plex.search(title, mediatype="movie")
|
||||
assert results[0] == movie
|
||||
# Test genre search
|
||||
genre = movie.genres[0]
|
||||
results = plex.search(genre.tag, mediatype="genre")
|
||||
hub_tag = results[0]
|
||||
assert utils.is_int(hub_tag.count)
|
||||
assert hub_tag.filter == "genre={}".format(hub_tag.id)
|
||||
assert utils.is_int(hub_tag.id)
|
||||
assert utils.is_metadata(
|
||||
hub_tag.key,
|
||||
prefix=hub_tag.librarySectionKey,
|
||||
contains="{}/all".format(hub_tag.librarySectionID),
|
||||
suffix=hub_tag.filter)
|
||||
assert utils.is_int(hub_tag.librarySectionID)
|
||||
assert utils.is_metadata(hub_tag.librarySectionKey, prefix="/library/sections")
|
||||
assert hub_tag.librarySectionTitle == "Movies"
|
||||
assert hub_tag.librarySectionType == 1
|
||||
assert hub_tag.reason == "section"
|
||||
assert hub_tag.reasonID == hub_tag.librarySectionID
|
||||
assert hub_tag.reasonTitle == hub_tag.librarySectionTitle
|
||||
assert hub_tag.type == "tag"
|
||||
assert hub_tag.tag == genre.tag
|
||||
assert hub_tag.tagType == 1
|
||||
assert hub_tag.tagValue is None
|
||||
assert hub_tag.thumb is None
|
||||
# Test director search
|
||||
director = movie.directors[0]
|
||||
assert plex.search(director.tag, mediatype="director")
|
||||
# Test actor search
|
||||
role = movie.roles[0]
|
||||
assert plex.search(role.tag, mediatype="actor")
|
||||
|
||||
|
||||
def test_server_playlist(plex, show):
|
||||
|
|
|
@ -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©ts=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©ts=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©ts=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©ts=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)
|
||||
|
@ -494,6 +546,30 @@ def test_video_Movie_art(movie):
|
|||
assert file_art.selected is True
|
||||
movie.setArt(arts[0]) # Reset to default art
|
||||
|
||||
|
||||
def test_video_Movie_hubs(movies):
|
||||
movie = movies.get('Big Buck Bunny')
|
||||
hubs = movie.hubs()
|
||||
assert len(hubs)
|
||||
hub = hubs[0]
|
||||
assert hub.context == "hub.movie.similar"
|
||||
assert utils.is_metadata(hub.hubKey)
|
||||
assert hub.hubIdentifier == "movie.similar"
|
||||
assert len(hub.items) == hub.size
|
||||
assert utils.is_metadata(hub.key)
|
||||
assert hub.more is False
|
||||
assert hub.size == 1
|
||||
assert hub.style in (None, "shelf")
|
||||
assert hub.title == "Related Movies"
|
||||
assert hub.type == "movie"
|
||||
assert len(hub) == hub.size
|
||||
# Force hub reload
|
||||
hub.more = True
|
||||
hub.reload()
|
||||
assert len(hub.items) == hub.size
|
||||
assert hub.more is False
|
||||
assert hub.size == 1
|
||||
|
||||
|
||||
def test_video_Show(show):
|
||||
assert show.title == "Game of Thrones"
|
||||
|
@ -942,7 +1018,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()
|
||||
|
|
Loading…
Reference in a new issue