From 355f4686c98d0afec2e58e80f8f3ad3fd0e25802 Mon Sep 17 00:00:00 2001 From: Michael Shepanski Date: Thu, 26 Jan 2017 00:25:13 -0500 Subject: [PATCH] Finish documenting utils.py --- plexapi/library.py | 3 +- plexapi/utils.py | 319 +++++++++++++++++++-------------------------- 2 files changed, 136 insertions(+), 186 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index 64d42ddf..366a27b4 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from plexapi import log, utils from plexapi import X_PLEX_CONTAINER_SIZE from plexapi.compat import unquote @@ -156,7 +155,7 @@ class LibrarySection(object): """ Base class for a single library section. Parameters: - server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional) + server (:class:`~plexapi.server.PlexServer`): PlexServer object this library section is from. data (:class:`ElementTree`): Response from PlexServer used to build this object (optional). initpath (str): Relative path requested when retrieving specified `data` (optional). diff --git a/plexapi/utils.py b/plexapi/utils.py index 87937c6c..8c88c53e 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -7,9 +7,8 @@ from plexapi.exceptions import NotFound, UnknownType, Unsupported from threading import Thread # Search Types - Plex uses these to filter specific media types when searching. -SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, - 'episode': 4, 'artist': 8, 'album': 9, 'track': 10} - +SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, + 'artist': 8, 'album': 9, 'track': 10} LIBRARY_TYPES = {} @@ -45,132 +44,102 @@ NA = _NA() class PlexPartialObject(object): - """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. + """ 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: - initpath (str): Relative url to PMS - server (): Description + Attributes: + data (:class:`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 __init__(self, data, initpath, server=None): - """ - Args: - data (xml.etree.ElementTree.Element): passed from server.query - initpath (str): Relative path - server (None or Plexserver, optional): PMS class your connected to - """ self.server = server self.initpath = initpath self._loadData(data) def __eq__(self, other): - """Summary - - Args: - other (TYPE): Description - - Returns: - TYPE: Description - """ return other is not None and self.key == other.key def __repr__(self): - """Pretty repr.""" 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 __getattr__(self, attr): - """Auto reload self, if the attribute is NA - - Args: - attr (str): fx key - """ + # Auto reload self, from the full key (path) when needed. if attr == 'key' or self.__dict__.get(attr) or self.isFullObject(): return self.__dict__.get(attr, NA) self.reload() return self.__dict__.get(attr, NA) def __setattr__(self, attr, value): - """Set attribute - - Args: - attr (str): fx key - value (TYPE): Description - """ if value != NA or self.isFullObject(): self.__dict__[attr] = value def _loadData(self, data): - """Uses a element to set a attrs. - - Args: - data (Element): Used by attrs - """ raise Exception('Abstract method not implemented.') 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() def reload(self): - """Reload the data for this object from PlexServer XML.""" + """ Reload the data for this object from PlexServer XML. """ data = self.server.query(self.key) self.initpath = self.key self._loadData(data[0]) 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, Albums which are all not playable. + """ 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, + Albums which are all not playable. - Attributes: # todo - player (Plexclient): Player - playlistItemID (int): Playlist item id - sessionKey (int): 1223 - transcodeSession (str): 12312312 - username (str): Fx Hellowlol - viewedAt (datetime): viewed at. + Attributes: + player (:class:`~plexapi.client.PlexClient`): Client object playing this item (for active sessions). + playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items). + sessionKey (int): Active session key. + transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object + if item is being transcoded (None otherwise). + username (str): Username of the person playing this item (for active sessions). + viewedAt (datetime): Datetime item was last viewed (history). """ def _loadData(self, data): - """Set the class attributes - - Args: - data (xml.etree.ElementTree.Element): usually from server.query - """ - # data for active sessions (/status/sessions) + # Load data for active sessions (/status/sessions) self.sessionKey = cast(int, data.attrib.get('sessionKey', NA)) self.username = findUsername(data) self.player = findPlayer(self.server, data) self.transcodeSession = findTranscodeSession(self.server, data) - # data for history details (/status/sessions/history/all) + # Load data for history details (/status/sessions/history/all) self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA)) - # data for playlist items + # Load data for playlist items self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA)) def getStreamURL(self, **params): - """Make a stream url that can be used by vlc. + """ Returns a stream url that may be used by external applications such as VLC. - Args: - **params (dict): Description + Parameters: + **params (dict): optional parameters to manipulate the playback when accessing + the stream. A few known parameters include: maxVideoBitrate, videoResolution + offset, copyts, protocol, mediaIndex, platform. - Returns: - string: '' - - Raises: - Unsupported: Raises a error is the type is wrong. + Raises: + Unsupported: When the item doesn't support fetching a stream URL. """ if self.TYPE not in ('movie', 'episode', 'track'): - raise Unsupported( - 'Fetching stream URL for %s is unsupported.' % self.TYPE) + raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE) mvb = params.get('maxVideoBitrate') vr = params.get('videoResolution', '') params = { @@ -189,32 +158,32 @@ class Playable(object): return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params))) def iterParts(self): - """Yield parts.""" + """ Iterates over the parts of this media item. """ for item in self.media: for part in item.parts: yield part def play(self, client): - """Start playback on a client. + """ Start playback on the specified client. - Args: - client (PlexClient): The client to start playing on. + Parameters: + client (:class:`~plexapi.client.PlexClient`): Client to start playing on. """ client.playMedia(self) def buildItem(server, elem, initpath, bytag=False): - """Build classes used by the plexapi. + """ Factory function to build the objects used within the PlexAPI. - Args: - server (Plexserver): Your connected to. - elem (xml.etree.ElementTree.Element): xml from PMS - initpath (str): Relative path - bytag (bool, optional): Description # figure out what this do - - Raises: - UnknownType: Unknown library type libtype + 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': @@ -226,11 +195,11 @@ def buildItem(server, elem, initpath, bytag=False): def cast(func, value): - """Helper to change to the correct type + """ Cast the specified value to the specified type (returned by func). - Args: - func (function): function to used [int, bool float] - value (string, int, float): value to cast + Parameters: + func (func): Calback function to used cast to type (int, bool, float, etc). + value (any): value to be cast and returned. """ if value not in [None, NA]: if func == bool: @@ -245,14 +214,14 @@ def cast(func, value): def findKey(server, key): - """Finds and builds a object based on ratingKey. + """ Finds and builds a object based on ratingKey. - Args: - server (Plexserver): PMS your connected to - key (int): key to look for + Parameters: + server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from. + key (int): ratingKey to find and return. - Raises: - NotFound: Unable to find key. Key + Raises: + NotFound: Unable to find key """ path = '/library/metadata/{0}'.format(key) try: @@ -264,15 +233,15 @@ def findKey(server, key): def findItem(server, path, title): - """Finds and builds a object based on title. + """ Finds and builds a object based on title. - Args: - server (Plexserver): Description - path (str): Relative path - title (str): Fx 16 blocks + 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: title + Raises: + NotFound: Unable to find item. """ for elem in server.query(path): if elem.attrib.get('title').lower() == title.lower(): @@ -281,14 +250,12 @@ def findItem(server, path, title): def findLocations(data, single=False): - """Extract the path from a location tag + """ Returns a list of filepaths from a location tag. - Args: - data (xml.etree.ElementTree.Element): xml from PMS as Element - single (bool, optional): Only return one - - Returns: - filepath string if single is True else list of filepaths + Parameters: + data (ElementTree): XML object to search for locations in. + single (bool): Set True to only return the first location found. + Return type will be a string if this is set to True. """ locations = [] for elem in data: @@ -300,33 +267,26 @@ def findLocations(data, single=False): def findPlayer(server, data): - """Find a player in a elementthee + """ Returns the :class:`~plexapi.client.PlexClient` object found in the specified data. - Args: - server (Plexserver): PMS your connected to - data (xml.etree.ElementTree.Element): xml from pms as a element - - Returns: - PlexClient or None + Parameters: + server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from. + data (:class:`ElementTree`): XML data to find Player in. """ elem = data.find('Player') if elem is not None: from plexapi.client import PlexClient - baseurl = 'http://%s:%s' % (elem.attrib.get('address'), - elem.attrib.get('port')) + baseurl = 'http://%s:%s' % (elem.attrib.get('address'), elem.attrib.get('port')) return PlexClient(baseurl, server=server, data=elem) return None def findStreams(media, streamtype): - """Find streams. + """ Returns a list of streams (str) found in media that match the specified streamtype. - Args: - media (Show, Movie, Episode): A item where find streams - streamtype (str): Possible options [movie, show, episode] # is this correct? - - Returns: - list: of streams + Parameters: + media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode). + streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream). """ streams = [] for mediaitem in media: @@ -338,14 +298,12 @@ def findStreams(media, streamtype): def findTranscodeSession(server, data): - """Find transcode session. + """ Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified + XML data. - Args: - server (Plexserver): PMS your connected to - data (xml.etree.ElementTree.Element): XML response from PMS as Element - - Returns: - media.TranscodeSession or None + Parameters: + server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from. + data (:class:`ElementTree`): XML data to find TranscodeSession in. """ elem = data.find('TranscodeSession') @@ -356,13 +314,10 @@ def findTranscodeSession(server, data): def findUsername(data): - """Find a username in a Element + """ Returns the username if found in the specified XML data. Returns None if not found. - Args: - data (xml.etree.ElementTree.Element): XML from PMS as a Element - - Returns: - username or None + Parameters: + data (:class:`ElementTree`): XML data to find username in. """ elem = data.find('User') if elem is not None: @@ -371,7 +326,7 @@ def findUsername(data): def isInt(str): - """Check of a string is a int""" + """ Returns True if the specified string passes as an int. """ try: int(str) return True @@ -380,14 +335,11 @@ def isInt(str): def joinArgs(args): - """Builds a query string where only - the value is quoted. + """ Returns a query string (uses for HTTP URLs) where only the value is URL encoded. + Example return value: '?genre=action&type=1337'. - Args: - args (dict): ex {'genre': 'action', 'type': 1337} - - Returns: - string: ?genre=action&type=1337 + Parameters: + args (dict): Arguments to include in query string. """ if not args: return '' @@ -399,30 +351,25 @@ def joinArgs(args): def listChoices(server, path): - """ListChoices is by _cleanSort etc. + """ Returns a dict of {title:key} for all simple choices in a search filter. - Args: - server (Plexserver): Server your connected to - path (str): Relative path to PMS - - Returns: - dict: title:key + Parameters: + 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): - """Return a list buildItem. See buildItem doc. + """ Returns a list of object built from :func:`~plexapi.utils.buildItem()` found + within the specified path. - Args: - server (Plexserver): PMS your connected to. - path (str): Relative path to PMS - libtype (None or string, optional): [movie, show, episode, music] # check me - watched (None, True, False, optional): Skip or include watched items - bytag (bool, optional): Dunno wtf this is used for # todo - - Returns: - list: of buildItem + 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): @@ -440,6 +387,17 @@ def listItems(server, path, libtype=None, watched=None, bytag=False): def rget(obj, attrstr, default=None, delim='.'): + """ Returns the value at the specified attrstr location within a nexted tree of + dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley + for each key in attrstr (split by by the delimiter) This function is heavily + influenced by the lookups used in Django templates. + + Parameters: + obj (any): Object to start the lookup in (dict, obj, list, tuple, etc). + attrstr (str): String to lookup (ex: 'foo.bar.baz.value') + default (any): Default value to return if not found. + delim (str): Delimiter separating keys in attrstr. + """ try: parts = attrstr.split(delim, 1) attr = parts[0] @@ -460,19 +418,15 @@ def rget(obj, attrstr, default=None, delim='.'): def searchType(libtype): - """Map search type name to int using SEACHTYPES - Used when querying PMS. + """ Returns the integer value of the library string type. - Args: - libtype (str): Possible options see SEARCHTYPES + Parameters: + libtype (str): Library type to lookup (movie, show, season, episode, + artist, album, track) - Returns: - int: fx 1 - - Raises: - NotFound: Unknown libtype: libtype + Raises: + NotFound: Unknown libtype """ - libtype = str(libtype) if libtype in [str(v) for v in SEARCHTYPES.values()]: return libtype @@ -482,12 +436,12 @@ def searchType(libtype): def threaded(callback, listargs): - """Run some function in threads. - - Args: - callback (function): funcion to run in thread - listargs (list): args parssed to the callback + """ Returns the result of for each set of *args in listargs. Each call + to