mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 06:04:15 +00:00
Add TAG constant to PlexObjects; Better method to save and build PLEXOBJECTS; All objects in media.py are now registered and can be looked up; Remove __len__ on Library class (it was causing URL to load twice).
This commit is contained in:
parent
3783f3c61b
commit
9b791b95e7
14 changed files with 262 additions and 233 deletions
|
@ -28,8 +28,6 @@ class Audio(PlexPartialObject):
|
|||
updatedAt (datatime): Datetime this item was updated.
|
||||
viewCount (int): Count of times this item was accessed.
|
||||
"""
|
||||
TYPE = None
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
|
@ -55,7 +53,7 @@ class Audio(PlexPartialObject):
|
|||
return self._server.url(self.thumb)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Artist(Audio):
|
||||
""" Represents a single audio artist.
|
||||
|
||||
|
@ -73,6 +71,7 @@ class Artist(Audio):
|
|||
location (str): Filepath this artist is found on disk.
|
||||
similar (list): List of :class:`~plexapi.media.Similar` artists.
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -82,9 +81,9 @@ class Artist(Audio):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.location = utils.findLocations(data, single=True)
|
||||
self.countries = self._buildItems(data, media.Country, bytag=True)
|
||||
self.genres = self._buildItems(data, media.Genre, bytag=True)
|
||||
self.similar = self._buildItems(data, media.Similar, bytag=True)
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
|
||||
def album(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||
|
@ -138,7 +137,7 @@ class Artist(Audio):
|
|||
return filepaths
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Album(Audio):
|
||||
""" Represents a single audio album.
|
||||
|
||||
|
@ -159,6 +158,7 @@ class Album(Audio):
|
|||
studio (str): Studio that released this album.
|
||||
year (int): Year this album was released.
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'album'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -173,7 +173,7 @@ class Album(Audio):
|
|||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.genres = self._buildItems(data, media.Genre, bytag=True)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
@ -216,7 +216,7 @@ class Album(Audio):
|
|||
return filepaths
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Track(Audio, Playable):
|
||||
""" Represents a single audio track.
|
||||
|
||||
|
@ -253,6 +253,7 @@ class Track(Audio, Playable):
|
|||
transcodeSession (None): :class:`~plexapi.media.TranscodeSession` for playing
|
||||
track (active sessions only).
|
||||
"""
|
||||
TAG = 'Track'
|
||||
TYPE = 'track'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -278,8 +279,8 @@ class Track(Audio, Playable):
|
|||
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.media = self._buildItems(data, media.Media, bytag=True)
|
||||
self.moods = self._buildItems(data, media.Mood, bytag=True)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.moods = self.findItems(data, media.Mood)
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.username = utils.findUsername(data)
|
||||
|
@ -298,8 +299,8 @@ class Track(Audio, Playable):
|
|||
|
||||
def album(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Album`. """
|
||||
return self.fetchItems(self.parentKey)[0]
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def artist(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
||||
return self.fetchItems(self.grandparentKey)[0]
|
||||
return self.fetchItem(self.grandparentKey)
|
||||
|
|
179
plexapi/base.py
179
plexapi/base.py
|
@ -2,8 +2,7 @@
|
|||
import re
|
||||
from plexapi import log, utils
|
||||
from plexapi.compat import urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.exceptions import UnknownType, Unsupported
|
||||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
|
||||
OPERATORS = {
|
||||
'exact': lambda v,q: v == q,
|
||||
|
@ -19,7 +18,7 @@ OPERATORS = {
|
|||
'istartswith': lambda v,q: v.lower().startswith(q),
|
||||
'endswith': lambda v,q: v.endswith(q),
|
||||
'iendswith': lambda v,q: v.lower().endswith(q),
|
||||
'ismissing': None, # special case in _checkAttrs
|
||||
'exists': lambda v,q: v is not None if q else v is None,
|
||||
'regex': lambda v,q: re.match(q, v),
|
||||
'iregex': lambda v,q: re.match(q, v, flags=re.IGNORECASE),
|
||||
}
|
||||
|
@ -27,73 +26,58 @@ OPERATORS = {
|
|||
|
||||
class PlexObject(object):
|
||||
""" Base class for all Plex objects.
|
||||
TODO: Finish documenting this.
|
||||
"""
|
||||
key = None
|
||||
|
||||
def __init__(self, root, data, initpath=None):
|
||||
self._server = root # Root MyPlexAccount or PlexServer
|
||||
self._data = data # XML data needed to build object
|
||||
self._initpath = initpath or self.key # Request path used to fetch data
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
"""
|
||||
TAG = None # xml element tag
|
||||
TYPE = None # xml element type
|
||||
key = None # plex relative url
|
||||
|
||||
def __init__(self, server, data, initpath=None):
|
||||
self._server = server
|
||||
self._data = data
|
||||
self._initpath = initpath or self.key
|
||||
self._loadData(data)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.__firstattr('_baseurl', 'key', 'id', 'playQueueID', 'uri'),
|
||||
self.__firstattr('title', 'name', 'username', 'product', 'tag')
|
||||
utils.firstAttr(self, '_baseurl', 'key', 'id', 'playQueueID', 'uri'),
|
||||
utils.firstAttr(self, 'title', 'name', 'username', 'product', 'tag')
|
||||
] if p])
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
# dont overwrite an attr with None unless its a private variable
|
||||
if value is not None or attr.startswith('_') or attr not in self.__dict__:
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def __firstattr(self, *attrs):
|
||||
for attr in attrs:
|
||||
value = self.__dict__.get(attr)
|
||||
if value:
|
||||
value = str(value).replace(' ','-')
|
||||
value = value.replace('/library/metadata/','')
|
||||
value = value.replace('/children','')
|
||||
return value[:20]
|
||||
|
||||
def _buildItem(self, elem, cls=None, initpath=None, bytag=False):
|
||||
""" Factory function to build objects based on registered LIBRARY_TYPES. """
|
||||
def _buildItem(self, elem, cls=None, initpath=None):
|
||||
""" Factory function to build objects based on registered PLEXOBJECTS. """
|
||||
# cls is specified, build the object and return
|
||||
initpath = initpath or self._initpath
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if cls and libtype == cls.TYPE:
|
||||
if cls is not None:
|
||||
return cls(self._server, elem, initpath)
|
||||
if libtype in utils.LIBRARY_TYPES:
|
||||
cls = utils.LIBRARY_TYPES[libtype]
|
||||
return cls(self._server, elem, initpath)
|
||||
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, libtype))
|
||||
# cls is not specified, try looking it up in PLEXOBJECTS
|
||||
etype = elem.attrib.get('type', elem.attrib.get('streamType'))
|
||||
ehash = '%s.%s' % (elem.tag, etype) if etype else elem.tag
|
||||
ecls = utils.PLEXOBJECTS.get(ehash, utils.PLEXOBJECTS.get(elem.tag))
|
||||
if ecls is not None:
|
||||
return ecls(self._server, elem, initpath)
|
||||
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, etype))
|
||||
|
||||
def _buildItemOrNone(self, elem, cls=None, initpath=None, bytag=False):
|
||||
def _buildItemOrNone(self, elem, cls=None, initpath=None):
|
||||
""" Calls :func:`~plexapi.base.PlexObject._buildItem()` but returns
|
||||
None if elem is an unknown type.
|
||||
"""
|
||||
try:
|
||||
return self._buildItem(elem, cls, initpath, bytag)
|
||||
return self._buildItem(elem, cls, initpath)
|
||||
except UnknownType:
|
||||
return None
|
||||
|
||||
def _buildItems(self, data, cls=None, initpath=None, bytag=False):
|
||||
""" Build and return a list of items (optionally filtered by tag).
|
||||
|
||||
Parameters:
|
||||
data (ElementTree): XML data to search for items.
|
||||
cls (:class:`plexapi.base.PlexObject`): Optionally specify the PlexObject
|
||||
to be built. If not specified _buildItem will be called and the best
|
||||
guess item will be built.
|
||||
"""
|
||||
items = []
|
||||
for elem in data:
|
||||
items.append(self._buildItemOrNone(elem, cls, initpath, bytag))
|
||||
return [item for item in items if item]
|
||||
|
||||
def fetchItem(self, key, cls=None, bytag=False, tag=None, **kwargs):
|
||||
def fetchItem(self, ekey, cls=None, **kwargs):
|
||||
""" Load the specified key to find and build the first item with the
|
||||
specified tag and attrs. If no tag or attrs are specified then
|
||||
the first item in the result set is returned.
|
||||
|
@ -105,10 +89,8 @@ class PlexObject(object):
|
|||
cls (:class:`~plexapi.base.PlexObject`): If you know the class of the
|
||||
items to be fetched, passing this in will help the parser ensure
|
||||
it only returns those items. By default we convert the xml elements
|
||||
to the best guess PlexObjects based on the type attr or tag.
|
||||
bytag (bool): Setting this to True tells the build-items function to guess
|
||||
the PlexObject to build from the tag (instead of the type attr).
|
||||
tag (str): Only fetch items with the specified tag.
|
||||
with the best guess PlexObjects based on tag and type attrs.
|
||||
etag (str): Only fetch items with the specified tag.
|
||||
**kwargs (dict): Optionally add attribute filters on the items to fetch. For
|
||||
example, passing in viewCount=0 will only return matching items. Filtering
|
||||
is done before the Python objects are built to help keep things speedy.
|
||||
|
@ -121,42 +103,57 @@ class PlexObject(object):
|
|||
passing in viewCount__gte=0 will return all items where viewCount >= 0.
|
||||
Available operations include:
|
||||
|
||||
* __exact: Value matches specified arg.
|
||||
* __iexact: Case insensative value matches specified arg.
|
||||
* __contains: Value contains specified arg.
|
||||
* __icontains: Case insensative value contains specified arg.
|
||||
* __in: Value is in a specified list or tuple.
|
||||
* __endswith: Value ends with specified arg.
|
||||
* __exact: Value matches specified arg.
|
||||
* __exists (bool): Value is or is not present in the attrs.
|
||||
* __gt: Value is greater than specified arg.
|
||||
* __gte: Value is greater than or equal to specified arg.
|
||||
* __icontains: Case insensative value contains specified arg.
|
||||
* __iendswith: Case insensative value ends with specified arg.
|
||||
* __iexact: Case insensative value matches specified arg.
|
||||
* __in: Value is in a specified list or tuple.
|
||||
* __iregex: Case insensative value matches the specified regular expression.
|
||||
* __istartswith: Case insensative value starts with specified arg.
|
||||
* __lt: Value is less than specified arg.
|
||||
* __lte: Value is less than or equal to specified arg.
|
||||
* __startswith: Value starts with specified arg.
|
||||
* __istartswith: Case insensative value starts with specified arg.
|
||||
* __endswith: Value ends with specified arg.
|
||||
* __iendswith: Case insensative value ends with specified arg.
|
||||
* __ismissing (bool): Value is or is not present in the attrs.
|
||||
* __regex: Value matches the specified regular expression.
|
||||
* __iregex: Case insensative value matches the specified regular expression.
|
||||
* __startswith: Value starts with specified arg.
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
key = '/library/metadata/%s' % key
|
||||
for elem in self._server.query(key):
|
||||
if tag and elem.tag != tag or not self._checkAttrs(elem, **kwargs):
|
||||
continue
|
||||
return self._buildItem(elem, cls, key, bytag)
|
||||
raise NotFound('Unable to find elem: tag=%s, attrs=%s' % (tag, kwargs))
|
||||
if isinstance(ekey, int):
|
||||
ekey = '/library/metadata/%s' % ekey
|
||||
for elem in self._server.query(ekey):
|
||||
if self._checkAttrs(elem, **kwargs):
|
||||
return self._buildItem(elem, cls, ekey)
|
||||
clsname = cls.__name__ if cls else 'None'
|
||||
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
|
||||
|
||||
def fetchItems(self, key, cls=None, bytag=False, tag=None, **kwargs):
|
||||
def fetchItems(self, ekey, cls=None, **kwargs):
|
||||
""" Load the specified key to find and build all items with the specified tag
|
||||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
on how this is used.
|
||||
"""
|
||||
data = self._server.query(ekey)
|
||||
return self.findItems(data, cls, ekey, **kwargs)
|
||||
|
||||
def findItems(self, data, cls=None, initpath=None, **kwargs):
|
||||
""" Load the specified data to find and build all items with the specified tag
|
||||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
on how this is used.
|
||||
"""
|
||||
# filter on cls attrs if specified
|
||||
if cls and cls.TAG and 'tag' not in kwargs:
|
||||
kwargs['etag'] = cls.TAG
|
||||
if cls and cls.TYPE and 'type' not in kwargs:
|
||||
kwargs['type'] = cls.TYPE
|
||||
# loop through all data elements to find matches
|
||||
items = []
|
||||
for elem in self._server.query(key):
|
||||
if tag and elem.tag != tag or not self._checkAttrs(elem, **kwargs):
|
||||
continue
|
||||
items.append(self._buildItemOrNone(elem, cls, key, bytag))
|
||||
return [item for item in items if item]
|
||||
for elem in data:
|
||||
if self._checkAttrs(elem, **kwargs):
|
||||
item = self._buildItemOrNone(elem, cls, initpath)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
def reload(self, safe=False):
|
||||
""" Reload the data for this object from self.key. """
|
||||
|
@ -173,28 +170,20 @@ class PlexObject(object):
|
|||
for attr, query in kwargs.items():
|
||||
attr, op, operator = self._getAttrOperator(attr)
|
||||
values = self._getAttrValue(elem, attr)
|
||||
# special case ismissing operator
|
||||
if op == 'ismissing':
|
||||
if query not in (True, False):
|
||||
raise BadRequest('Value when using __ismissing must be in (True, False).')
|
||||
if (query is True and values) or (query is False and not values):
|
||||
return False
|
||||
# special case query in (None,0,'') to include missing attr
|
||||
if op == 'exact' and query in (None, 0, '') and not values:
|
||||
# special case query in (None, 0, '') to include missing attr
|
||||
if op == 'exact' and not values and query in (None, 0, ''):
|
||||
return True
|
||||
# return if attr were looking for is missing
|
||||
attrsFound[attr] = False
|
||||
for value in values:
|
||||
if isinstance(query, int): value = int(value)
|
||||
if isinstance(query, float): value = float(value)
|
||||
if isinstance(query, bool): value = bool(int(value))
|
||||
value = self._castAttrValue(op, query, value)
|
||||
if operator(value, query):
|
||||
attrsFound[attr] = True
|
||||
break
|
||||
#log.debug('Checking %s for %s found: %s', elem.tag, kwargs, attrsFound)
|
||||
return all(attrsFound.values())
|
||||
|
||||
def _getAttrOperator(self, attr):
|
||||
attr = attr.lstrip('_')
|
||||
for op, operator in OPERATORS.items():
|
||||
if attr.endswith('__%s' % op):
|
||||
attr = attr.rsplit('__', 1)[0]
|
||||
|
@ -212,12 +201,28 @@ class PlexObject(object):
|
|||
for child in [c for c in elem if c.tag.lower() == attr.lower()]:
|
||||
results += self._getAttrValue(child, attrstr, results)
|
||||
return [r for r in results if r is not None]
|
||||
# check were looking for the tag
|
||||
if attr.lower() == 'etag':
|
||||
return [elem.tag]
|
||||
# loop through attrs so we can perform case-insensative match
|
||||
for _attr, value in elem.attrib.items():
|
||||
if attr.lower() == _attr.lower():
|
||||
return [value]
|
||||
return []
|
||||
|
||||
def _castAttrValue(self, op, query, value):
|
||||
if op == 'exists':
|
||||
return value
|
||||
if isinstance(query, bool):
|
||||
return bool(int(value))
|
||||
if isinstance(query, int) and '.' in value:
|
||||
return float(value)
|
||||
if isinstance(query, int):
|
||||
return int(value)
|
||||
if isinstance(query, float):
|
||||
return float(value)
|
||||
return value
|
||||
|
||||
def _loadData(self, data):
|
||||
raise NotImplementedError('Abstract method not implemented.')
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ class PlexClient(PlexObject):
|
|||
_proxyThroughServer (bool): Set to True after calling
|
||||
:func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
|
||||
"""
|
||||
TAG = 'Player'
|
||||
key = '/resources'
|
||||
|
||||
def __init__(self, baseurl, token=None, session=None, server=None, data=None):
|
||||
|
|
|
@ -27,38 +27,29 @@ class Library(PlexObject):
|
|||
self.title1 = data.attrib.get('title1')
|
||||
self.title2 = data.attrib.get('title2')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.sections())
|
||||
|
||||
def sections(self):
|
||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
|
||||
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
|
||||
"""
|
||||
SECTION_TYPES = {MovieSection.TYPE:MovieSection, ShowSection.TYPE:ShowSection,
|
||||
MusicSection.TYPE: MusicSection, PhotoSection.TYPE: PhotoSection}
|
||||
items = []
|
||||
key = '/library/sections'
|
||||
sections = []
|
||||
for elem in self._server.query(key):
|
||||
stype = elem.attrib['type']
|
||||
if stype in SECTION_TYPES:
|
||||
cls = SECTION_TYPES[stype]
|
||||
section = cls(self._server, elem, key)
|
||||
self._sectionsByID[section.key] = section
|
||||
items.append(section)
|
||||
return items
|
||||
for cls in (MovieSection, ShowSection, MusicSection, PhotoSection):
|
||||
if elem.attrib.get('type') == cls.TYPE:
|
||||
section = cls(self._server, elem, key)
|
||||
self._sectionsByID[section.key] = section
|
||||
sections.append(section)
|
||||
return sections
|
||||
|
||||
def section(self, title=None):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the section to return.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: Invalid library section title.
|
||||
"""
|
||||
for section in self.sections():
|
||||
if section.title == title:
|
||||
if section.title.lower() == title.lower():
|
||||
return section
|
||||
raise NotFound('Invalid library section: %s' % title)
|
||||
|
||||
|
@ -76,7 +67,11 @@ class Library(PlexObject):
|
|||
""" Returns a list of all media from all library sections.
|
||||
This may be a very large dataset to retrieve.
|
||||
"""
|
||||
return [item for section in self.sections() for item in section.all(**kwargs)]
|
||||
items = []
|
||||
for section in self.sections():
|
||||
for item in section.all(**kwargs):
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns a list of all media items on deck. """
|
||||
|
@ -185,10 +180,6 @@ class LibrarySection(PlexObject):
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.uuid = data.attrib.get('uuid')
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [self.__class__.__name__,
|
||||
self.key, self.librarySectionTitle] if p])
|
||||
|
||||
def get(self, title):
|
||||
""" Returns the media item with the specified title.
|
||||
|
||||
|
@ -259,7 +250,7 @@ class LibrarySection(PlexObject):
|
|||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
|
||||
return self.fetchItems(key, bytag=True)
|
||||
return self.fetchItems(key, cls=FilterChoice)
|
||||
|
||||
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
|
||||
""" Search the library. If there are many results, they will be fetched from the server
|
||||
|
@ -360,12 +351,14 @@ class MovieSection(LibrarySection):
|
|||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'movie'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
|
||||
'collection', 'director', 'actor', 'country', 'studio', 'resolution')
|
||||
ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'movie'
|
||||
|
||||
|
||||
|
@ -377,11 +370,13 @@ class ShowSection(LibrarySection):
|
|||
'year', 'genre', 'contentRating', 'network', 'collection')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt', 'lastViewedAt',
|
||||
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'show'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
|
||||
'rating', 'unwatched')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'show'
|
||||
|
||||
def searchShows(self, **kwargs):
|
||||
|
@ -409,10 +404,12 @@ class MusicSection(LibrarySection):
|
|||
'country', 'collection')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'lastViewedAt', 'viewCount', 'titleSort')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'artist'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('genre', 'country', 'collection')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
||||
def albums(self):
|
||||
|
@ -437,12 +434,15 @@ class PhotoSection(LibrarySection):
|
|||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. <NONE>
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. <NONE>
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('all', 'iso',
|
||||
'make', 'lens', 'aperture', 'exposure')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'photo'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
|
||||
ALLOWED_SORT = ('addedAt')
|
||||
ALLOWED_SORT = ('addedAt',)
|
||||
TAG = 'Directory'
|
||||
TYPE = 'photo'
|
||||
|
||||
def searchAlbums(self, title, **kwargs):
|
||||
|
@ -456,7 +456,6 @@ class PhotoSection(LibrarySection):
|
|||
return self.fetchItems(key, title=title)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class FilterChoice(PlexObject):
|
||||
""" Represents a single filter choice. These objects are gathered when using filters
|
||||
while searching for library items and is the object returned in the result set of
|
||||
|
@ -472,7 +471,7 @@ class FilterChoice(PlexObject):
|
|||
title (str): Human readable name for this filter option.
|
||||
type (str): Filter type (genre, contentRating, etc).
|
||||
"""
|
||||
TYPE = 'Directory'
|
||||
TAG = 'Directory'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -483,10 +482,10 @@ class FilterChoice(PlexObject):
|
|||
self.type = data.attrib.get('type')
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Hub(PlexObject):
|
||||
FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role}
|
||||
TYPE = 'Hub'
|
||||
TAG = 'Hub'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -494,13 +493,7 @@ class Hub(PlexObject):
|
|||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.items = self._buildItems(data)
|
||||
self.items = self.findItems(data)
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def _buildItems(self, data):
|
||||
if self.type in self.FILTERTYPES:
|
||||
cls = self.FILTERTYPES[self.type]
|
||||
return [cls(self._server, elem, self._initpath) for elem in data]
|
||||
return super(Hub, self)._buildItems(data)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest
|
||||
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.
|
||||
|
@ -34,7 +36,7 @@ class Media(PlexObject):
|
|||
width (int): Width of the video in pixels (ex: 608).
|
||||
parts (list<:class:`~plexapi.media.MediaPart`>): List of MediaParts in this video.
|
||||
"""
|
||||
TYPE = 'Media'
|
||||
TAG = 'Media'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -52,9 +54,10 @@ class Media(PlexObject):
|
|||
self.videoFrameRate = data.attrib.get('videoFrameRate')
|
||||
self.videoResolution = data.attrib.get('videoResolution')
|
||||
self.width = cast(int, data.attrib.get('width'))
|
||||
self.parts = self._buildItems(data, MediaPart, bytag=True)
|
||||
self.parts = self.findItems(data, MediaPart)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class MediaPart(PlexObject):
|
||||
""" Represents a single media part (often a single file) for the media this belongs to.
|
||||
|
||||
|
@ -70,7 +73,7 @@ class MediaPart(PlexObject):
|
|||
size (int): Size of this file in bytes (ex: 733884416).
|
||||
streams (list<:class:`~plexapi.media.MediaPartStream`>): List of streams in this media part.
|
||||
"""
|
||||
TYPE = 'Part'
|
||||
TAG = 'Part'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -121,8 +124,6 @@ class MediaPartStream(PlexObject):
|
|||
2=:class:`~plexapi.media.AudioStream`, 3=:class:`~plexapi.media.SubtitleStream`).
|
||||
type (int): Alias for streamType.
|
||||
"""
|
||||
TYPE = None
|
||||
STREAMTYPE = None
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -145,6 +146,7 @@ class MediaPartStream(PlexObject):
|
|||
return cls(server, data, initpath)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class VideoStream(MediaPartStream):
|
||||
""" Respresents a video stream within a :class:`~plexapi.media.MediaPart`.
|
||||
|
||||
|
@ -166,7 +168,7 @@ class VideoStream(MediaPartStream):
|
|||
title (str): Title of this video stream.
|
||||
width (int): Width of video stream.
|
||||
"""
|
||||
TYPE = 'videostream'
|
||||
TAG = 'Stream'
|
||||
STREAMTYPE = 1
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -189,6 +191,7 @@ class VideoStream(MediaPartStream):
|
|||
self.width = cast(int, data.attrib.get('width'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class AudioStream(MediaPartStream):
|
||||
""" Respresents a audio stream within a :class:`~plexapi.media.MediaPart`.
|
||||
|
||||
|
@ -203,7 +206,7 @@ class AudioStream(MediaPartStream):
|
|||
samplingRate (int): Sampling rate (ex: xxx)
|
||||
title (str): Title of this audio stream.
|
||||
"""
|
||||
TYPE = 'audiostream'
|
||||
TAG = 'Stream'
|
||||
STREAMTYPE = 2
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -219,6 +222,7 @@ class AudioStream(MediaPartStream):
|
|||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class SubtitleStream(MediaPartStream):
|
||||
""" Respresents a audio stream within a :class:`~plexapi.media.MediaPart`.
|
||||
|
||||
|
@ -227,7 +231,7 @@ class SubtitleStream(MediaPartStream):
|
|||
key (str): Key of this subtitle stream (ex: /library/streams/212284).
|
||||
title (str): Title of this subtitle stream.
|
||||
"""
|
||||
TYPE = 'subtitlestream'
|
||||
TAG = 'Stream'
|
||||
STREAMTYPE = 3
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -237,11 +241,12 @@ class SubtitleStream(MediaPartStream):
|
|||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class TranscodeSession(PlexObject):
|
||||
""" Represents a current transcode session.
|
||||
TODO: Document this.
|
||||
"""
|
||||
TYPE = 'TranscodeSession'
|
||||
TAG = 'TranscodeSession'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -289,8 +294,6 @@ class MediaTag(PlexObject):
|
|||
* tagType (int): Tag type ID.
|
||||
* thumb (str): URL to thumbnail image.
|
||||
"""
|
||||
TYPE = None
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
|
@ -313,45 +316,54 @@ class MediaTag(PlexObject):
|
|||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collection(MediaTag):
|
||||
TYPE = 'Collection'
|
||||
TAG = 'Collection'
|
||||
FILTER = 'collection'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Country(MediaTag):
|
||||
TYPE = 'Country'
|
||||
TAG = 'Country'
|
||||
FILTER = 'country'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Director(MediaTag):
|
||||
TYPE = 'Director'
|
||||
TAG = 'Director'
|
||||
FILTER = 'director'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Genre(MediaTag):
|
||||
TYPE = 'Genre'
|
||||
TAG = 'Genre'
|
||||
FILTER = 'genre'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Mood(MediaTag):
|
||||
TYPE = 'Mood'
|
||||
TAG = 'Mood'
|
||||
FILTER = 'mood'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Producer(MediaTag):
|
||||
TYPE = 'Producer'
|
||||
TAG = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Role(MediaTag):
|
||||
TYPE = 'Role'
|
||||
TAG = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Similar(MediaTag):
|
||||
TYPE = 'Similar'
|
||||
TAG = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Writer(MediaTag):
|
||||
TYPE = 'Writer'
|
||||
TAG = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Field(PlexObject):
|
||||
TYPE = 'Field'
|
||||
TAG = 'Field'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
|
|
@ -152,6 +152,7 @@ class MyPlexAccount(PlexObject):
|
|||
return [MyPlexUser(self, elem) for elem in data]
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class MyPlexUser(PlexObject):
|
||||
""" This object represents non-signed in users such as friends and linked
|
||||
accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount`
|
||||
|
@ -177,6 +178,7 @@ class MyPlexUser(PlexObject):
|
|||
title (str): Seems to be an aliad for username
|
||||
username (str): User's username
|
||||
"""
|
||||
TAG = 'User'
|
||||
key = 'https://plex.tv/api/users/'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -200,6 +202,7 @@ class MyPlexUser(PlexObject):
|
|||
self.username = data.attrib.get('username')
|
||||
|
||||
|
||||
#@utils.registerPlexObject
|
||||
class MyPlexResource(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that can provide
|
||||
content such as Plex Media Servers, iPhone or Android clients, etc. The raw xml
|
||||
|
@ -225,6 +228,7 @@ class MyPlexResource(PlexObject):
|
|||
player, pubsub-player, etc.)
|
||||
synced (bool): Unknown (possibly True if the resource has synced content?)
|
||||
"""
|
||||
TAG = 'Device'
|
||||
key = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -246,7 +250,7 @@ class MyPlexResource(PlexObject):
|
|||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.synced = utils.cast(bool, data.attrib.get('synced'))
|
||||
self.presence = utils.cast(bool, data.attrib.get('presence'))
|
||||
self.connections = self._buildItems(data, ResourceConnection, bytag=True)
|
||||
self.connections = self.findItems(data, ResourceConnection)
|
||||
|
||||
def connect(self, ssl=None, safe=False):
|
||||
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
|
||||
|
@ -300,6 +304,7 @@ class MyPlexResource(PlexObject):
|
|||
results[i] = (url, self.accessToken, None)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class ResourceConnection(PlexObject):
|
||||
""" Represents a Resource Connection object found within the
|
||||
:class:`~myplex.MyPlexResource` objects.
|
||||
|
@ -312,7 +317,7 @@ class ResourceConnection(PlexObject):
|
|||
protocol (str): HTTP or HTTPS
|
||||
uri (str): External address
|
||||
"""
|
||||
TYPE = 'Connection'
|
||||
TAG = 'Connection'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -324,6 +329,7 @@ class ResourceConnection(PlexObject):
|
|||
self.httpuri = 'http://%s:%s' % (self.address, self.port)
|
||||
|
||||
|
||||
#@utils.registerPlexObject
|
||||
class MyPlexDevice(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that provide
|
||||
playback ability from your Plex Server, iPhone or Android clients, Plex Web,
|
||||
|
@ -350,6 +356,7 @@ class MyPlexDevice(PlexObject):
|
|||
vendor (str): Device vendor (ubuntu, etc).
|
||||
version (str): Unknown (1, 2, 1.3.3.3148-b38628e, 1.3.15, etc.)
|
||||
"""
|
||||
TAG = 'Device'
|
||||
key = 'https://plex.tv/devices.xml'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
|
|
@ -9,6 +9,10 @@ class PlexNotifier(threading.Thread):
|
|||
notifications. These often include messages from Plex about media scans
|
||||
as well as updates to currently running Transcode Sessions.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this notifier is connected to.
|
||||
callback (func): Callback function to call on recieved messages.
|
||||
|
||||
NOTE: You need websocket-client installed in order to use this feature.
|
||||
>> pip install websocket-client
|
||||
"""
|
||||
|
@ -21,9 +25,12 @@ class PlexNotifier(threading.Thread):
|
|||
super(PlexNotifier, self).__init__()
|
||||
|
||||
def run(self):
|
||||
""" Starts the PlexNotifier thread. This function should not be called
|
||||
directly, instead use :func:`~plexapi.server.PlexServer.startNotifier`.
|
||||
"""
|
||||
# try importing websocket-client package
|
||||
try:
|
||||
import websocket # only require when needed
|
||||
import websocket
|
||||
except:
|
||||
raise Unsupported('Websocket-client package is required to use this feature.')
|
||||
# create the websocket connection
|
||||
|
@ -35,6 +42,7 @@ class PlexNotifier(threading.Thread):
|
|||
self._ws.run_forever()
|
||||
|
||||
def stop(self):
|
||||
""" Stop the PlexNotifier thread. """
|
||||
log.info('Stopping PlexNotifier.')
|
||||
self._ws.close()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from plexapi.base import PlexPartialObject
|
|||
from plexapi.exceptions import NotFound
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Photoalbum(PlexPartialObject):
|
||||
""" Represents a photoalbum (collection of photos).
|
||||
|
||||
|
@ -29,7 +29,8 @@ class Photoalbum(PlexPartialObject):
|
|||
type (str): Unknown
|
||||
updatedAt (datatime): Datetime this item was updated.
|
||||
"""
|
||||
TYPE = 'photoalbum'
|
||||
TAG = 'Directory'
|
||||
TYPE = 'photo'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
|
@ -61,7 +62,7 @@ class Photoalbum(PlexPartialObject):
|
|||
raise NotFound('Unable to find photo: %s' % title)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Photo(PlexPartialObject):
|
||||
""" Represents a single photo.
|
||||
|
||||
|
@ -87,6 +88,7 @@ class Photo(PlexPartialObject):
|
|||
updatedAt (datatime): Datetime this item was updated.
|
||||
year (int): Year this photo was taken.
|
||||
"""
|
||||
TAG = 'Photo'
|
||||
TYPE = 'photo'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -106,7 +108,7 @@ class Photo(PlexPartialObject):
|
|||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.media = self._buildItems(data, media.Media)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
|
||||
def photoalbum(self):
|
||||
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
|
||||
|
|
|
@ -6,8 +6,9 @@ from plexapi.playqueue import PlayQueue
|
|||
from plexapi.utils import cast, toDatetime
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Playlist(PlexPartialObject, Playable):
|
||||
TAG = 'Playlist'
|
||||
TYPE = 'playlist'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
|
|
@ -30,7 +30,7 @@ class PlayQueue(PlexObject):
|
|||
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
|
||||
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
|
||||
self.playQueueVersion = data.attrib.get('playQueueVersion')
|
||||
self.items = self._buildItems(data)
|
||||
self.items = self.findItems(data)
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1):
|
||||
|
|
|
@ -7,13 +7,15 @@ from plexapi.base import PlexObject
|
|||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.library import Library
|
||||
from plexapi.library import Library, Hub
|
||||
from plexapi.notify import PlexNotifier
|
||||
from plexapi.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import cast
|
||||
# We import media to populate utils.LIBRARY_TYPES
|
||||
from plexapi import audio, video, photo, playlist as _pl
|
||||
|
||||
# Need these imports to populate utils.PLEXOBJECTS
|
||||
from plexapi import (audio as _audio, video as _video,
|
||||
photo as _photo, media as _media, playlist as _playlist)
|
||||
|
||||
|
||||
class PlexServer(PlexObject):
|
||||
|
@ -243,13 +245,6 @@ class PlexServer(PlexObject):
|
|||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def url(self, key):
|
||||
""" Build a URL string with proper token argument. """
|
||||
if self._token:
|
||||
delim = '&' if '?' in key else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token)
|
||||
return '%s%s' % (self._baseurl, key)
|
||||
|
||||
def search(self, query, mediatype=None, limit=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
`Hub Search <https://www.plex.tv/blog/seek-plex-shall-find-leveling-web-app/>`_
|
||||
|
@ -274,7 +269,7 @@ class PlexServer(PlexObject):
|
|||
if limit:
|
||||
params['limit'] = limit
|
||||
key = '/hubs/search?%s' % urlencode(params)
|
||||
for hub in self.fetchItems(key, bytag=True):
|
||||
for hub in self.fetchItems(key, Hub):
|
||||
results += hub.items
|
||||
return results
|
||||
|
||||
|
@ -311,12 +306,18 @@ class PlexServer(PlexObject):
|
|||
opacity (int): Opacity of the resulting image (possibly deprecated).
|
||||
saturation (int): Saturating of the resulting image.
|
||||
"""
|
||||
# TODO: Does this function really belong here?
|
||||
if media:
|
||||
transcode_url = '/photo/:/transcode?height=%s&width=%s&opacity=%s&saturation=%s&url=%s' % (
|
||||
height, width, opacity, saturation, media)
|
||||
return self.url(transcode_url)
|
||||
|
||||
def url(self, key):
|
||||
""" Build a URL string with proper token argument. """
|
||||
if self._token:
|
||||
delim = '&' if '?' in key else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token)
|
||||
return '%s%s' % (self._baseurl, key)
|
||||
|
||||
|
||||
class Account(PlexObject):
|
||||
""" Contains the locally cached MyPlex account information. The properties provided don't
|
||||
|
|
|
@ -9,7 +9,7 @@ from plexapi.exceptions import NotFound
|
|||
# Library Types - Populated at runtime
|
||||
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4,
|
||||
'artist': 8, 'album': 9, 'track': 10, 'photo': 14}
|
||||
LIBRARY_TYPES = {}
|
||||
PLEXOBJECTS = {}
|
||||
|
||||
|
||||
class SecretsFilter(logging.Filter):
|
||||
|
@ -30,12 +30,17 @@ class SecretsFilter(logging.Filter):
|
|||
return True
|
||||
|
||||
|
||||
def register_libtype(cls):
|
||||
def registerPlexObject(cls):
|
||||
""" Registry of library types we may come across when parsing XML. This allows us to
|
||||
define a few helper functions to dynamically convery the XML into objects. See
|
||||
buildItem() below for an example.
|
||||
"""
|
||||
LIBRARY_TYPES[cls.TYPE] = cls
|
||||
etype = getattr(cls, 'STREAMTYPE', 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' %
|
||||
(cls.__name__, cls.TAG, etype, PLEXOBJECTS[ehash].__name__))
|
||||
PLEXOBJECTS[ehash] = cls
|
||||
return cls
|
||||
|
||||
|
||||
|
@ -91,22 +96,6 @@ def findPlayer(server, data):
|
|||
return None
|
||||
|
||||
|
||||
def findStreams(media, streamtype):
|
||||
""" Returns a list of streams (str) found in media that match the specified streamtype.
|
||||
|
||||
Parameters:
|
||||
media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode).
|
||||
streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream).
|
||||
"""
|
||||
streams = []
|
||||
for mediaitem in media:
|
||||
for part in mediaitem.parts:
|
||||
for stream in part.streams:
|
||||
if stream.TYPE == streamtype:
|
||||
streams.append(stream)
|
||||
return streams
|
||||
|
||||
|
||||
def findTranscodeSession(server, data):
|
||||
""" Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified
|
||||
XML data.
|
||||
|
@ -135,6 +124,18 @@ def findUsername(data):
|
|||
return None
|
||||
|
||||
|
||||
def firstAttr(elem, *attrs):
|
||||
""" Return the first attribute in attrs that is not None. """
|
||||
for attr in attrs:
|
||||
value = elem.__dict__.get(attr)
|
||||
if value is not None:
|
||||
value = str(value).replace(' ','-')
|
||||
if attr == 'key':
|
||||
value = value.replace('/library/metadata/','')
|
||||
value = value.replace('/children','')
|
||||
return value[:20]
|
||||
|
||||
|
||||
def getattributeOrNone(obj, self, attr):
|
||||
try:
|
||||
return super(obj, self).__getattribute__(attr)
|
||||
|
|
|
@ -24,8 +24,6 @@ class Video(PlexPartialObject):
|
|||
updatedAt (datatime): Datetime this item was updated.
|
||||
viewCount (int): Count of times this item was accessed.
|
||||
"""
|
||||
TYPE = None
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.listType = 'video'
|
||||
|
@ -61,7 +59,7 @@ class Video(PlexPartialObject):
|
|||
self.reload()
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Movie(Video, Playable):
|
||||
""" Represents a single Movie.
|
||||
|
||||
|
@ -83,16 +81,10 @@ class Movie(Video, Playable):
|
|||
userRating (float): User rating (2.0; 8.0).
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
year (int): Year movie was released.
|
||||
collections
|
||||
countries
|
||||
directors
|
||||
fields
|
||||
genres
|
||||
media
|
||||
producers
|
||||
roles
|
||||
writers
|
||||
|
||||
# TODO: Finish documenting plexapi.video.Movie
|
||||
"""
|
||||
TAG = 'Video'
|
||||
TYPE = 'movie'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -116,15 +108,15 @@ class Movie(Video, Playable):
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.collections = self._buildItems(data, media.Collection)
|
||||
self.countries = self._buildItems(data, media.Country, bytag=True)
|
||||
self.directors = self._buildItems(data, media.Director, bytag=True)
|
||||
self.fields = self._buildItems(data, media.Field, bytag=True)
|
||||
self.genres = self._buildItems(data, media.Genre, bytag=True)
|
||||
self.media = self._buildItems(data, media.Media, bytag=True)
|
||||
self.producers = self._buildItems(data, media.Producer, bytag=True)
|
||||
self.roles = self._buildItems(data, media.Role, bytag=True)
|
||||
self.writers = self._buildItems(data, media.Writer, bytag=True)
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -163,8 +155,9 @@ class Movie(Video, Playable):
|
|||
return downloaded
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Show(Video):
|
||||
TAG = 'Directory'
|
||||
TYPE = 'show'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -187,8 +180,8 @@ class Show(Video):
|
|||
self.theme = data.attrib.get('theme')
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.genres = self._buildItems(data, media.Genre, bytag=True)
|
||||
self.roles = self._buildItems(data, media.Role, bytag=True)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -201,7 +194,7 @@ class Show(Video):
|
|||
def seasons(self, **kwargs):
|
||||
"""Returns a list of Season."""
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self.fetchItems(key, type=Season.TYPE, **kwargs)
|
||||
return self.fetchItems(key, **kwargs)
|
||||
|
||||
def season(self, title=None):
|
||||
""" Returns the season with the specified title or number.
|
||||
|
@ -212,7 +205,7 @@ class Show(Video):
|
|||
if isinstance(title, int):
|
||||
title = 'Season %s' % title
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self.fetchItem(key, tag='Directory', title__iexact=title)
|
||||
return self.fetchItem(key, etag='Directory', title__iexact=title)
|
||||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returs a list of Episode """
|
||||
|
@ -278,8 +271,9 @@ class Show(Video):
|
|||
return downloaded
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Season(Video):
|
||||
TAG = 'Directory'
|
||||
TYPE = 'season'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -316,7 +310,7 @@ class Season(Video):
|
|||
def episodes(self, **kwargs):
|
||||
""" Returs a list of Episode. """
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self.fetchItems(key, type=Episode.TYPE, **kwargs)
|
||||
return self.fetchItems(key, **kwargs)
|
||||
|
||||
def episode(self, title=None, num=None):
|
||||
""" Returns the episode with the given title or number.
|
||||
|
@ -369,8 +363,9 @@ class Season(Video):
|
|||
return downloaded
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
@utils.registerPlexObject
|
||||
class Episode(Video, Playable):
|
||||
TAG = 'Video'
|
||||
TYPE = 'episode'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -402,9 +397,9 @@ class Episode(Video, Playable):
|
|||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.directors = self._buildItems(data, media.Director, bytag=True)
|
||||
self.media = self._buildItems(data, media.Media, bytag=True)
|
||||
self.writers = self._buildItems(data, media.Writer, bytag=True)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.username = utils.findUsername(data)
|
||||
|
|
|
@ -279,11 +279,13 @@ def test_audio_Track_attrs(a_music_album):
|
|||
|
||||
|
||||
def test_audio_Track_album(a_music_album):
|
||||
assert a_music_album.tracks()[0].album() == a_music_album
|
||||
tracks = a_music_album.tracks()
|
||||
assert tracks[0].album() == a_music_album
|
||||
|
||||
|
||||
def test_audio_Track_artist(a_music_album, a_artist):
|
||||
assert a_music_album.tracks()[0].artist() == a_artist
|
||||
tracks = a_music_album.tracks()
|
||||
assert tracks[0].artist() == a_artist
|
||||
|
||||
|
||||
def test_audio_Audio_section(a_artist, a_music_album, a_track):
|
||||
|
|
Loading…
Reference in a new issue