Much more stability from yesterday; Easier to use fetchItem funtions; Common __repr__ for all plexobjects; Fix all uses if listItems

This commit is contained in:
Michael Shepanski 2017-02-07 01:20:49 -05:00
parent 4624512356
commit 8212ca9c46
13 changed files with 309 additions and 495 deletions

View file

@ -13,7 +13,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
# Core Settings
PROJECT = 'PlexAPI' # name provided to plex server
VERSION = '2.0.2' # version of this api
VERSION = '2.9.0' # version of this api
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int) # request timeout
X_PLEX_CONTAINER_SIZE = 50 # max results to return in a single search page

View file

@ -57,7 +57,7 @@ class Audio(PlexPartialObject):
def refresh(self):
""" Tells Plex to refresh the metadata for this and all subitems. """
self._root.query('%s/refresh' % self.key, method=self._root.session.put)
self._root._query('%s/refresh' % self.key, method=self._root.session.put)
def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
@ -109,7 +109,7 @@ class Artist(Audio):
def albums(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
key = '%s/children' % self.key
return self._fetchItems(key, Album.TYPE)
return self.fetchItems(key, tag=Album.TYPE)
def track(self, title):
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
@ -125,7 +125,7 @@ class Artist(Audio):
def tracks(self):
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
key = '%s/allLeaves' % self.key
return self._fetchItems(key)
return self.fetchItems(key)
def get(self, title):
""" Alias of :func:`~plexapi.audio.Artist.track`. """
@ -202,7 +202,7 @@ class Album(Audio):
def tracks(self):
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
key = '%s/children' % self.key
return self._fetchItems(key)
return self.fetchItems(key)
def get(self, title):
""" Alias of :func:`~plexapi.audio.Album.track`. """
@ -210,7 +210,7 @@ class Album(Audio):
def artist(self):
""" Return :func:`~plexapi.audio.Artist` of this album. """
return self._fetchItems(self.parentKey)[0]
return self.fetchItem(self.parentKey)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
""" Downloads all tracks for this artist to the specified location.
@ -313,8 +313,8 @@ class Track(Audio, Playable):
def album(self):
""" Return this track's :class:`~plexapi.audio.Album`. """
return self._fetchItems(self.parentKey)[0]
return self.fetchItems(self.parentKey)[0]
def artist(self):
""" Return this track's :class:`~plexapi.audio.Artist`. """
return self._fetchItems(self.grandparentKey)[0]
return self.fetchItems(self.grandparentKey)[0]

View file

@ -5,6 +5,151 @@ from plexapi.compat import urlencode
from plexapi.exceptions import NotFound, UnknownType, Unsupported
class PlexObject(object):
""" Base class for all Plex objects.
TODO: Finish documenting this.
"""
key = None
def __init__(self, root, data, initpath=None):
self._root = 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
self._loadData(data)
def __repr__(self):
return '<%s>' % ':'.join([p for p in [
self.__class__.__name__,
self.__firstattr('_baseurl', 'key', 'id', 'uri'),
self.__firstattr('title', 'name', 'username', 'librarySectionTitle', 'product')
] if p])
def __setattr__(self, attr, value):
if value is not None or attr.startswith('_'):
self.__dict__[attr] = value
def __firstattr(self, *attrs):
for attr in attrs:
value = str(self.__dict__.get(attr,'')).replace(' ','-')
value = value.replace('/library/metadata/','').replace('/children','')
if value: return value[:20]
def _buildItem(self, elem, initpath, cls=None, bytag=False):
""" Factory function to build objects based on registered LIBRARY_TYPES. """
libtype = elem.tag if bytag else elem.attrib.get('type')
if libtype == 'photo' and elem.tag == 'Directory':
libtype = 'photoalbum'
if libtype in utils.LIBRARY_TYPES:
cls = cls or utils.LIBRARY_TYPES[libtype]
return cls(self._root, elem, initpath)
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, libtype))
def _buildItemOrNone(self, elem, initpath, cls=None, bytag=False):
""" Calls :func:`~plexapi.base.PlexObject._buildItem()` but returns
None if elem is an unknown type.
"""
try:
return self._buildItem(elem, initpath, cls, bytag)
except UnknownType:
return None
def _buildItems(self, data, cls=None):
""" 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 = []
tag = cls.TYPE if cls else None
for elem in data:
if elem.tag == tag:
items.append(self._buildItemOrNone(elem, self._initpath, cls))
return [item for item in items if item]
def fetchItem(self, key, cls=None, bytag=False, tag=None, **attrs):
""" 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.
"""
for elem in self._root._query(key):
if tag and elem.tag != tag:
continue
if not all(elem.attrib.get(a,'').lower() == str(v).lower() for a,v in attrs.items()):
continue
return self._buildItem(elem, key, cls, bytag)
raise NotFound('Unable to find elem: tag=%s, attrs=%s' % (tag, attrs))
def fetchItems(self, key, cls=None, bytag=False, tag=None, **attrs):
""" Load the specified key to find and build all items with the
specified tag and attrs.
"""
items = []
for elem in self._root._query(key):
if tag and elem.tag != tag:
continue
if not all(elem.attrib.get(a,'').lower() == str(v).lower() for a,v in attrs.items()):
continue
items.append(self._buildItemOrNone(elem, key, cls, bytag))
return [item for item in items if item]
def _loadData(self, data):
raise NotImplemented('Abstract method not implemented.')
def reload(self, safe=False):
""" Reload the data for this object from self.key. """
if not self.key:
if safe: return None
raise Unsupported('Cannot reload an object not built from a URL.')
self._initpath = self.key
data = self._root._query(self.key)
self._loadData(data[0])
class PlexPartialObject(PlexObject):
""" Not all objects in the Plex listings return the complete list of elements
for the object. This object will allow you to assume each object is complete,
and if the specified value you request is None it will fetch the full object
automatically and update itself.
Attributes:
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
"""
def __eq__(self, other):
return other is not None and self.key == other.key
def __getattribute__(self, attr):
# Check a few cases where we dont want to reload
value = super(PlexPartialObject, self).__getattribute__(attr)
if attr == 'key' or attr.startswith('_'): return value
if value not in (None, []): return value
if self.isFullObject(): return value
# Log warning that were reloading the object
clsname = self.__class__.__name__
title = self.__dict__.get('title', self.__dict__.get('name'))
objname = "%s '%s'" % (clsname, title) if title else clsname
log.warn("Reloading %s for attr '%s'" % (objname, attr))
# Reload and return the value
self.reload()
return super(PlexPartialObject, self).__getattribute__(attr)
def isFullObject(self):
""" Retruns True if this is already a full object. A full object means all attributes
were populated from the api path representing only this item. For example, the
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie contain.
"""
return not self.key or self.key == self._initpath
def isPartialObject(self):
""" Returns True if this is not a full object. """
return not self.isFullObject()
class Playable(object):
""" This is a general place to store functions specific to media that is Playable.
Things were getting mixed up a bit when dealing with Shows, Season, Artists,
@ -107,142 +252,3 @@ class Playable(object):
if filepath:
filepaths.append(filepath)
return filepaths
class PlexObject(object):
""" Base class for all Plex objects.
TODO: Finish documenting this.
"""
key = None
def __init__(self, root, data, initpath=None):
self._root = 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
self._loadData(data)
def __setattr__(self, attr, value):
if value is not None or attr.startswith('_'):
self.__dict__[attr] = value
def _buildItem(self, elem, initpath, bytag=False):
""" Factory function to build objects based on registered LIBRARY_TYPES. """
libtype = elem.tag if bytag else elem.attrib.get('type')
if libtype == 'photo' and elem.tag == 'Directory':
libtype = 'photoalbum'
if libtype in utils.LIBRARY_TYPES:
cls = utils.LIBRARY_TYPES[libtype]
return cls(self._root, elem, initpath)
raise UnknownType('Unknown library type: %s' % libtype)
def _buildItems(self, data, cls=None, tag=None, attrs=None, safe=False):
""" Build and return a list of items (optionally filtered by tag).
Parameters:
data (ElementTree): XML data to search for items.
cls (PlexObject): Optionally specify the PlexObject to be built. If not specified
_buildItem will be called and the best guess item will be built.
tag (str): Only build items with the specified tag. If not specified and
cls is specified, tag will be set to cls.TYPE.
attrs (dict): Dict containing attributes to filter the elements by. If not
specified, all elements will be considered.
safe (bool): If True, dont raise an exception when unable to build an object.
"""
items = []
tag = cls.TYPE if not tag and cls else tag
attrs = attrs or {}
for elem in data:
try:
if not tag or elem.tag == tag:
for attr, value in attrs.items():
if elem.attrib.get(attr) != str(value):
continue
if cls is not None:
items.append(cls(self._root, elem, self._initpath))
else:
items.append(self._buildItem(elem, self._initpath))
except Exception as err:
if safe:
log.warn('Failed to build %s (type=%s); %s' % (elem.tag, elem.attrib.get('type', 'NA'), err))
continue
raise
return items
def _fetchItem(self, key, title=None, name=None):
for elem in self._root._query(key):
if title and elem.attrib.get('title').lower() == title.lower():
return self._buildItem(elem, key)
if name and elem.attrib.get('name').lower() == name.lower():
return self._buildItem(elem, key)
raise NotFound('Unable to find item: %s' % (title or name))
def _fetchItems(self, key, libtype=None, bytag=False):
""" Fetch and build items from the specified key. """
items = []
for elem in self._root._query(key):
try:
if not libtype or elem.attrib.get('type') == libtype:
items.append(self._buildItem(elem, key, bytag))
except UnknownType:
pass
return items
def _loadData(self, data):
raise NotImplemented('Abstract method not implemented.')
def reload(self, safe=False):
""" Reload the data for this object from self.key. """
if not self.key:
if safe: return None
raise Unsupported('Cannot reload an object not built from a URL.')
self._initpath = self.key
data = self._root._query(self.key)
self._loadData(data[0])
class PlexPartialObject(PlexObject):
""" Not all objects in the Plex listings return the complete list of elements
for the object. This object will allow you to assume each object is complete,
and if the specified value you request is None it will fetch the full object
automatically and update itself.
Attributes:
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
"""
def __eq__(self, other):
return other is not None and self.key == other.key
def __repr__(self):
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s>' % (clsname, key, title)
def __getattribute__(self, attr):
# Check a few cases where we dont want to reload
value = super(PlexPartialObject, self).__getattribute__(attr)
if attr == 'key' or attr.startswith('_'): return value
if value not in (None, []): return value
if self.isFullObject(): return value
# Log warning that were reloading the object
clsname = self.__class__.__name__
title = self.__dict__.get('title', self.__dict__.get('name'))
objname = "%s '%s'" % (clsname, title) if title else clsname
log.warn("Reloading %s for attr '%s'" % (objname, attr))
# Reload and return the value
self.reload()
return super(PlexPartialObject, self).__getattribute__(attr)
def isFullObject(self):
""" Retruns True if this is already a full object. A full object means all attributes
were populated from the api path representing only this item. For example, the
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie contain.
"""
return not self.key or self.key == self._initpath
def isPartialObject(self):
""" Returns True if this is not a full object. """
return not self.isFullObject()

