diff --git a/plexapi/audio.py b/plexapi/audio.py index 629f7282..35775b86 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -1,26 +1,58 @@ # -*- coding: utf-8 -*- -""" -PlexAPI Audio -""" + from plexapi import media, utils from plexapi.utils import Playable, PlexPartialObject + NA = utils.NA class Audio(PlexPartialObject): + """Base class for audio. + + Attributes: + addedAt (int): int from epoch, datetime.datetime + index (sting): 1 + key (str): Fx /library/metadata/102631 + lastViewedAt (datetime.datetime): parse int into datetime.datetime. + librarySectionID (int): + listType (str): audio + ratingKey (int): Unique key to identify this item + summary (str): Summery of the artist, track, album + thumb (str): Url to thumb image + title (str): Fx Aerosmith + titleSort (str): Defaults title if None + TYPE (str): overwritten by subclass + type (string, NA): Description + updatedAt (datatime.datetime): parse int to datetime.datetime + viewCount (int): How many time has this item been played + """ TYPE = None - + def __init__(self, server, data, initpath): + """Used to set the attributes. + + Args: + server (Plexserver): PMS your connected to + data (Element): XML reponse from PMS as Element + normally built from server.query + initpath (str): Fx /library/sections/7/all + """ super(Audio, self).__init__(data, initpath, server) def _loadData(self, data): + """Used to set the attributes. + + Args: + data (Element): XML reponse from PMS as Element + normally built from server.query + """ self.listType = 'audio' self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.index = data.attrib.get('index', NA) self.key = data.attrib.get('key', 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) @@ -28,58 +60,124 @@ class Audio(PlexPartialObject): self.type = data.attrib.get('type', NA) self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA)) self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0)) - + @property def thumbUrl(self): + """Return url to thumb image.""" return self.server.url(self.thumb) - + def refresh(self): + """Refresh the metadata.""" self.server.query('%s/refresh' % self.key, method=self.server.session.put) - + def section(self): + """Library section.""" return self.server.library.sectionByID(self.librarySectionID) @utils.register_libtype class Artist(Audio): + """Artist. + + Attributes: + art (str): /library/metadata/102631/art/1469310342 + countries (list): List of media.County fx [] + genres (list): List of media.Genre fx [] + guid (str): Fx guid com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en + key (str): Fx /library/metadata/102631 + location (str): Filepath + similar (list): List of media.Similar fx [] + TYPE (str): artist + """ + TYPE = 'artist' def _loadData(self, data): + """Used to set the attributes. + + Args: + data (Element): XML reponse from PMS as Element + normally built from server.query + """ Audio._loadData(self, data) self.art = data.attrib.get('art', NA) self.guid = data.attrib.get('guid', NA) self.key = self.key.replace('/children', '') # FIX_BUG_50 self.location = utils.findLocations(data, single=True) - if self.isFullObject(): - self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE] - self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE] - self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE] + if self.isFullObject(): # check if this is needed + self.countries = [media.Country(self.server, e) + for e in data if e.tag == media.Country.TYPE] + self.genres = [media.Genre(self.server, e) + for e in data if e.tag == media.Genre.TYPE] + self.similar = [media.Similar(self.server, e) + for e in data if e.tag == media.Similar.TYPE] def albums(self): + """Return a list of Albums by thus artist.""" path = '%s/children' % self.key return utils.listItems(self.server, path, Album.TYPE) def album(self, title): + """Return a album from this artist that match title.""" path = '%s/children' % self.key return utils.findItem(self.server, path, title) def tracks(self, watched=None): + """Return all tracks to this artist. + + Args: + watched(None, False, True): Default to None. + + Returns: + List: of Track + """ path = '%s/allLeaves' % self.key return utils.listItems(self.server, path, watched=watched) def track(self, title): + """Return a Track that matches title. + + Args: + title (str): Fx song name + + Returns: + Track: + """ path = '%s/allLeaves' % self.key return utils.findItem(self.server, path, title) def get(self, title): + """Alias. See track.""" return self.track(title) @utils.register_libtype class Album(Audio): + """Album. + + Attributes: + art (str): Fx /library/metadata/102631/art/1469310342 + genres (list): List of media.Genre + key (str): Fx /library/metadata/102632 + originallyAvailableAt (TYPE): Description + parentKey (str): /library/metadata/102631 + parentRatingKey (int): Fx 1337 + parentThumb (TYPE): Relative url to parent thumb image + parentTitle (str): Aerosmith + studio (str): + TYPE (str): album + year (int): 1999 + """ + TYPE = 'album' def _loadData(self, data): + """Used to set the attributes. + + Args: + data (Element): XML reponse from PMS as Element + normally built from server.query + """ Audio._loadData(self, data) self.art = data.attrib.get('art', NA) self.key = self.key.replace('/children', '') # FIX_BUG_50 @@ -94,31 +192,87 @@ class Album(Audio): self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE] def tracks(self, watched=None): + """Return all tracks to this album. + + Args: + watched(None, False, True): Default to None. + + Returns: + List: of Track + """ path = '%s/children' % self.key return utils.listItems(self.server, path, watched=watched) def track(self, title): + """Return a Track that matches title. + + Args: + title (str): Fx song name + + Returns: + Track: + """ path = '%s/children' % self.key return utils.findItem(self.server, path, title) def get(self, title): + """Alias. See track.""" return self.track(title) def artist(self): + """Return Artist of this album.""" return utils.listItems(self.server, self.parentKey)[0] def watched(self): + """Return Track that is lisson on.""" return self.tracks(watched=True) def unwatched(self): + """Return Track that is not lisson on.""" return self.tracks(watched=False) @utils.register_libtype class Track(Audio, Playable): + """Track. + + Attributes: + art (str): Relative path fx /library/metadata/102631/art/1469310342 + chapterSource (TYPE): Description + duration (TYPE): Description + grandparentArt (str): Relative path + grandparentKey (str): Relative path Fx /library/metadata/102631 + grandparentRatingKey (TYPE): Description + grandparentThumb (str): Relative path to Artist thumb img + grandparentTitle (str): Aerosmith + guid (TYPE): Description + media (list): List of media.Media + moods (list): List of media.Moods + originalTitle (str): Some track title + parentIndex (int): 1 + parentKey (str): Relative path Fx /library/metadata/102632 + parentRatingKey (int): 1337 + parentThumb (str): Relative path to Album thumb + parentTitle (str): Album title + player (None): #TODO + primaryExtraKey (TYPE): #TODO + ratingCount (int): 10 + sessionKey (int): Description + transcodeSession (None): + TYPE (str): track + username (str): username@mail.com + viewOffset (int): 100 + year (int): 1999 + """ + TYPE = 'track' def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ Audio._loadData(self, data) Playable._loadData(self, data) self.art = data.attrib.get('art', NA) @@ -140,9 +294,11 @@ class Track(Audio, Playable): self.ratingCount = utils.cast(int, data.attrib.get('ratingCount', 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.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.TYPE] - self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE] + if self.isFullObject(): # check me + self.moods = [media.Mood(self.server, e) + for e in data if e.tag == media.Mood.TYPE] + self.media = [media.Media(self.server, e, self.initpath, self) + for e in data if e.tag == media.Media.TYPE] # data for active sessions and history self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA)) self.username = utils.findUsername(data) @@ -151,10 +307,13 @@ class Track(Audio, Playable): @property def thumbUrl(self): + """Return url to thumb image.""" return self.server.url(self.parentThumb) def album(self): + """Return this track's Album.""" return utils.listItems(self.server, self.parentKey)[0] def artist(self): + """Return this track's Artist.""" return utils.listItems(self.server, self.grandparentKey)[0] diff --git a/plexapi/client.py b/plexapi/client.py index 7cd5533a..698020b3 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -13,27 +13,27 @@ from xml.etree import ElementTree class PlexClient(object): - """Main class for interacting with a client + """Main class for interacting with a client. Attributes: - baseurl (string): http adress for the client + baseurl (str): http adress for the client device (None): Description deviceClass (sting): pc, phone - machineIdentifier (string): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3 + machineIdentifier (str): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3 model (TYPE): Description platform (TYPE): Description platformVersion (TYPE): Description - product (string): plex for ios - protocol (string): plex + product (str): plex for ios + protocol (str): plex protocolCapabilities (list): List of what client can do - protocolVersion (string): 1 + protocolVersion (str): 1 server (plexapi.server.Plexserver): PMS your connected to session (None or requests.Session): Add your own session object to cache stuff state (None): Description - title (string): fx Johns Iphone - token (string): X-Plex-Token, using for authenication with PMS - vendor (string): Description - version (string): fx. 4.6 + title (str): fx Johns Iphone + token (str): X-Plex-Token, using for authenication with PMS + vendor (str): Description + version (str): fx. 4.6 """ def __init__(self, baseurl, token=None, session=None, server=None, data=None): @@ -115,7 +115,7 @@ class PlexClient(object): """Used to fetch relative paths to pms. Args: - path (string): Relative path + path (str): Relative path method (None, optional): requests.post etc headers (None, optional): Set headers manually **kwargs (TYPE): Passord to the http request used for filter, sorting. @@ -141,7 +141,7 @@ class PlexClient(object): """Send a command to the client Args: - command (string): See the commands listed below + command (str): See the commands listed below proxy (None, optional): Description **params (dict): Description @@ -170,7 +170,7 @@ class PlexClient(object): """Return a full url Args: - path (string): Relative path + path (str): Relative path Returns: string: full path to PMS @@ -234,7 +234,7 @@ class PlexClient(object): """Go to a media on the client. Args: - media (string): movie, music, photo + media (str): movie, music, photo **params (TYPE): Description # todo Raises: @@ -261,7 +261,7 @@ class PlexClient(object): """Pause playback Args: - mtype (string): music, photo, video + mtype (str): music, photo, video """ self.sendCommand('playback/pause', type=mtype) @@ -269,7 +269,7 @@ class PlexClient(object): """Start playback Args: - mtype (string): music, photo, video + mtype (str): music, photo, video """ self.sendCommand('playback/play', type=mtype) @@ -346,7 +346,7 @@ class PlexClient(object): """Stop playback Args: - mtype (string): video, music, photo + mtype (str): video, music, photo """ self.sendCommand('playback/stop', type=mtype) @@ -383,7 +383,7 @@ class PlexClient(object): Args: audioStreamID (TYPE): Description - mtype (string): video, music, photo + mtype (str): video, music, photo """ self.setStreams(audioStreamID=audioStreamID, mtype=mtype) @@ -392,7 +392,7 @@ class PlexClient(object): Args: subtitleStreamID (TYPE): Description - mtype (string): video, music, photo + mtype (str): video, music, photo """ self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype) @@ -401,7 +401,7 @@ class PlexClient(object): Args: videoStreamID (TYPE): Description - mtype (string): video, music, photo + mtype (str): video, music, photo """ self.setStreams(videoStreamID=videoStreamID, mtype=mtype) @@ -410,7 +410,7 @@ class PlexClient(object): """Start playback on a media item. Args: - media (string): movie, music, photo + media (str): movie, music, photo **params (TYPE): Description Raises: diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 6aac18f9..a44e6f9a 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -1,22 +1,68 @@ # -*- coding: utf-8 -*- -""" -PlexAPI MyPlex -""" -import plexapi, requests +import sys + +if sys.version_info <= (3, 3): + try: + from xml.etree import cElementTree as ElementTree + except ImportError: + from xml.etree import ElementTree +else: + # py 3.3 and above selects the fastest automatically + from xml.etree import ElementTree + +from requests.status_codes import _codes as codes + +import plexapi +import requests from plexapi import TIMEOUT, log, utils from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.client import PlexClient from plexapi.server import PlexServer -from requests.status_codes import _codes as codes -from xml.etree import ElementTree -# Your personal MyPlex account and profile information class MyPlexAccount(object): + """Your personal MyPlex account and profile information + + Attributes: + authenticationToken (TYPE): Description + BASEURL (str): Description + certificateVersion (TYPE): Description + cloudSyncDevice (TYPE): Description + email (TYPE): Description + entitlements (TYPE): Description + guest (TYPE): Description + home (TYPE): Description + homeSize (TYPE): Description + id (TYPE): Description + locale (TYPE): Description + mailing_list_status (TYPE): Description + maxHomeSize (TYPE): Description + queueEmail (TYPE): Description + queueUid (TYPE): Description + restricted (TYPE): Description + roles (TYPE): Description + scrobbleTypes (TYPE): Description + secure (TYPE): Description + SIGNIN (str): Description + subscriptionActive (TYPE): Description + subscriptionFeatures (TYPE): Description + subscriptionPlan (TYPE): Description + subscriptionStatus (TYPE): Description + thumb (TYPE): Description + title (TYPE): Description + username (TYPE): Description + uuid (TYPE): Description + """ BASEURL = 'https://plex.tv/users/account' SIGNIN = 'https://my.plexapp.com/users/sign_in.xml' - + def __init__(self, data, initpath=None): + """Sets the attrs. + + Args: + data (Element): XML response from PMS as a Element + initpath (string, optional): relative path. + """ self.authenticationToken = data.attrib.get('authenticationToken') self.certificateVersion = data.attrib.get('certificateVersion') self.cloudSyncDevice = data.attrib.get('cloudSyncDevice') @@ -37,7 +83,7 @@ class MyPlexAccount(object): self.title = data.attrib.get('title') self.username = data.attrib.get('username') self.uuid = data.attrib.get('uuid') - + # TODO: Complete these items! self.subscriptionActive = None # renamed on server self.subscriptionStatus = None # renamed on server @@ -45,51 +91,135 @@ class MyPlexAccount(object): self.subscriptionFeatures = None # renamed on server self.roles = None self.entitlements = None - + def __repr__(self): + """Pretty print.""" return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8')) def devices(self): + """Return a all devices connected to the plex account. + + Returns: + list: of MyPlexDevice + """ return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice) - + def device(self, name): + """Return a device wth a matching name. + + Args: + name (str): Name to match against. + + Returns: + class: MyPlexDevice + """ return _findItem(self.devices(), name) def resources(self): + """Resources. + + Returns: + List: of MyPlexResource + """ return _listItems(MyPlexResource.BASEURL, self.authenticationToken, MyPlexResource) def resource(self, name): + """Find resource ny name. + + Args: + name (str): to find + + Returns: + class: MyPlexResource + """ return _findItem(self.resources(), name) - + def users(self): + """List of users. + + Returns: + List: of MyPlexuser + """ return _listItems(MyPlexUser.BASEURL, self.authenticationToken, MyPlexUser) - + def user(self, email): + """Find a user by email. + + Args: + email (str): Username to match against. + + Returns: + class: User + """ return _findItem(self.users(), email, ['username', 'email']) @classmethod def signin(cls, username, password): + """Summary + + Args: + username (str): username + password (str): password + + Returns: + class: MyPlexAccount + + Raises: + BadRequest: (HTTPCODE) http codename + Unauthorized: (HTTPCODE) http codename + """ if 'X-Plex-Token' in plexapi.BASE_HEADERS: del plexapi.BASE_HEADERS['X-Plex-Token'] auth = (username, password) log.info('POST %s', cls.SIGNIN) - response = requests.post(cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) + response = requests.post( + cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) if response.status_code != requests.codes.created: codename = codes.get(response.status_code)[0] if response.status_code == 401: - raise Unauthorized('(%s) %s' % (response.status_code, codename)) + raise Unauthorized('(%s) %s' % + (response.status_code, codename)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = ElementTree.fromstring(response.text.encode('utf8')) return cls(data, cls.SIGNIN) -# Not to be confused with the MyPlexAccount, this represents +# Not to be confused with the MyPlexAccount, this represents # non-signed in users such as friends and linked accounts. class MyPlexUser(object): + """Class to other users. + + Attributes: + allowCameraUpload (bool): True if this user can upload images + allowChannels (bool): True if this user has access to channels + allowSync (bool): True if this user can sync + BASEURL (str): Description + email (str): user@gmail.com + filterAll (str): Description + filterMovies (str): Description + filterMusic (str): Description + filterPhotos (str): Description + filterTelevision (str): Description + home (bool): + id (int): 1337 + protected (False): Is this if ssl? check it + recommendationsPlaylistId (str): Description + restricted (str): fx 0 + thumb (str): Link to the users avatar + title (str): Hellowlol + username (str): Hellowlol + """ BASEURL = 'https://plex.tv/api/users/' - + def __init__(self, data, initpath=None): - self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload')) + """Summary + + Args: + data (Element): XML repsonse as Element + initpath (None, optional): Relative url str + """ + self.allowCameraUpload = utils.cast( + bool, data.attrib.get('allowCameraUpload')) self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels')) self.allowSync = utils.cast(bool, data.attrib.get('allowSync')) self.email = data.attrib.get('email') @@ -101,20 +231,48 @@ class MyPlexUser(object): self.home = utils.cast(bool, data.attrib.get('home')) self.id = utils.cast(int, data.attrib.get('id')) self.protected = utils.cast(bool, data.attrib.get('protected')) - self.recommendationsPlaylistId = data.attrib.get('recommendationsPlaylistId') + self.recommendationsPlaylistId = data.attrib.get( + 'recommendationsPlaylistId') self.restricted = data.attrib.get('restricted') self.thumb = data.attrib.get('thumb') self.title = data.attrib.get('title') self.username = data.attrib.get('username') - + def __repr__(self): + """Pretty repr.""" return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username) - + class MyPlexResource(object): + """Summary + + Attributes: + accessToken (str): This resource accesstoken. + BASEURL (TYPE): Description + clientIdentifier (str): 1f2fe128794fd... + connections (list): of ResourceConnection + createdAt (datetime): Description + device (str): pc + home (None): Dunno wtf this can me + lastSeenAt (datetime): Description + name (str): Pretty name fx S-PC + owned (bool): True if this is your own. + platform (str): Windows + platformVersion (str): fx. 6.1 (Build 7601) + presence (bool): True if online + product (str): Plex Media Server + productVersion (str): 1.3.3.3148-b38628e + provides (str): fx server + synced (bool): Description + """ BASEURL = 'https://plex.tv/api/resources?includeHttps=1' def __init__(self, data): + """Summary + + Args: + data (Element): XML response as Element + """ self.name = data.attrib.get('name') self.accessToken = data.attrib.get('accessToken') self.product = data.attrib.get('product') @@ -130,16 +288,31 @@ class MyPlexResource(object): self.home = utils.cast(bool, data.attrib.get('home')) self.synced = utils.cast(bool, data.attrib.get('synced')) self.presence = utils.cast(bool, data.attrib.get('presence')) - self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection'] + self.connections = [ResourceConnection( + elem) for elem in data if elem.tag == 'Connection'] def __repr__(self): + """Pretty repr.""" return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8')) def connect(self, ssl=None): + """Connect. + + Args: + ssl (None, optional): Use ssl. + + Returns: + class: Plexserver + + Raises: + NotFound: Unable to connect to resource: name + """ + # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource forcelocal = lambda c: self.owned or c.local - connections = sorted(self.connections, key=lambda c:c.local, reverse=True) + connections = sorted( + self.connections, key=lambda c: c.local, reverse=True) https = [c.uri for c in self.connections if forcelocal(c)] http = [c.httpuri for c in self.connections if forcelocal(c)] connections = https + http @@ -148,26 +321,56 @@ class MyPlexResource(object): listargs = [[c] for c in connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) - # or (url, token, None) in the case a connection could not be established. + # or (url, token, None) in the case a connection could not be + # established. for url, token, result in results: okerr = 'OK' if result else 'ERR' - log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr) - results = list(filter(None, [r[2] for r in results if r])) + log.info( + 'Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr) + + results = [r[2] for r in results if r and r is not None] if not results: raise NotFound('Unable to connect to resource: %s' % self.name) - log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) + log.info('Connecting to server: %s?X-Plex-Token=%s', + results[0].baseurl, results[0].token) + return results[0] def _connect(self, url, results, i): + """Connect. + + Args: + url (str): url to the resource + results (TYPE): Description + i (TYPE): Description + + Returns: + TYPE: Description + """ try: - results[i] = (url, self.accessToken, PlexServer(url, self.accessToken)) + results[i] = (url, self.accessToken, + PlexServer(url, self.accessToken)) except NotFound: results[i] = (url, self.accessToken, None) class ResourceConnection(object): + """ResourceConnection. + Attributes: + address (str): Local ip adress + httpuri (str): Full local address + local (bool): True if local + port (int): 32400 + protocol (str): http or https + uri (str): External adress + """ def __init__(self, data): + """Set attrs. + + Args: + data (Element): XML response as Element from PMS. + """ self.protocol = data.attrib.get('protocol') self.address = data.attrib.get('address') self.port = utils.cast(int, data.attrib.get('port')) @@ -176,13 +379,42 @@ class ResourceConnection(object): self.httpuri = 'http://%s:%s' % (self.address, self.port) def __repr__(self): + """Pretty repr.""" return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8')) class MyPlexDevice(object): + """Device connected. + + Attributes: + BASEURL (str): Plex.tv XML device url + clientIdentifier (str): 0x685d43d... + connections (list): + device (str): fx Windows + id (str): 123 + model (str): + name (str): fx Computername + platform (str): Windows + platformVersion (str): Fx 8 + product (str): Fx PlexAPI + productVersion (string): 2.0.2 + provides (str): fx controller + publicAddress (str): Public ip address + screenDensity (str): Description + screenResolution (str): Description + token (str): Auth token + vendor (str): Description + version (str): fx 2.0.2 + """ + BASEURL = 'https://plex.tv/devices.xml' def __init__(self, data): + """Set attrs + + Args: + data (Element): XML response as Element from PMS + """ self.name = data.attrib.get('name') self.publicAddress = data.attrib.get('publicAddress') self.product = data.attrib.get('product') @@ -199,36 +431,75 @@ class MyPlexDevice(object): self.token = data.attrib.get('token') self.screenResolution = data.attrib.get('screenResolution') self.screenDensity = data.attrib.get('screenDensity') - self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')] + self.connections = [connection.attrib.get( + 'uri') for connection in data.iter('Connection')] def __repr__(self): + """Pretty repr.""" return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8')) def connect(self, ssl=None): + """Connect to the first server. + + Args: + ssl (None, optional): Use SSL? + + Returns: + TYPE: Plexserver + + Raises: + NotFound: Unable to connect to resource: name + """ # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in self.connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) - # or (url, token, None) in the case a connection could not be established. + # or (url, token, None) in the case a connection could not be + # established. for url, token, result in results: okerr = 'OK' if result else 'ERR' - log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr) - results = list(filter(None, [r[2] for r in results if r])) + log.info('Testing device connection: %s?X-Plex-Token=%s %s', + url, token, okerr) + results = [r[2] for r in results if r and r[2] is not None] if not results: raise NotFound('Unable to connect to resource: %s' % self.name) - log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) + log.info('Connecting to server: %s?X-Plex-Token=%s', + results[0].baseurl, results[0].token) + return results[0] def _connect(self, url, results, i): + """Summary + + Args: + url (TYPE): Description + results (TYPE): Description + i (TYPE): Description + + Returns: + TYPE: Description + """ try: results[i] = (url, self.token, PlexClient(url, self.token)) except NotFound as err: - print(err) results[i] = (url, self.token, None) def _findItem(items, value, attrs=None): + """Simple helper to find something using attrs + + Args: + items (cls): list of Object to get the attrs from + value (str): value to match against + attrs (None, optional): attr to match against value. + + Returns: + TYPE: Description + + Raises: + NotFound: Description + """ attrs = attrs or ['name'] for item in items: for attr in attrs: @@ -238,6 +509,16 @@ def _findItem(items, value, attrs=None): def _listItems(url, token, cls): + """Helper that builds list of classes from a XML response. + + Args: + url (str): Description + token (str): Description + cls (class): Class to initate + + Returns: + List: of classes + """ headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', url, token) diff --git a/plexapi/photo.py b/plexapi/photo.py index 7e24f347..cb387ee4 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- """ PlexPhoto + +Attributes: + NA (TYPE): Description """ from plexapi import media, utils from plexapi.utils import PlexPartialObject @@ -9,12 +12,46 @@ NA = utils.NA @utils.register_libtype class Photoalbum(PlexPartialObject): + """Summary + + Attributes: + addedAt (TYPE): Description + art (TYPE): Description + composite (TYPE): Description + guid (TYPE): Description + index (TYPE): Description + key (TYPE): Description + librarySectionID (TYPE): Description + listType (str): Description + ratingKey (TYPE): Description + summary (TYPE): Description + thumb (TYPE): Description + title (TYPE): Description + TYPE (str): Description + type (TYPE): Description + updatedAt (TYPE): Description + """ TYPE = 'photoalbum' def __init__(self, server, data, initpath): + """Summary + + Args: + server (TYPE): Description + data (TYPE): Description + initpath (TYPE): Description + """ super(Photoalbum, self).__init__(data, initpath, server) def _loadData(self, data): + """Summary + + Args: + data (TYPE): Description + + Returns: + TYPE: Description + """ self.listType = 'photo' self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.art = data.attrib.get('art', NA) @@ -29,32 +66,86 @@ class Photoalbum(PlexPartialObject): self.title = data.attrib.get('title', NA) self.type = data.attrib.get('type', NA) self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA)) - + def photos(self): + """Summary + + Returns: + TYPE: Description + """ path = '/library/metadata/%s/children' % self.ratingKey return utils.listItems(self.server, path, Photo.TYPE) def photo(self, title): + """Summary + + Args: + title (TYPE): Description + + Returns: + TYPE: Description + """ path = '/library/metadata/%s/children' % self.ratingKey return utils.findItem(self.server, path, title) - + def section(self): + """Summary + + Returns: + TYPE: Description + """ return self.server.library.sectionByID(self.librarySectionID) @utils.register_libtype class Photo(PlexPartialObject): + """Summary + + Attributes: + addedAt (TYPE): Description + index (TYPE): Description + key (TYPE): Description + listType (str): Description + media (TYPE): Description + originallyAvailableAt (TYPE): Description + parentKey (TYPE): Description + parentRatingKey (TYPE): Description + ratingKey (TYPE): Description + summary (TYPE): Description + thumb (TYPE): Description + title (TYPE): Description + TYPE (str): Description + type (TYPE): Description + updatedAt (TYPE): Description + year (TYPE): Description + """ TYPE = 'photo' def __init__(self, server, data, initpath): + """Summary + + Args: + server (TYPE): Description + data (TYPE): Description + initpath (TYPE): Description + """ super(Photo, self).__init__(data, initpath, server) def _loadData(self, data): + """Summary + + Args: + data (TYPE): Description + + Returns: + TYPE: Description + """ self.listType = 'photo' self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.index = utils.cast(int, data.attrib.get('index', NA)) self.key = data.attrib.get('key', 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.parentKey = data.attrib.get('parentKey', NA) self.parentRatingKey = data.attrib.get('parentRatingKey', NA) self.ratingKey = data.attrib.get('ratingKey', NA) @@ -65,10 +156,21 @@ class Photo(PlexPartialObject): self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA)) self.year = utils.cast(int, data.attrib.get('year', NA)) if self.isFullObject(): - self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE] - + self.media = [media.Media(self.server, e, self.initpath, self) + for e in data if e.tag == media.Media.TYPE] + def photoalbum(self): + """Summary + + Returns: + TYPE: Description + """ return utils.listItems(self.server, self.parentKey)[0] def section(self): + """Summary + + Returns: + TYPE: Description + """ return self.server.library.sectionByID(self.photoalbum().librarySectionID) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index f63fe3c4..36fa5ddf 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -14,9 +14,22 @@ class Playlist(PlexPartialObject, Playable): TYPE = 'playlist' def __init__(self, server, data, initpath): + """Playlist stuff. + + Args: + server (Plexserver): The PMS server your connected to + data (Element): Element built from server.query + initpath (str): Relativ path fx /library/sections/1/all + + """ super(Playlist, self).__init__(data, initpath, server) def _loadData(self, data): + """Used to set the attributes + + Args: + data (Element): Usually built from server.query + """ Playable._loadData(self, data) self.addedAt = toDatetime(data.attrib.get('addedAt', NA)) self.composite = data.attrib.get('composite', NA) # url to thumbnail @@ -27,7 +40,7 @@ class Playlist(PlexPartialObject, Playable): self.key = self.key.replace('/items', '') if self.key else self.key # FIX_BUG_50 self.leafCount = cast(int, data.attrib.get('leafCount', NA)) self.playlistType = data.attrib.get('playlistType', NA) - self.ratingKey = data.attrib.get('ratingKey', NA) + self.ratingKey = cast(int, data.attrib.get('ratingKey', NA)) self.smart = cast(bool, data.attrib.get('smart', NA)) self.summary = data.attrib.get('summary', NA) self.title = data.attrib.get('title', NA) @@ -35,10 +48,12 @@ class Playlist(PlexPartialObject, Playable): self.updatedAt = toDatetime(data.attrib.get('updatedAt', NA)) def items(self): + """Return all items in the playlist.""" path = '%s/items' % self.key return utils.listItems(self.server, path) def addItems(self, items): + """Add items to a playlist.""" if not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] @@ -54,23 +69,29 @@ class Playlist(PlexPartialObject, Playable): return self.server.query(path, method=self.server.session.put) def removeItem(self, item): + """Remove a file from a playlist.""" path = '%s/items/%s' % (self.key, item.playlistItemID) return self.server.query(path, method=self.server.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) - if after: path += '?after=%s' % after.playlistItemID + if after: + path += '?after=%s' % after.playlistItemID return self.server.query(path, method=self.server.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.server.query(path, method=self.server.session.put) def delete(self): + """Delete playlist.""" return self.server.query(self.key, method=self.server.session.delete) @classmethod def create(cls, server, title, items): + """Create a playlist.""" if not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] diff --git a/plexapi/playqueue.py b/plexapi/playqueue.py index bf0ec5fe..c238e31b 100644 --- a/plexapi/playqueue.py +++ b/plexapi/playqueue.py @@ -1,28 +1,64 @@ # -*- coding: utf-8 -*- -""" -PlexAPI Play PlayQueues -""" -import plexapi, requests + + +import plexapi +import requests from plexapi import utils class PlayQueue(object): + """Summary + Attributes: + identifier (TYPE): Description + initpath (TYPE): Description + items (TYPE): Description + mediaTagPrefix (TYPE): Description + mediaTagVersion (TYPE): Description + playQueueID (TYPE): Description + playQueueSelectedItemID (TYPE): Description + playQueueSelectedItemOffset (TYPE): Description + playQueueTotalCount (TYPE): Description + playQueueVersion (TYPE): Description + server (TYPE): Description + """ def __init__(self, server, data, initpath): + """Summary + + Args: + server (TYPE): Description + data (TYPE): Description + initpath (TYPE): Description + """ self.server = server self.initpath = initpath self.identifier = data.attrib.get('identifier') self.mediaTagPrefix = data.attrib.get('mediaTagPrefix') self.mediaTagVersion = data.attrib.get('mediaTagVersion') self.playQueueID = data.attrib.get('playQueueID') - self.playQueueSelectedItemID = data.attrib.get('playQueueSelectedItemID') - self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset') + self.playQueueSelectedItemID = data.attrib.get( + 'playQueueSelectedItemID') + self.playQueueSelectedItemOffset = data.attrib.get( + 'playQueueSelectedItemOffset') self.playQueueTotalCount = data.attrib.get('playQueueTotalCount') self.playQueueVersion = data.attrib.get('playQueueVersion') self.items = [utils.buildItem(server, elem, initpath) for elem in data] @classmethod def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1): + """Summary + + Args: + server (TYPE): Description + item (TYPE): Description + shuffle (int, optional): Description + repeat (int, optional): Description + includeChapters (int, optional): Description + includeRelated (int, optional): Description + + Returns: + TYPE: Description + """ args = {} args['includeChapters'] = includeChapters args['includeRelated'] = includeRelated diff --git a/plexapi/server.py b/plexapi/server.py index dbf46296..a9389417 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +import sys -""" -PlexServer -""" - -from xml.etree import ElementTree +if sys.version_info <= (3, 3): + try: + from xml.etree import cElementTree as ElementTree + except ImportError: + from xml.etree import ElementTree +else: + # py 3.3 and above selects the fastest automatically + from xml.etree import ElementTree import requests from requests.status_codes import _codes as codes @@ -32,29 +36,29 @@ class PlexServer(object): See test/example.py for more examples Attributes: - baseurl (string): Base url for PMS - friendlyName (string): Pretty name for PMS - machineIdentifier (string): uuid for PMS - myPlex (TYPE): Description - myPlexMappingState (TYPE): Description - myPlexSigninState (TYPE): Description - myPlexSubscription (TYPE): Description - myPlexUsername (string): Description - platform (string): Description - platformVersion (string): Description + baseurl (str): Base url for PMS. Fx http://10.0.0.97:32400 + friendlyName (str): Pretty name for PMS fx s-PC + machineIdentifier (str): uuid for PMS + myPlex (bool): Description + myPlexMappingState (str): fx mapped + myPlexSigninState (str): fx ok + myPlexSubscription (str): 1 + myPlexUsername (str): username@email.com + platform (str): The platform PMS is running on. + platformVersion (str): fx 6.1 (Build 7601) session (requests.Session, optinal): Add your own session object for caching - token (string): X-Plex-Token, using for authenication with PMS + token (str): X-Plex-Token, using for authenication with PMS transcoderActiveVideoSessions (int): How any active video sessions - updatedAt (int): Last updated at - version (TYPE): Description + updatedAt (int): Last updated at as epoch + version (str): fx 1.3.2.3112-1751929 """ def __init__(self, baseurl='http://localhost:32400', token=None, session=None): """ Args: - baseurl (string): Base url for PMS - token (string): X-Plex-Token, using for authenication with PMS + baseurl (str): Base url for PMS + token (str): X-Plex-Token, using for authenication with PMS session (requests.Session, optional): Use your own session object if you want to cache the http responses from PMS """ @@ -96,11 +100,12 @@ class PlexServer(object): return self._library def account(self): + """Returns Account.""" data = self.query('/myplex/account') return Account(self, data) def clients(self): - """Query PMS for all clients connected to PMS + """Query PMS for all clients connected to PMS. Returns: list: of Plexclient connnected to PMS @@ -114,13 +119,13 @@ class PlexServer(object): return items def client(self, name): - """Querys PMS for all clients connected to PMS + """Querys PMS for all clients connected to PMS. Returns: Plexclient Args: - name (string): client title, John's Iphone + name (str): client title, John's Iphone Raises: NotFound: Unknown client name Betty @@ -134,7 +139,7 @@ class PlexServer(object): raise NotFound('Unknown client name: %s' % name) def createPlaylist(self, title, items): - """Create a playlist + """Create a playlist. Returns: Playlist @@ -165,10 +170,10 @@ class PlexServer(object): """Returns a playlist with a given name or raise NotFound. Args: - title (string): title of the playlist + title (str): title of the playlist Raises: - NotFound: Description + NotFound: Invalid playlist title: title """ for item in self.playlists(): if item.title == title: @@ -178,16 +183,16 @@ class PlexServer(object): def query(self, path, method=None, headers=None, **kwargs): """Main method used to handle http connection to PMS. encodes the response to utf-8 and parses the xml returned - from PMS + from PMS into a Element Args: - path (sting): relative path to PMS, fx /search?query=HELLO - method (None, optional): requests.method, fx requests.put + path (str): relative path to PMS, fx /search?query=HELLO + method (None, optional): requests.method, requests.put headers (None, optional): Headers that will be passed to PMS **kwargs (dict): Used for filter and sorting. Raises: - BadRequest: Description + BadRequest: fx (404) Not Found Returns: xml.etree.ElementTree.Element or None @@ -209,8 +214,8 @@ class PlexServer(object): """Searching within a library section is much more powerful. Args: - query (string): Search string - mediatype (string, optional): Limit your search to a media type. + query (str): Search str + mediatype (str, optional): Limit your search to a media type. Returns: List @@ -233,30 +238,34 @@ class PlexServer(object): class Account(object): - """This is the locally cached MyPlex account information. The properties provided don't match - the myplex.MyPlexAccount object very well. I believe this is here because access to myplex - is not required to get basic plex information. + """This is the locally cached MyPlex account information. + The properties provided don't matchthe myplex.MyPlexAccount object very well. + I believe this is here because access to myplexis not required + to get basic plex information. Attributes: authToken (sting): X-Plex-Token, using for authenication with PMS - mappingError (TYPE): Description - mappingErrorMessage (TYPE): Description + mappingError (str): + mappingErrorMessage (None, str): Description mappingState (TYPE): Description - privateAddress (TYPE): Description - privatePort (TYPE): Description - publicAddress (TYPE): Description - publicPort (TYPE): Description - signInState (TYPE): Description - subscriptionActive (TYPE): Description - subscriptionFeatures (TYPE): Description - subscriptionState (TYPE): Description - username (TYPE): Description + privateAddress (str): Local ip + privatePort (str): Local port + publicAddress (str): Public ip + publicPort (str): Public port + signInState (str): ok + subscriptionActive (str): is returned as it + subscriptionFeatures (str): What feature your account has access to. + Fx: camera_upload,cloudsync,content_filter + subscriptionState (str): Active + username (str): You username """ def __init__(self, server, data): - """Args: - server (Plexclient): - data (xml.etree.ElementTree.Element): used to set the class attributes. + """Set attrs. + + 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') diff --git a/plexapi/sync.py b/plexapi/sync.py index e9142e2f..5d615292 100644 --- a/plexapi/sync.py +++ b/plexapi/sync.py @@ -1,15 +1,35 @@ # -*- coding: utf-8 -*- -""" -PlexAPI Sync -""" + import requests from plexapi import utils from plexapi.exceptions import NotFound class SyncItem(object): + """Summary + Attributes: + device (TYPE): Description + id (TYPE): Description + location (TYPE): Description + machineIdentifier (TYPE): Description + MediaSettings (TYPE): Description + metadataType (TYPE): Description + policy (TYPE): Description + rootTitle (TYPE): Description + servers (TYPE): Description + status (TYPE): Description + title (TYPE): Description + version (TYPE): Description + """ def __init__(self, device, data, servers=None): + """Summary + + Args: + device (TYPE): Description + data (TYPE): Description + servers (None, optional): Description + """ self.device = device self.servers = servers self.id = utils.cast(int, data.attrib.get('id')) @@ -24,20 +44,49 @@ class SyncItem(object): self.location = data.find('Location').attrib.copy() def __repr__(self): + """Summary + + Returns: + TYPE: Description + """ return '<%s:%s>' % (self.__class__.__name__, self.id) def server(self): - server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self.servers)) + """Summary + + Returns: + TYPE: Description + + Raises: + NotFound: Description + """ + server = list(filter(lambda x: x.machineIdentifier == + self.machineIdentifier, self.servers)) if 0 == len(server): - raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier) + raise NotFound('Unable to find server with uuid %s' % + self.machineIdentifier) return server[0] def getMedia(self): + """Summary + + Returns: + TYPE: Description + """ server = self.server().connect() items = utils.listItems(server, '/sync/items/%s' % self.id) return items def markAsDone(self, sync_id): + """Summary + + Args: + sync_id (TYPE): Description + + Returns: + TYPE: Description + """ server = self.server().connect() - url = '/sync/%s/%s/files/%s/downloaded' % (self.device.clientIdentifier, server.machineIdentifier, sync_id) + url = '/sync/%s/%s/files/%s/downloaded' % ( + self.device.clientIdentifier, server.machineIdentifier, sync_id) server.query(url, method=requests.put) diff --git a/plexapi/utils.py b/plexapi/utils.py index 000f27b7..efdceb40 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -1,12 +1,5 @@ # -*- coding: utf-8 -*- -""" -PlexAPI Utils -Attributes: - LIBRARY_TYPES (dict): Description - NA (TYPE): Description - SEARCHTYPES (TYPE): Description -""" import re from datetime import datetime from plexapi.compat import quote, urlencode @@ -39,38 +32,29 @@ class _NA(object): """ def __bool__(self): - """Summary + """Make sure Na always is False. Returns: - TYPE: Description + bool: False """ return False def __eq__(self, other): - """Summary + """Check eq. Args: - other (TYPE): Description + other (str): Description Returns: - TYPE: Description + bool: True is equal """ 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 - """ + """Pretty print.""" return '__NA__' NA = _NA() @@ -83,15 +67,15 @@ class PlexPartialObject(object): automatically and update itself. Attributes: - initpath (TYPE): Description - server (TYPE): Description + initpath (str): Relative url to PMS + server (): Description """ def __init__(self, data, initpath, server=None): """ Args: data (xml.etree.ElementTree.Element): passed from server.query - initpath (string): Relative path + initpath (str): Relative path server (None or Plexserver, optional): PMS class your connected to """ self.server = server @@ -120,7 +104,7 @@ class PlexPartialObject(object): """Auto reload self, if the attribute is NA Args: - attr (string): fx key + attr (str): fx key """ if attr == 'key' or self.__dict__.get(attr) or self.isFullObject(): return self.__dict__.get(attr, NA) @@ -131,7 +115,7 @@ class PlexPartialObject(object): """Set attribute Args: - attr (string): fx key + attr (str): fx key value (TYPE): Description """ if value != NA or self.isFullObject(): @@ -164,12 +148,12 @@ class Playable(object): Artists, Albums which are all not playable. Attributes: # todo - player (TYPE): Description - playlistItemID (TYPE): Description - sessionKey (TYPE): Description - transcodeSession (TYPE): Description - username (TYPE): Description - viewedAt (datetime): Description + player (Plexclient): Player + playlistItemID (int): Playlist item id + sessionKey (int): 1223 + transcodeSession (str): 12312312 + username (str): Fx Hellowlol + viewedAt (datetime): viewed at. """ def _loadData(self, data): @@ -241,7 +225,7 @@ def buildItem(server, elem, initpath, bytag=False): Args: server (Plexserver): Your connected to. elem (xml.etree.ElementTree.Element): xml from PMS - initpath (string): Relative path + initpath (str): Relative path bytag (bool, optional): Description # figure out what this do Raises: @@ -300,8 +284,8 @@ def findItem(server, path, title): Args: server (Plexserver): Description - path (string): Relative path - title (string): Fx 16 blocks + path (str): Relative path + title (str): Fx 16 blocks Raises: NotFound: Unable to find item: title @@ -355,7 +339,7 @@ def findStreams(media, streamtype): Args: media (Show, Movie, Episode): A item where find streams - streamtype (string): Possible options [movie, show, episode] # is this correct? + streamtype (str): Possible options [movie, show, episode] # is this correct? Returns: list: of streams @@ -402,10 +386,10 @@ def findUsername(data): return None -def isInt(string): +def isInt(str): """Check of a string is a int""" try: - int(string) + int(str) return True except ValueError: return False @@ -435,7 +419,7 @@ def listChoices(server, path): Args: server (Plexserver): Server your connected to - path (string): Relative path to PMS + path (str): Relative path to PMS Returns: dict: title:key @@ -448,7 +432,7 @@ def listItems(server, path, libtype=None, watched=None, bytag=False): Args: server (Plexserver): PMS your connected to. - path (string): Relative path to PMS + 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 @@ -496,7 +480,7 @@ def searchType(libtype): Used when querying PMS. Args: - libtype (string): Possible options see SEARCHTYPES + libtype (str): Possible options see SEARCHTYPES Returns: int: fx 1 @@ -533,10 +517,10 @@ def threaded(callback, listargs): def toDatetime(value, format=None): - """Helper for datetime + """Helper for datetime. Args: - value (string): value to use to make datetime + value (str): value to use to make datetime format (None, optional): string as strptime. Returns: diff --git a/plexapi/video.py b/plexapi/video.py index ba93fb9f..57528236 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -1,13 +1,8 @@ # -*- coding: utf-8 -*- -""" -PlexVideo - -Attributes: - NA (TYPE): Description -""" from plexapi import media, utils from plexapi.utils import Playable, PlexPartialObject + NA = utils.NA @@ -15,18 +10,19 @@ class Video(PlexPartialObject): TYPE = None def __init__(self, server, data, initpath): - """ + """Default class for all video types. + 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 - + initpath (str): 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 """ @@ -47,6 +43,7 @@ class Video(PlexPartialObject): @property def thumbUrl(self): + """Return url to thumb image.""" return self.server.url(self.thumb) def analyze(self): @@ -58,28 +55,24 @@ class Video(PlexPartialObject): self.server.query('/%s/analyze' % self.key) def markWatched(self): - """Mark a items as watched. - """ + """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. - """ + """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): - """Refresh a item. - """ + """Refresh a item.""" self.server.query('%s/refresh' % self.key, method=self.server.session.put) def section(self): - """Library section. - """ + """Library section.""" return self.server.library.sectionByID(self.librarySectionID) @@ -89,9 +82,10 @@ class Movie(Video, Playable): def _loadData(self, data): """Used to set the attributes - + Args: - data (Element): Usually built from server.query + data (Element): XML reponse from PMS as Element + normally built from server.query """ Video._loadData(self, data) Playable._loadData(self, data) @@ -153,7 +147,7 @@ class Show(Video): def _loadData(self, data): """Used to set the attributes - + Args: data (Element): Usually built from server.query """ @@ -190,16 +184,25 @@ class Show(Video): return bool(self.viewedLeafCount == self.leafCount) def seasons(self): - """Returns a list of Season - """ + """Returns a list of Season.""" path = '/library/metadata/%s/children' % self.ratingKey return utils.listItems(self.server, path, Season.TYPE) def season(self, title): + """Returns a Season + + Args: + title (str): fx Season1 + """ path = '/library/metadata/%s/children' % self.ratingKey return utils.findItem(self.server, path, 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.server, leavesKey, watched=watched) @@ -208,21 +211,23 @@ class Show(Video): return utils.findItem(self.server, path, title) def watched(self): - """Return a list of watched episodes - """ + """Return a list of watched episodes""" return self.episodes(watched=True) def unwatched(self): - """Return a list of unwatched episodes - """ + """Return a list of unwatched episodes""" return self.episodes(watched=False) def get(self, title): + """Get a Episode with a title. + + Args: + title (str): fx Secret santa + """ return self.episode(title) def refresh(self): - """Refresh the metadata - """ + """Refresh the metadata.""" self.server.query('/library/metadata/%s/refresh' % self.ratingKey) @@ -232,7 +237,7 @@ class Season(Video): def _loadData(self, data): """Used to set the attributes - + Args: data (Element): Usually built from server.query """ @@ -250,36 +255,51 @@ class Season(Video): @property def seasonNumber(self): + """Reurns season number.""" return self.index def episodes(self, watched=None): - """Return list of Episode - - Args: - watched (None, optional): Description + """Returs a list of Episode + + Args: + watched (bool): Defaults to None. Exclude watched episodes """ childrenKey = '/library/metadata/%s/children' % self.ratingKey return utils.listItems(self.server, childrenKey, watched=watched) def episode(self, title): - """Return Episode - + """Find a episode with a matching title. + Args: - title (TYPE): Description + title (sting): Fx + + Returns: + Episode """ path = '/library/metadata/%s/children' % self.ratingKey return utils.findItem(self.server, path, title) def get(self, title): + """Get a episode witha mathcing title + + Args: + title (str): fx Secret santa + + Returns: + Episode + """ return self.episode(title) def show(self): + """Return this seasons show.""" return utils.listItems(self.server, self.parentKey)[0] def watched(self): + """Returns a list of watched Episode""" return self.episodes(watched=True) def unwatched(self): + """Returns a list of unwatched Episode""" return self.episodes(watched=False) @@ -288,6 +308,11 @@ class Episode(Video, Playable): TYPE = 'episode' 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) @@ -311,7 +336,6 @@ class Episode(Video, Playable): 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) @@ -331,20 +355,25 @@ class Episode(Video, Playable): @property def isWatched(self): + """Returns True if watched, False if not.""" return bool(self.viewCount > 0) @property def seasonNumber(self): + """Return this episode seasonnumber.""" if self._seasonNumber is None: self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber return self._seasonNumber @property def thumbUrl(self): + """Return url to thumb image.""" return self.server.url(self.grandparentThumb) def season(self): + """Return this episode Season""" return utils.listItems(self.server, self.parentKey)[0] def show(self): + """Return this episodes Show""" return utils.listItems(self.server, self.grandparentKey)[0]