diff --git a/plexapi/__init__.py b/plexapi/__init__.py index afb163df..ed275233 100644 --- a/plexapi/__init__.py +++ b/plexapi/__init__.py @@ -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 diff --git a/plexapi/audio.py b/plexapi/audio.py index fbc18f30..58f8f19c 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -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] diff --git a/plexapi/base.py b/plexapi/base.py index 2772ba4b..36af28a3 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -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() diff --git a/plexapi/client.py b/plexapi/client.py index 7b7cd783..7ea170c9 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -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, diff --git a/plexapi/library.py b/plexapi/library.py index 3b38ff04..21140f21 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -27,8 +27,8 @@ class Library(PlexObject): self.title1 = data.attrib.get('title1') self.title2 = data.attrib.get('title2') - def __repr__(self): - return '' % 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 '' % 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) diff --git a/plexapi/media.py b/plexapi/media.py index 886b1495..62548945 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -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) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 13f9e19d..c9d6cf0f 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -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 diff --git a/plexapi/photo.py b/plexapi/photo.py index 1f171fbb..403f0c8b 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -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. """ diff --git a/plexapi/playlist.py b/plexapi/playlist.py index e02d2896..c61c8deb 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -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) diff --git a/plexapi/server.py b/plexapi/server.py index 914e4b55..be84c056 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -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) diff --git a/plexapi/sync.py b/plexapi/sync.py index 0aeaf259..6e42ef92 100644 --- a/plexapi/sync.py +++ b/plexapi/sync.py @@ -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) diff --git a/plexapi/utils.py b/plexapi/utils.py index 45087f47..3f14d08f 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -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. - -# 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 diff --git a/plexapi/video.py b/plexapi/video.py index 1163bd7a..d03963c1 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -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 know–whether 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) @@ -335,31 +327,21 @@ class Season(Video): """ - 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):