View file

@ -89,9 +89,6 @@ class PlexClient(PlexObject):
self.vendor = data.attrib.get('vendor')
self.version = data.attrib.get('version')
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
def _query(self, path, method=None, headers=None, **kwargs):
""" Main method used to handle HTTPS requests to the Plex client. This method helps
by encoding the response to utf-8 and parsing the returned XML into and
@ -134,12 +131,12 @@ class PlexClient(PlexObject):
Raises:
:class:`~plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
"""
if value is True and not self.server:
if value is True and not self._server:
raise Unsupported('Cannot use client proxy with unknown server.')
self._proxyThroughServer = value
def sendCommand(self, command, proxy=None, **params):
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily
""" Convenience wrapper around :func:`~plexapi.client.PlexClient._query()` to more easily
send simple commands to the client. Returns an ElementTree object containing
the response.
@ -149,22 +146,21 @@ class PlexClient(PlexObject):
**params (dict): Additional GET parameters to include with the command.
Raises:
:class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
:class:`~plexapi.exceptions.Unsupported`: When we detect the client
doesn't support this capability.
"""
command = command.strip('/')
controller = command.split('/')[0]
if controller not in self.protocolCapabilities:
raise Unsupported('Client %s does not support the %s controller.' %
(self.title, controller))
path = '/player/%s%s' % (command, utils.joinArgs(params))
raise Unsupported('Client %s doesnt support %s controller.' % (self.title, controller))
key = '/player/%s%s' % (command, utils.joinArgs(params))
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
self._commandId += 1
params['commandID'] = self._commandId
proxy = self._proxyThroughServer if proxy is None else proxy
if proxy:
return self.server.query(path, headers=headers)
path = '/player/%s%s' % (command, utils.joinArgs(params))
return self._query(path, headers=headers)
return self._root._query(key, headers=headers)
return self._query(key, headers=headers)
#---------------------
# Navigation Commands
@ -235,11 +231,11 @@ class PlexClient(PlexObject):
Raises:
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
"""
if not self.server:
if not self._server:
raise Unsupported('A server must be specified before using this command.')
server_url = media.server._baseurl.split(':')
server_url = media._server._baseurl.split(':')
self.sendCommand('mirror/details', **dict({
'machineIdentifier': self.server.machineIdentifier,
'machineIdentifier': self._server.machineIdentifier,
'address': server_url[1].strip('/'),
'port': server_url[-1],
'key': media.key,
@ -402,12 +398,12 @@ class PlexClient(PlexObject):
Raises:
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
"""
if not self.server:
if not self._server:
raise Unsupported('A server must be specified before using this command.')
server_url = media.server._baseurl.split(':')
playqueue = self.server.createPlayQueue(media)
server_url = media._server._baseurl.split(':')
playqueue = self._server.createPlayQueue(media)
self.sendCommand('playback/playMedia', **dict({
'machineIdentifier': self.server.machineIdentifier,
'machineIdentifier': self._server.machineIdentifier,
'address': server_url[1].strip('/'),
'port': server_url[-1],
'offset': offset,

View file

@ -27,8 +27,8 @@ class Library(PlexObject):
self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2')
def __repr__(self):
return '<Library:%s>' % self.title1.encode('utf8')
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
@ -57,9 +57,9 @@ class Library(PlexObject):
Raises:
:class:`~plexapi.exceptions.NotFound`: Invalid library section title.
"""
for item in self.sections():
if item.title == title:
return item
for section in self.sections():
if section.title == title:
return section
raise NotFound('Invalid library section: %s' % title)
def sectionByID(self, sectionID):
@ -80,29 +80,11 @@ class Library(PlexObject):
def onDeck(self):
""" Returns a list of all media items on deck. """
return self._fetchItems('/library/onDeck')
return self.fetchItems('/library/onDeck')
def recentlyAdded(self):
""" Returns a list of all media items recently added. """
return self._fetchItems('/library/recentlyAdded')
def get(self, title): # this should use hub search when its merged
""" Return the first item from all items with the specified title.
Parameters:
title (str): Title of the item to return.
"""
for i in self.all():
if i.title.lower() == title.lower():
return i
def getByKey(self, key):
""" Return the first item from all items with the specified key.
Parameters:
key (str): Key of the item to return.
"""
return utils.findKey(self.server, key)
return self.fetchItems('/library/recentlyAdded')
def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain
@ -121,7 +103,7 @@ class Library(PlexObject):
for attr, value in kwargs.items():
args[attr] = value
key = '/library/all%s' % utils.joinArgs(args)
return self._fetchItems(key)
return self.fetchItems(key)
def cleanBundles(self):
""" Poster images and other metadata for items in your library are kept in "bundle"
@ -130,7 +112,7 @@ class Library(PlexObject):
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
"""
# TODO: Should this check the response for success or the correct mediaprefix?
self.server.query('/library/clean/bundles')
self._server._query('/library/clean/bundles')
def emptyTrash(self):
""" If a library has items in the Library Trash, use this option to empty the Trash. """
@ -142,16 +124,13 @@ class Library(PlexObject):
For example, if you have deleted or added an entire library or many items in a
library, you may like to optimize the database.
"""
self.server.query('/library/optimize')
self._root._query('/library/optimize')
def refresh(self):
""" Refresh the metadata for the entire library. This will fetch fresh metadata for
all contents in the library, including items that already have metadata.
"""
self.server.query('/library/sections/all/refresh')
def __len__(self):
return len(self.sections())
self._root._query('/library/sections/all/refresh')
class LibrarySection(PlexObject):
@ -205,10 +184,6 @@ class LibrarySection(PlexObject):
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.uuid = data.attrib.get('uuid')
def __repr__(self):
title = self.title.replace(' ', '.')[0:20]
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
def get(self, title):
""" Returns the media item with the specified title.
@ -216,16 +191,17 @@ class LibrarySection(PlexObject):
title (str): Title of the item to return.
"""
key = '/library/sections/%s/all' % self.key
return utils.findItem(self.server, key, title)
return self.fetchItem(key, title=title)
def all(self):
""" Returns a list of media from this library section. """
key = '/library/sections/%s/all' % self.key
return self._fetchItems(key)
return self.fetchItems(key)
def onDeck(self):
""" Returns a list of media items on deck from this library section. """
return utils.listItems(self.server, '/library/sections/%s/onDeck' % self.key)
key = '/library/sections/%s/onDeck' % self.key
return self.fetchItems(key)
def recentlyAdded(self, maxresults=50):
""" Returns a list of media items recently added from this library section.
@ -237,17 +213,20 @@ class LibrarySection(PlexObject):
def analyze(self):
""" Run an analysis on all of the items in this library section. """
self.server.query('/library/sections/%s/analyze' % self.key, method=self.server.session.put)
key = '/library/sections/%s/analyze' % self.key
self._server._query(key, method=self.server.session.put)
def emptyTrash(self):
""" If a section has items in the Trash, use this option to empty the Trash. """
self.server.query('/library/sections/%s/emptyTrash' % self.key)
key = '/library/sections/%s/emptyTrash' % self.key
self._server._query(key)
def refresh(self):
""" Refresh the metadata for this library section. This will fetch fresh metadata for
all contents in the section, including items that already have metadata.
"""
self.server.query('/library/sections/%s/refresh' % self.key)
key = '/library/sections/%s/refresh' % self.key
self._server._query(key)
def listChoices(self, category, libtype=None, **kwargs):
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
@ -263,6 +242,7 @@ class LibrarySection(PlexObject):
Raises:
:class:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
"""
# TODO: Should this be moved to base?
if category in kwargs:
raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
args = {}
@ -270,8 +250,8 @@ class LibrarySection(PlexObject):
args[category] = self._cleanSearchFilter(subcategory, value)
if libtype is not None:
args['type'] = utils.searchType(libtype)
query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
return utils.listItems(self.server, query, bytag=True)
key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
return self.fetchItems(key, bytag=True)
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
@ -303,8 +283,7 @@ class LibrarySection(PlexObject):
* studio: List of studios to search within ([studio_or_key, ...]). [music]
* year: List of years to search within ([yyyy, ...]). [all]
"""
# Cleanup the core arguments
# TODO: maxresults is raising a 500 error here.
# cleanup the core arguments
args = {}
for category, value in kwargs.items():
args[category] = self._cleanSearchFilter(category, value, libtype)
@ -314,14 +293,13 @@ class LibrarySection(PlexObject):
args['sort'] = self._cleanSearchSort(sort)
if libtype is not None:
args['type'] = utils.searchType(libtype)
# Iterate over the results
# iterate over the results
results, subresults = [], '_init'
args['X-Plex-Container-Start'] = 0
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
while subresults and maxresults > len(results):
query = '/library/sections/%s/all%s' % (
self.key, utils.joinArgs(args))
subresults = utils.listItems(self.server, query)
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
subresults = self.fetchItems(key)
results += subresults[:maxresults - len(results)]
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
return results
@ -342,16 +320,10 @@ class LibrarySection(PlexObject):
for item in value:
item = str(item.id if isinstance(item, MediaTag) else item).lower()
# find most logical choice(s) to use in url
if item in allowed:
result.add(item)
continue
if item in lookup:
result.add(lookup[item])
continue
if item in allowed: result.add(item); continue
if item in lookup: result.add(lookup[item]); continue
matches = [k for t, k in lookup.items() if item in t]
if matches:
map(result.add, matches)
continue
if matches: map(result.add, matches); continue
# nothing matched; use raw item value
log.warning('Filter value not listed, using raw item value: %s' % item)
result.add(item)
@ -435,7 +407,8 @@ class MusicSection(LibrarySection):
def albums(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
return utils.listItems(self.server, '/library/sections/%s/albums' % self.key)
key = '/library/sections/%s/albums' % self.key
return self.fetchItems(key)
def searchArtists(self, **kwargs):
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
@ -459,48 +432,22 @@ class PhotoSection(LibrarySection):
TYPE (str): 'photo'
"""
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
ALLOWED_SORT = ()
ALLOWED_SORT = ('addedAt')
TYPE = 'photo'
def searchAlbums(self, title, **kwargs): # lets use this for now.
def searchAlbums(self, title, **kwargs):
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
albums = utils.listItems(self.server, '/library/sections/%s/all?type=14' % self.key)
return [i for i in albums if i.title.lower() == title.lower()]
key = '/library/sections/%s/all?type=14' % self.key
return self.fetchItems(key, title=title)
def searchPhotos(self, title, **kwargs):
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
photos = utils.listItems(self.server, '/library/sections/%s/all?type=13' % self.key)
return [i for i in photos if i.title.lower() == title.lower()]
key = '/library/sections/%s/all?type=13' % self.key
return self.fetchItems(key, title=title)
@utils.register_libtype
class Hub(PlexObject):
TYPE = 'Hub'
FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role}
def _loadData(self, data):
self._data = data
self.hubIdentifier = data.attrib.get('hubIdentifier')
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)
def __repr__(self):
return '<Hub:%s>' % self.title.encode('utf8')
def __len__(self):
return self.size
def _buildItems(self, data):
if self.type in self.FILTERTYPES:
cls = self.FILTERTYPES[self.type]
return [cls(self._root, elem, self._initpath) for elem in data]
return super(Hub, self)._buildItems(data, safe=True)
@utils.register_libtype
class FilterChoice(object):
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
:func:`~plexapi.library.LibrarySection.listChoices()`.
@ -517,16 +464,33 @@ class FilterChoice(object):
"""
TYPE = 'Directory'
def __init__(self, server, data, initpath):
def _loadData(self, data):
self._data = data
self.server = server
self.initpath = initpath
self.fastKey = data.attrib.get('fastKey')
self.key = data.attrib.get('key')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
def __repr__(self):
title = self.title.replace(' ', '.')[0:20]
return '<%s:%s:%s>' % (self.__class__.__name__, self.key, title)
@utils.register_libtype
class Hub(PlexObject):
FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role}
TYPE = 'Hub'
def _loadData(self, data):
self._data = data
self.hubIdentifier = data.attrib.get('hubIdentifier')
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)
def __len__(self):
return self.size
def _buildItems(self, data):
if self.type in self.FILTERTYPES:
cls = self.FILTERTYPES[self.type]
return [cls(self._root, elem, self._initpath) for elem in data]
return super(Hub, self)._buildItems(data)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest
from plexapi.utils import cast, listItems
from plexapi.utils import cast
class Media(PlexObject):
@ -54,10 +54,6 @@ class Media(PlexObject):
self.width = cast(int, data.attrib.get('width'))
self.parts = self._buildItems(data, MediaPart)
def __repr__(self):
title = self.video.title.replace(' ','.')[0:20]
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
class MediaPart(PlexObject):
""" Represents a single media part (often a single file) for the media this belongs to.
@ -85,9 +81,6 @@ class MediaPart(PlexObject):
self.key = data.attrib.get('key')
self.size = cast(int, data.attrib.get('size'))
self.streams = self._buildStreams(data)
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.id)
def _buildStreams(self, data):
streams = []
@ -151,9 +144,6 @@ class MediaPartStream(PlexObject):
cls = STREAMCLS.get(stype, MediaPartStream)
return cls(server, data, initpath)
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.id)
class VideoStream(MediaPartStream):
""" Respresents a video stream within a :class:`~plexapi.media.MediaPart`.
@ -314,19 +304,13 @@ class MediaTag(PlexObject):
self.tagType = cast(int, data.attrib.get('tagType'))
self.thumb = data.attrib.get('thumb')
def __repr__(self):
tag = self.tag.replace(' ', '.')[0:20].encode('utf-8')
if self.librarySectionTitle:
return u'<%s:%s:%s:%s>' % (self.__class__.__name__, self.id, tag, self.librarySectionTitle)
return u'<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
def items(self, *args, **kwargs):
""" Return the list of items within this tag. This function is only applicable
in search results from PlexServer :func:`~plexapi.server.PlexServer.search()`.
"""
if not self.key:
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
return listItems(self.server, self.key)
return self.fetchItems(self.key)
class Collection(MediaTag):
@ -373,7 +357,3 @@ class Field(PlexObject):
self._data = data
self.name = data.attrib.get('name')
self.locked = cast(bool, data.attrib.get('locked'))
def __repr__(self):
name = self.name.replace(' ', '.')[0:20]
return '<%s:%s:%s>' % (self.__class__.__name__, name, self.locked)

View file

@ -55,9 +55,6 @@ class MyPlexAccount(PlexObject):
data = self._query(self.SIGNIN, method=self._session.post, auth=(username, password))
super(MyPlexAccount, self).__init__(self, data, self.SIGNIN)
def __repr__(self):
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
def _loadData(self, data):
self._data = data
self._token = data.attrib.get('authenticationToken')
@ -202,9 +199,6 @@ class MyPlexUser(PlexObject):
self.title = data.attrib.get('title')
self.username = data.attrib.get('username')
def __repr__(self):
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
class MyPlexResource(PlexObject):
""" This object represents resources connected to your Plex server that can provide
@ -254,9 +248,6 @@ class MyPlexResource(PlexObject):
self.presence = utils.cast(bool, data.attrib.get('presence'))
self.connections = self._buildItems(data, ResourceConnection)
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
def connect(self, ssl=None, safe=False):
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
one address specified for a server or client. This function will prioritize local
@ -332,9 +323,6 @@ class ResourceConnection(PlexObject):
self.local = utils.cast(bool, data.attrib.get('local'))
self.httpuri = 'http://%s:%s' % (self.address, self.port)
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
class MyPlexDevice(PlexObject):
""" This object represents resources connected to your Plex server that provide
@ -386,9 +374,6 @@ class MyPlexDevice(PlexObject):
self.screenDensity = data.attrib.get('screenDensity')
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
def __repr__(self):
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
def connect(self, safe=False):
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
one address specified for a server or client. After trying to connect to all

View file

@ -51,7 +51,7 @@ class Photoalbum(PlexPartialObject):
def photos(self):
""" Returns a list of :class:`~plexapi.photo.Photo` objects in this album. """
key = '/library/metadata/%s/children' % self.ratingKey
return self._fetchItems(key)
return self.fetchItems(key)
def photo(self, title):
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """
@ -114,7 +114,7 @@ class Photo(PlexPartialObject):
def photoalbum(self):
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
return utils.listItems(self._root, self.parentKey)[0]
return self.fetchItem(self.parentKey)
def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """

View file

@ -30,7 +30,7 @@ class Playlist(PlexPartialObject, Playable):
def items(self):
""" Returns a list of all items in the playlist. """
key = '%s/items' % self.key
return self._fetchItems(key)
return self.fetchItems(key)
def addItems(self, items):
"""Add items to a playlist."""
@ -43,27 +43,27 @@ class Playlist(PlexPartialObject, Playable):
ratingKeys.append(str(item.ratingKey))
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '%s/items%s' % (self.key, utils.joinArgs({
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
key = '%s/items%s' % (self.key, utils.joinArgs({
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys)
}))
return self._root._query(path, method=self._root._session.put)
return self._root._query(key, method=self._root._session.put)
def removeItem(self, item):
"""Remove a file from a playlist."""
path = '%s/items/%s' % (self.key, item.playlistItemID)
return self._root._query(path, method=self._root._session.delete)
key = '%s/items/%s' % (self.key, item.playlistItemID)
return self._root._query(key, method=self._root._session.delete)
def moveItem(self, item, after=None):
"""Move a to a new position in playlist."""
path = '%s/items/%s/move' % (self.key, item.playlistItemID)
key = '%s/items/%s/move' % (self.key, item.playlistItemID)
if after:
path += '?after=%s' % after.playlistItemID
return self._root._query(path, method=self._root._session.put)
key += '?after=%s' % after.playlistItemID
return self._root._query(key, method=self._root._session.put)
def edit(self, title=None, summary=None):
"""Edit playlist."""
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
return self._root._query(path, method=self._root._session.put)
key = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
return self._root._query(key, method=self._root._session.put)
def delete(self):
"""Delete playlist."""
@ -81,11 +81,11 @@ class Playlist(PlexPartialObject, Playable):
ratingKeys.append(str(item.ratingKey))
ratingKeys = ','.join(ratingKeys)
uuid = items[0].section().uuid
path = '/playlists%s' % utils.joinArgs({
key = '/playlists%s' % utils.joinArgs({
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
'type': items[0].listType,
'title': title,
'smart': 0
})
data = server._query(path, method=server._session.post)[0]
return cls(server, data, initpath=path)
data = server._query(key, method=server._session.post)[0]
return cls(server, data, initpath=key)

View file

@ -140,9 +140,6 @@ class PlexServer(PlexObject):
self.version = data.attrib.get('version')
self.voiceSearch = cast(bool, data.attrib.get('voiceSearch'))
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
def _query(self, key, method=None, headers=None, **kwargs):
""" Main method used to handle HTTPS requests to the Plex server. This method helps
by encoding the response to utf-8 and parsing the returned XML into and
@ -233,14 +230,13 @@ class PlexServer(PlexObject):
def history(self):
""" Returns a list of media items from watched history. """
#return utils.listItems(self, '/status/sessions/history/all')
return self._fetchItems('/status/sessions/history/all')
return self.fetchItems('/status/sessions/history/all')
def playlists(self):
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects saved on the server. """
# TODO: Add sort and type options?
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
return self._fetchItems('/playlists')
return self.fetchItems('/playlists')
def playlist(self, title):
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
@ -251,10 +247,7 @@ class PlexServer(PlexObject):
Raises:
:class:`~plexapi.exceptions.NotFound`: Invalid playlist title
"""
for item in self.playlists():
if item.title == title:
return item
raise NotFound('Invalid playlist title: %s' % title)
return self.fetchItem('/playlists', title=title)
def search(self, query, mediatype=None, limit=None):
""" Returns a list of media items or filter categories from the resulting
@ -280,14 +273,13 @@ 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, bytag=True):
results += hub.items
return results
def sessions(self):
""" Returns a list of all active session (currently playing) media objects. """
return self._fetchItems('/status/sessions')
#return utils.listItems(self, '/status/sessions')
return self.fetchItems('/status/sessions')
def transcodeImage(self, media, height, width, opacity=100, saturation=100):
""" Returns the URL for a transcoded image from the specified media object.
@ -300,6 +292,7 @@ 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)

