diff --git a/plexapi/server.py b/plexapi/server.py index 6143d8b3..dbf46296 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + """ PlexServer """ @@ -183,19 +184,20 @@ class PlexServer(object): path (sting): relative path to PMS, fx /search?query=HELLO method (None, optional): requests.method, fx requests.put headers (None, optional): Headers that will be passed to PMS - **kwargs (dict): Loads of different stuff + **kwargs (dict): Used for filter and sorting. Raises: BadRequest: Description Returns: - ElementTree or None + xml.etree.ElementTree.Element or None """ url = self.url(path) method = method or self.session.get log.info('%s %s', method.__name__.upper(), url) - h = headers.copy() - h.update(headers or {}) + h = self.headers().copy() + if headers: + h.update(headers) response = method(url, headers=h, timeout=TIMEOUT, **kwargs) if response.status_code not in [200, 201]: codename = codes.get(response.status_code)[0] @@ -236,7 +238,7 @@ class Account(object): is not required to get basic plex information. Attributes: - authToken (TYPE): Description + authToken (sting): X-Plex-Token, using for authenication with PMS mappingError (TYPE): Description mappingErrorMessage (TYPE): Description mappingState (TYPE): Description @@ -252,6 +254,10 @@ class Account(object): """ def __init__(self, server, data): + """Args: + server (Plexclient): + data (xml.etree.ElementTree.Element): used to set the class attributes. + """ self.authToken = data.attrib.get('authToken') self.username = data.attrib.get('username') self.mappingState = data.attrib.get('mappingState') diff --git a/plexapi/utils.py b/plexapi/utils.py index 8779269c..000f27b7 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -39,15 +39,38 @@ class _NA(object): """ def __bool__(self): + """Summary + + Returns: + TYPE: Description + """ return False def __eq__(self, other): + """Summary + + Args: + other (TYPE): Description + + Returns: + TYPE: Description + """ return isinstance(other, _NA) or other in [None, '__NA__'] def __nonzero__(self): + """Summary + + Returns: + TYPE: Description + """ return False def __repr__(self): + """Summary + + Returns: + TYPE: Description + """ return '__NA__' NA = _NA() @@ -65,11 +88,25 @@ class PlexPartialObject(object): """ def __init__(self, data, initpath, server=None): + """ + Args: + data (xml.etree.ElementTree.Element): passed from server.query + initpath (string): 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): @@ -80,18 +117,32 @@ class PlexPartialObject(object): return '<%s:%s:%s>' % (clsname, key, title) def __getattr__(self, attr): - """Auto reload self, if the attribute is NA""" + """Auto reload self, if the attribute is NA + + Args: + attr (string): fx key + """ 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 (string): fx key + value (TYPE): Description + """ if value != NA or self.isFullObject(): - self.__dict__[attr] = value # twice as fast - #super(PlexPartialObject, self).__setattr__(attr, value) + 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): @@ -101,29 +152,32 @@ class PlexPartialObject(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: + Attributes: # todo player (TYPE): Description playlistItemID (TYPE): Description sessionKey (TYPE): Description transcodeSession (TYPE): Description username (TYPE): Description - viewedAt (TYPE): Description + viewedAt (datetime): Description """ def _loadData(self, data): + """Set the class attributes + + Args: + data (xml.etree.ElementTree.Element): usually from server.query + """ # data for active sessions (/status/sessions) self.sessionKey = cast(int, data.attrib.get('sessionKey', NA)) self.username = findUsername(data) @@ -135,6 +189,17 @@ class Playable(object): self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA)) def getStreamURL(self, **params): + """Make a stream url that can be used by vlc. + + Args: + **params (dict): Description + + Returns: + string: '' + + Raises: + Unsupported: Raises a error is the type is wrong. + """ if self.TYPE not in ('movie', 'episode', 'track'): raise Unsupported( 'Fetching stream URL for %s is unsupported.' % self.TYPE) @@ -156,32 +221,31 @@ class Playable(object): return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params))) def iterParts(self): - """Yield parts - """ + """Yield parts.""" for item in self.media: for part in item.parts: yield part def play(self, client): - """Start playback on a client.""" + """Start playback on a client. + + Args: + client (PlexClient): The client to start playing on. + """ client.playMedia(self) def buildItem(server, elem, initpath, bytag=False): """Build classes used by the plexapi. - Args: - server (Plexserver): Server - elem (ElementThree): - initpath (string): init path of the url, used to determin - if this is a full object. - bytag (bool): Dunno # TOO + Args: + server (Plexserver): Your connected to. + elem (xml.etree.ElementTree.Element): xml from PMS + initpath (string): Relative path + bytag (bool, optional): Description # figure out what this do - Returns: - library type - - Raises: - UnknownType + Raises: + UnknownType: Unknown library type libtype """ libtype = elem.tag if bytag else elem.attrib.get('type') @@ -194,7 +258,12 @@ def buildItem(server, elem, initpath, bytag=False): def cast(func, value): - """Helper to change to the correct type""" + """Helper to change to the correct type + + Args: + func (function): function to used [int, bool float] + value (string, int, float): value to cast + """ if value not in [None, NA]: if func == bool: return bool(int(value)) @@ -208,7 +277,15 @@ 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 + + Raises: + NotFound: Unable to find key. Key + """ path = '/library/metadata/{0}'.format(key) try: # Item seems to be the first sub element @@ -219,7 +296,16 @@ 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 (string): Relative path + title (string): Fx 16 blocks + + Raises: + NotFound: Unable to find item: title + """ for elem in server.query(path): if elem.attrib.get('title').lower() == title.lower(): return buildItem(server, elem, path) @@ -229,12 +315,12 @@ def findItem(server, path, title): def findLocations(data, single=False): """Extract the path from a location tag - Args: - data (elementthree): - single (bool): One or more locations + Args: + data (xml.etree.ElementTree.Element): xml from PMS as Element + single (bool, optional): Only return one - Returns: - list: of filepaths + Returns: + filepath string if single is True else list of filepaths """ locations = [] for elem in data: @@ -248,11 +334,12 @@ def findLocations(data, single=False): def findPlayer(server, data): """Find a player in a elementthee - Args: - data (elementthree): + Args: + server (Plexserver): PMS your connected to + data (xml.etree.ElementTree.Element): xml from pms as a element - Returns: - PlexClient or None + Returns: + PlexClient or None """ elem = data.find('Player') if elem is not None: @@ -264,6 +351,15 @@ def findPlayer(server, data): def findStreams(media, streamtype): + """Find streams. + + Args: + media (Show, Movie, Episode): A item where find streams + streamtype (string): Possible options [movie, show, episode] # is this correct? + + Returns: + list: of streams + """ streams = [] for mediaitem in media: for part in mediaitem.parts: @@ -274,6 +370,16 @@ def findStreams(media, streamtype): def findTranscodeSession(server, data): + """Find transcode session. + + Args: + server (Plexserver): PMS your connected to + data (xml.etree.ElementTree.Element): XML response from PMS as Element + + Returns: + media.TranscodeSession or None + """ + elem = data.find('TranscodeSession') if elem is not None: from plexapi import media @@ -282,6 +388,14 @@ def findTranscodeSession(server, data): def findUsername(data): + """Find a username in a Element + + Args: + data (xml.etree.ElementTree.Element): XML from PMS as a Element + + Returns: + username or None + """ elem = data.find('User') if elem is not None: return elem.attrib.get('title') @@ -289,6 +403,7 @@ def findUsername(data): def isInt(string): + """Check of a string is a int""" try: int(string) return True @@ -297,6 +412,15 @@ def isInt(string): def joinArgs(args): + """Builds a query string where only + the value is quoted. + + Args: + args (dict): ex {'genre': 'action', 'type': 1337} + + Returns: + string: ?genre=action&type=1337 + """ if not args: return '' arglist = [] @@ -307,10 +431,31 @@ def joinArgs(args): def listChoices(server, path): + """ListChoices is by _cleanSort etc. + + Args: + server (Plexserver): Server your connected to + path (string): Relative path to PMS + + Returns: + dict: title:key + """ 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. + + Args: + server (Plexserver): PMS your connected to. + path (string): 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 + """ items = [] for elem in server.query(path): if libtype and elem.attrib.get('type') != libtype: @@ -347,6 +492,19 @@ def rget(obj, attrstr, default=None, delim='.'): def searchType(libtype): + """Map search type name to int using SEACHTYPES + Used when querying PMS. + + Args: + libtype (string): Possible options see SEARCHTYPES + + Returns: + int: fx 1 + + Raises: + NotFound: Unknown libtype: libtype + """ + libtype = str(libtype) if libtype in [str(v) for v in SEARCHTYPES.values()]: return libtype @@ -356,6 +514,13 @@ 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 + + """ threads, results = [], [] for args in listargs: args += [results, len(results)] @@ -368,7 +533,15 @@ def threaded(callback, listargs): def toDatetime(value, format=None): - """Helper for datetime""" + """Helper for datetime + + Args: + value (string): value to use to make datetime + format (None, optional): string as strptime. + + Returns: + datetime + """ if value and value != NA: if format: value = datetime.strptime(value, format) diff --git a/plexapi/video.py b/plexapi/video.py index 52a0e618..ba93fb9f 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- + """ PlexVideo + +Attributes: + NA (TYPE): Description """ from plexapi import media, utils from plexapi.utils import Playable, PlexPartialObject @@ -11,15 +15,28 @@ class Video(PlexPartialObject): TYPE = None def __init__(self, server, data, initpath): + """ + Args: + server (Plexserver): The PMS server your connected to + data (Element): Element built from server.query + initpath (string): Relativ path fx /library/sections/1/all + + """ super(Video, self).__init__(data, initpath, server) def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ self.listType = 'video' self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.key = data.attrib.get('key', NA) - self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA)) + self.lastViewedAt = utils.toDatetime( + data.attrib.get('lastViewedAt', NA)) self.librarySectionID = data.attrib.get('librarySectionID', NA) - self.ratingKey = data.attrib.get('ratingKey', NA) + self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA)) self.summary = data.attrib.get('summary', NA) self.thumb = data.attrib.get('thumb', NA) self.title = data.attrib.get('title', NA) @@ -33,26 +50,36 @@ class Video(PlexPartialObject): return self.server.url(self.thumb) def analyze(self): - """ The primary purpose of media analysis is to gather information about that media - item. All of the media you add to a Library has properties that are useful to - know–whether it's a video file, a music track, or one of your photos. + """The primary purpose of media analysis is to gather information about + that mediaitem. All of the media you add to a Library has properties + that are useful to know–whether it's a video file, + a music track, or one of your photos. """ self.server.query('/%s/analyze' % self.key) def markWatched(self): + """Mark a items as watched. + """ path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey self.server.query(path) self.reload() def markUnwatched(self): + """Mark a item as unwatched. + """ path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey self.server.query(path) self.reload() def refresh(self): - self.server.query('%s/refresh' % self.key, method=self.server.session.put) - + """Refresh a item. + """ + self.server.query('%s/refresh' % + self.key, method=self.server.session.put) + def section(self): + """Library section. + """ return self.server.library.sectionByID(self.librarySectionID) @@ -61,17 +88,24 @@ class Movie(Video, Playable): TYPE = 'movie' def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ Video._loadData(self, data) Playable._loadData(self, data) self.art = data.attrib.get('art', NA) - self.audienceRating = utils.cast(float, data.attrib.get('audienceRating', NA)) + self.audienceRating = utils.cast( + float, data.attrib.get('audienceRating', NA)) self.audienceRatingImage = data.attrib.get('audienceRatingImage', NA) self.chapterSource = data.attrib.get('chapterSource', NA) self.contentRating = data.attrib.get('contentRating', NA) self.duration = utils.cast(int, data.attrib.get('duration', NA)) self.guid = data.attrib.get('guid', NA) self.originalTitle = data.attrib.get('originalTitle', NA) - self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') + self.originallyAvailableAt = utils.toDatetime( + data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') self.primaryExtraKey = data.attrib.get('primaryExtraKey', NA) self.rating = data.attrib.get('rating', NA) self.ratingImage = data.attrib.get('ratingImage', NA) @@ -80,24 +114,34 @@ class Movie(Video, Playable): self.userRating = utils.cast(float, data.attrib.get('userRating', NA)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year', NA)) - if self.isFullObject(): - self.collections = [media.Collection(self.server, e) for e in data if e.tag == media.Collection.TYPE] - self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE] - self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE] - self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE] - self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE] - self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE] - self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE] - self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE] - self.fields = [media.Field(e) for e in data if e.tag == media.Field.TYPE] + if self.isFullObject(): # check this + self.collections = [media.Collection( + self.server, e) for e in data if e.tag == media.Collection.TYPE] + self.countries = [media.Country(self.server, e) + for e in data if e.tag == media.Country.TYPE] + self.directors = [media.Director( + self.server, e) for e in data if e.tag == media.Director.TYPE] + self.genres = [media.Genre(self.server, e) + for e in data if e.tag == media.Genre.TYPE] + self.media = [media.Media(self.server, e, self.initpath, self) + for e in data if e.tag == media.Media.TYPE] + self.producers = [media.Producer( + self.server, e) for e in data if e.tag == media.Producer.TYPE] + self.roles = [media.Role(self.server, e) + for e in data if e.tag == media.Role.TYPE] + self.writers = [media.Writer(self.server, e) + for e in data if e.tag == media.Writer.TYPE] + self.fields = [media.Field(e) + for e in data if e.tag == media.Field.TYPE] self.videoStreams = utils.findStreams(self.media, 'videostream') self.audioStreams = utils.findStreams(self.media, 'audiostream') - self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') - + self.subtitleStreams = utils.findStreams( + self.media, 'subtitlestream') + @property def actors(self): return self.roles - + @property def isWatched(self): return bool(self.viewCount > 0) @@ -108,6 +152,11 @@ class Show(Video): TYPE = 'show' def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ Video._loadData(self, data) self.art = data.attrib.get('art', NA) self.banner = data.attrib.get('banner', NA) @@ -118,25 +167,31 @@ class Show(Video): self.index = data.attrib.get('index', NA) self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA)) self.location = utils.findLocations(data, single=True) - self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') + self.originallyAvailableAt = utils.toDatetime( + data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') self.rating = utils.cast(float, data.attrib.get('rating', NA)) self.studio = data.attrib.get('studio', NA) self.theme = data.attrib.get('theme', NA) - self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA)) + self.viewedLeafCount = utils.cast( + int, data.attrib.get('viewedLeafCount', NA)) self.year = utils.cast(int, data.attrib.get('year', NA)) if self.isFullObject(): - self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE] - self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE] + self.genres = [media.Genre(self.server, e) + for e in data if e.tag == media.Genre.TYPE] + self.roles = [media.Role(self.server, e) + for e in data if e.tag == media.Role.TYPE] @property def actors(self): return self.roles - + @property def isWatched(self): return bool(self.viewedLeafCount == self.leafCount) def seasons(self): + """Returns a list of Season + """ path = '/library/metadata/%s/children' % self.ratingKey return utils.listItems(self.server, path, Season.TYPE) @@ -153,15 +208,21 @@ class Show(Video): return utils.findItem(self.server, path, title) def watched(self): + """Return a list of watched episodes + """ return self.episodes(watched=True) def unwatched(self): + """Return a list of unwatched episodes + """ return self.episodes(watched=False) def get(self, title): return self.episode(title) def refresh(self): + """Refresh the metadata + """ self.server.query('/library/metadata/%s/refresh' % self.ratingKey) @@ -170,13 +231,19 @@ class Season(Video): TYPE = 'season' def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ Video._loadData(self, data) self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA)) self.index = data.attrib.get('index', NA) self.parentKey = data.attrib.get('parentKey', NA) - self.parentRatingKey = data.attrib.get('parentRatingKey', NA) - self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA)) - + self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA)) + self.viewedLeafCount = utils.cast( + int, data.attrib.get('viewedLeafCount', NA)) + @property def isWatched(self): return bool(self.viewedLeafCount == self.leafCount) @@ -186,10 +253,20 @@ class Season(Video): return self.index def episodes(self, watched=None): + """Return list of Episode + + Args: + watched (None, optional): Description + """ childrenKey = '/library/metadata/%s/children' % self.ratingKey return utils.listItems(self.server, childrenKey, watched=watched) def episode(self, title): + """Return Episode + + Args: + title (TYPE): Description + """ path = '/library/metadata/%s/children' % self.ratingKey return utils.findItem(self.server, path, title) @@ -219,27 +296,31 @@ class Episode(Video, Playable): self.duration = utils.cast(int, data.attrib.get('duration', NA)) self.grandparentArt = data.attrib.get('grandparentArt', NA) self.grandparentKey = data.attrib.get('grandparentKey', NA) - self.grandparentRatingKey = data.attrib.get('grandparentRatingKey', NA) + self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey', NA)) self.grandparentTheme = data.attrib.get('grandparentTheme', NA) self.grandparentThumb = data.attrib.get('grandparentThumb', NA) self.grandparentTitle = data.attrib.get('grandparentTitle', NA) self.guid = data.attrib.get('guid', NA) self.index = data.attrib.get('index', NA) - self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') + self.originallyAvailableAt = utils.toDatetime( + data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') self.parentIndex = data.attrib.get('parentIndex', NA) self.parentKey = data.attrib.get('parentKey', NA) - self.parentRatingKey = data.attrib.get('parentRatingKey', NA) + self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA)) self.parentThumb = data.attrib.get('parentThumb', NA) self.rating = utils.cast(float, data.attrib.get('rating', NA)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year', NA)) - if self.isFullObject(): - self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE] - self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE] - self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE] - self.videoStreams = utils.findStreams(self.media, 'videostream') - self.audioStreams = utils.findStreams(self.media, 'audiostream') - self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') + #if self.isFullObject(): + self.directors = [media.Director(self.server, e) + for e in data if e.tag == media.Director.TYPE] + self.media = [media.Media(self.server, e, self.initpath, self) + for e in data if e.tag == media.Media.TYPE] + self.writers = [media.Writer(self.server, e) + for e in data if e.tag == media.Writer.TYPE] + self.videoStreams = utils.findStreams(self.media, 'videostream') + self.audioStreams = utils.findStreams(self.media, 'audiostream') + self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') # data for active sessions and history self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA)) self.username = utils.findUsername(data)