View file

@ -39,9 +39,6 @@ class SyncItem(object):
self.policy = data.find('Policy').attrib.copy()
self.location = data.find('Location').attrib.copy()
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.id)
def server(self):
server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self._servers))
if 0 == len(server):
@ -50,11 +47,11 @@ class SyncItem(object):
def getMedia(self):
server = self.server().connect()
items = utils.listItems(server, '/sync/items/%s' % self.id)
return items
key = '/sync/items/%s' % self.id
return server.fetchItems(key)
def markAsDone(self, sync_id):
server = self.server().connect()
url = '/sync/%s/%s/files/%s/downloaded' % (
self._device.clientIdentifier, server.machineIdentifier, sync_id)
server.query(url, method=requests.put)
server._query(url, method=requests.put)

View file

@ -39,28 +39,6 @@ def register_libtype(cls):
return cls
# def buildItem(server, elem, initpath, bytag=False):
# """ Factory function to build the objects used within the PlexAPI.
# Parameters:
# server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
# elem (ElementTree): XML data needed to build the object.
# initpath (str): Relative path requested when retrieving specified `data` (optional).
# bytag (bool): Creates the object from the name specified by the tag instead of the
# default which builds the object specified by the type attribute. <tag type='foo' />
# Raises:
# UnknownType: Unknown library type.
# """
# libtype = elem.tag if bytag else elem.attrib.get('type')
# if libtype == 'photo' and elem.tag == 'Directory':
# libtype = 'photoalbum'
# if libtype in LIBRARY_TYPES:
# cls = LIBRARY_TYPES[libtype]
# return cls(server, elem, initpath)
# raise UnknownType('Unknown library type: %s' % libtype)
def cast(func, value):
""" Cast the specified value to the specified type (returned by func). Currently this
only support int, float, bool. Should be extended if needed.
@ -81,42 +59,6 @@ def cast(func, value):
return value
def findKey(server, key):
""" Finds and builds a object based on ratingKey.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
key (int): ratingKey to find and return.
Raises:
NotFound: Unable to find key
"""
path = '/library/metadata/{0}'.format(key)
try:
# Item seems to be the first sub element
elem = server.query(path)[0]
return buildItem(server, elem, path)
except:
raise NotFound('Unable to find key: %s' % key)
def findItem(server, path, title):
""" Finds and builds a object based on title.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): API path that returns item to search title for.
title (str): Title of the item to find and return.
Raises:
NotFound: Unable to find item.
"""
for elem in server.query(path):
if elem.attrib.get('title').lower() == title.lower():
return buildItem(server, elem, path)
raise NotFound('Unable to find item: %s' % title)
def findLocations(data, single=False):
""" Returns a list of filepaths from a location tag.
@ -225,33 +167,7 @@ def listChoices(server, path):
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
"""
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None, bytag=False):
""" Returns a list of object built from :func:`~plexapi.utils.buildItem()` found
within the specified path.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
libtype (str): Optionally return only the specified library type.
watched (bool): Optionally return only watched or unwatched items.
bytag (bool): Set true if libtype is found in the XML tag (and not the 'type' attribute).
"""
items = []
for elem in server.query(path):
if libtype and elem.attrib.get('type') != libtype:
continue
if watched is True and int(elem.attrib.get('viewCount', 0)) == 0:
continue
if watched is False and int(elem.attrib.get('viewCount', 0)) >= 1:
continue
try:
items.append(buildItem(server, elem, path, bytag))
except UnknownType:
pass
return items
return {c.attrib['title']: c.attrib['key'] for c in server._query(path)}
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from plexapi import media, utils
from plexapi.exceptions import NotFound
from plexapi.exceptions import BadRequest, NotFound
from plexapi.base import Playable, PlexPartialObject
@ -36,24 +36,25 @@ class Video(PlexPartialObject):
that are useful to knowwhether it's a video file,
a music track, or one of your photos.
"""
self._root.query('/%s/analyze' % self.key.lstrip('/'), method=self._root._session.put)
key = '/%s/analyze' % self.key.lstrip('/')
self._root._query(key, method=self._root._session.put)
def markWatched(self):
"""Mark a items as watched."""
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._root.query(path)
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._root._query(key)
self.reload()
def markUnwatched(self):
"""Mark a item as unwatched."""
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._root.query(path)
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._root._query(key)
self.reload()
def refresh(self):
"""Refresh a item."""
self._root.query('%s/refresh' %
self.key, method=self._root._session.put)
key = '%s/refresh' % self.key
self._root._query(key, method=self._root._session.put)
def section(self):
"""Library section."""
@ -69,13 +70,12 @@ class Movie(Video, Playable):
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
normally built from server._query
"""
Video._loadData(self, data)
Playable._loadData(self, data)
self.art = data.attrib.get('art')
self.audienceRating = utils.cast(
float, data.attrib.get('audienceRating'))
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
self.chapterSource = data.attrib.get('chapterSource')
self.contentRating = data.attrib.get('contentRating')
@ -101,9 +101,6 @@ class Movie(Video, Playable):
self.producers = self._buildItems(data, media.Producer)
self.roles = self._buildItems(data, media.Role)
self.writers = self._buildItems(data, media.Writer)
# self.videoStreams = utils.findStreams(self.media, 'videostream') # these dont go here
# self.audioStreams = utils.findStreams(self.media, 'audiostream') # these dont go here
# self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') # these dont go here
@property
def actors(self):
@ -181,7 +178,7 @@ class Show(Video):
def seasons(self):
"""Returns a list of Season."""
key = '/library/metadata/%s/children' % self.ratingKey
return self._fetchItems(key, Season.TYPE)
return self.fetchItems(key, type=Season.TYPE)
def season(self, title=None):
""" Returns the season with the specified title or number.
@ -192,16 +189,12 @@ class Show(Video):
if isinstance(title, int):
title = 'Season %s' % title
key = '/library/metadata/%s/children' % self.ratingKey
return self._fetchItem(key, title)
return self.fetchItem(key, tag='Directory', title=title)
def episodes(self, watched=None):
"""Returs a list of Episode
Args:
watched (bool): Defaults to None. Exclude watched episodes
"""
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.listItems(self._root, leavesKey, watched=watched)
def episodes(self):
""" Returs a list of Episode """
key = '/library/metadata/%s/allLeaves' % self.ratingKey
return self.fetchItems(key)
def episode(self, title=None, season=None, episode=None):
"""Find a episode using a title or season and episode.
@ -231,8 +224,8 @@ class Show(Video):
if not title and (not season or not episode):
raise TypeError('Missing argument: title or season and episode are required')
if title:
path = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.findItem(self._root, path, title)
key = '/library/metadata/%s/allLeaves' % self.ratingKey
return self._findItem(key, title)
elif season and episode:
results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode]
if results:
@ -261,7 +254,7 @@ class Show(Video):
def refresh(self):
"""Refresh the metadata."""
self._root.query('/library/metadata/%s/refresh' % self.ratingKey, method=self._root._session.put)
self._root._query('/library/metadata/%s/refresh' % self.ratingKey, method=self._root._session.put)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
@ -280,7 +273,7 @@ class Season(Video):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
data (Element): Usually built from server._query
"""
Video._loadData(self, data)
self.key = self.key.replace('/children', '')
@ -291,6 +284,13 @@ class Season(Video):
self.parentTitle = data.attrib.get('parentTitle')
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
def __repr__(self):
return '<%s>' % ':'.join([p for p in [
self.__class__.__name__,
self.key.replace('/library/metadata/', '').replace('/children', ''),
'%s-s%s' % (self.parentTitle.replace(' ','-')[:20], self.seasonNumber),
] if p])
@property
def isWatched(self):
return bool(self.viewedLeafCount == self.leafCount)
@ -307,27 +307,19 @@ class Season(Video):
watched (bool): Defaults to None. Exclude watched episodes
"""
key = '/library/metadata/%s/children' % self.ratingKey
return self._fetchItems(key, Episode.TYPE)
# childrenKey = '/library/metadata/%s/children' % self.ratingKey
# return utils.listItems(self._root, childrenKey, watched=watched)
return self.fetchItems(key, type=Episode.TYPE)
def episode(self, title=None, episode=None):
"""Find a episode using a title or season and episode.
def episode(self, title=None, num=None):
""" Returns the episode with the given title or number.
Note:
episode is required if title is missing.
Args:
title (str): Default None
episode (int): Episode number, default None
Parameters:
title (str): Title of the episode to return.
num (int): Number of the episode to return (if title not specified).
Raises:
TypeError: If title and episode is missing.
NotFound: If that episode cant be found.
Returns:
Episode
Examples:
>>> plex.search('The blacklist').season(1).episode(episode=1)
<Episode:116263:The.Freelancer>
@ -335,31 +327,21 @@ class Season(Video):
<Episode:116263:The.Freelancer>
"""
if not title and not episode:
raise TypeError('Missing argument, you need to use title or episode.')
if not title and not num:
raise BadRequest('Missing argument, you need to use title or episode.')
key = '/library/metadata/%s/children' % self.ratingKey
if title:
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self._root, path, title)
elif episode:
results = [i for i in self.episodes() if i.seasonNumber == self.index and i.index == episode]
if results:
return results[0]
raise NotFound('Couldnt find %s.Season %s Episode %s.' % (self.grandparentTitle, self.index. episode))
return self._findItem(key, title=title)
return self._findItem(key, seasonNumber=self.index, index=num)
def get(self, title):
"""Get a episode with a matching title.
Args:
title (str): fx Secret santa
Returns:
Episode
"""
""" Alias for self.episode. """
return self.episode(title)
def show(self):
"""Return this seasons show."""
return utils.listItems(self._root, self.parentKey)[0]
return self.fetchItem(self.parentKey)
def watched(self):
"""Returns a list of watched Episode"""
@ -369,12 +351,6 @@ class Season(Video):
"""Returns a list of unwatched Episode"""
return self.episodes(watched=False)
def __repr__(self):
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '').replace('/children', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s:%s>' % (clsname, key, self.parentTitle, title)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
for ep in self.episodes():
@ -392,7 +368,7 @@ class Episode(Video, Playable):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
data (Element): Usually built from server._query
"""
Video._loadData(self, data)
Playable._loadData(self, data)
@ -427,10 +403,11 @@ class Episode(Video, Playable):
self.transcodeSession = utils.findTranscodeSession(self._root, data)
def __repr__(self):
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '').replace('/children', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s:S%s:E%s:%s>' % (clsname, key, self.grandparentTitle, self.seasonNumber, self.index, title)
return '<%s>' % ':'.join([p for p in [
self.__class__.__name__,
self.key.replace('/library/metadata/', '').replace('/children', ''),
'%s-s%se%s' % (self.grandparentTitle.replace(' ','-')[:20], self.seasonNumber, self.index),
] if p])
@property
def isWatched(self):
@ -452,11 +429,11 @@ class Episode(Video, Playable):
def season(self):
"""Return this episode Season"""
return utils.listItems(self._root, self.parentKey)[0]
return self.fetchItem(self.parentKey)
def show(self):
"""Return this episodes Show"""
return utils.listItems(self._root, self.grandparentKey)[0]
return self.fetchItem(self.grandparentKey)
@property
def location(self):