diff --git a/plexapi/base.py b/plexapi/base.py index 4652fa4f..f452e3e1 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -43,6 +43,7 @@ class PlexObject(object): self._server = server self._data = data self._initpath = initpath or self.key + self._details_key = '' if data is not None: self._loadData(data) @@ -182,7 +183,7 @@ class PlexObject(object): def reload(self, key=None): """ Reload the data for this object from self.key. """ - key = key or self.key + key = key or self._details_key or self.key if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key @@ -275,7 +276,7 @@ class PlexPartialObject(PlexObject): if attr == 'key' or attr.startswith('_'): return value if value not in (None, []): return value if self.isFullObject(): return value - # Log warning that were reloading the object + # Log the reload. clsname = self.__class__.__name__ title = self.__dict__.get('title', self.__dict__.get('name')) objname = "%s '%s'" % (clsname, title) if title else clsname @@ -448,6 +449,14 @@ class Playable(object): self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist + 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 self._details_key == self._initpath or not self.key + def getStreamURL(self, **params): """ Returns a stream url that may be used by external applications such as VLC. diff --git a/plexapi/media.py b/plexapi/media.py index 7a9ce41b..81f7d6f9 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -469,6 +469,26 @@ class Writer(MediaTag): FILTER = 'writer' +@utils.registerPlexObject +class Chapter(PlexObject): + """ Represents a single Writer media tag. + + Attributes: + TAG (str): 'Chapter' + """ + TAG = 'Chapter' + + def _loadData(self, data): + self._data = data + self.id = cast(int, data.attrib.get('id', 0)) + self.filter = data.attrib.get('filter') # I couldn't filter on it anyways + self.tag = data.attrib.get('tag') + self.title = self.tag + self.index = cast(int, data.attrib.get('index')) + self.start = cast(int, data.attrib.get('startTimeOffset')) + self.end = cast(int, data.attrib.get('endTimeOffset')) + + @utils.registerPlexObject class Field(PlexObject): """ Represents a single Field. diff --git a/plexapi/myplex.py b/plexapi/myplex.py index b207164c..e719c8ea 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -512,11 +512,12 @@ class MyPlexServerShare(PlexObject): class MyPlexResource(PlexObject): """ This object represents resources connected to your Plex server that can provide content such as Plex Media Servers, iPhone or Android clients, etc. The raw xml - for the data presented here can be found at: https://plex.tv/api/resources?includeHttps=1 + for the data presented here can be found at: + https://plex.tv/api/resources?includeHttps=1&includeRelay=1 Attributes: TAG (str): 'Device' - key (str): 'https://plex.tv/api/resources?includeHttps=1' + key (str): 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1' accessToken (str): This resources accesstoken. clientIdentifier (str): Unique ID for this resource. connections (list): List of :class:`~myplex.ResourceConnection` objects @@ -537,7 +538,7 @@ class MyPlexResource(PlexObject): synced (bool): Unknown (possibly True if the resource has synced content?) """ TAG = 'Device' - key = 'https://plex.tv/api/resources?includeHttps=1' + key = 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1' def _loadData(self, data): self._data = data @@ -557,6 +558,11 @@ class MyPlexResource(PlexObject): self.synced = utils.cast(bool, data.attrib.get('synced')) self.presence = utils.cast(bool, data.attrib.get('presence')) self.connections = self.findItems(data, ResourceConnection) + self.publicAddressMatches = utils.cast(bool, data.attrib.get('publicAddressMatches')) + # This seems to only be available if its not your device (say are shared server) + self.httpsRequired = utils.cast(bool, data.attrib.get('httpsRequired')) + self.ownerid = utils.cast(int, data.attrib.get('ownerId', 0)) + self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username. def connect(self, ssl=None, timeout=None): """ Returns a new :class:`~server.PlexServer` or :class:`~client.PlexClient` object. @@ -615,6 +621,7 @@ class ResourceConnection(PlexObject): self.uri = data.attrib.get('uri') self.local = utils.cast(bool, data.attrib.get('local')) self.httpuri = 'http://%s:%s' % (self.address, self.port) + self.relay = utils.cast(bool, data.attrib.get('relay')) class MyPlexDevice(PlexObject): diff --git a/plexapi/video.py b/plexapi/video.py index fba13a9e..2d308510 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -30,7 +30,7 @@ class Video(PlexPartialObject): self._data = data self.listType = 'video' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) - self.key = data.attrib.get('key') + self.key = data.attrib.get('key', '') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.librarySectionID = data.attrib.get('librarySectionID') self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) @@ -79,11 +79,11 @@ class Video(PlexPartialObject): @utils.registerPlexObject -class Movie(Video, Playable): +class Movie(Playable, Video): """ Represents a single Movie. Attributes: - TAG (str): 'Diectory' + TAG (str): 'Video' TYPE (str): 'movie' art (str): Key to movie artwork (/library/metadata//art/) audienceRating (float): Audience rating (usually from Rotten Tomatoes). @@ -111,14 +111,21 @@ class Movie(Video, Playable): producers (List<:class:`~plexapi.media.Producer`>): List of producers objects. roles (List<:class:`~plexapi.media.Role`>): List of role objects. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. + chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. + similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. """ TAG = 'Video' TYPE = 'movie' + _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' + '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' + '&includeConcerts=1&includePreferences=1') def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) Playable._loadData(self, data) + + self._details_key = self.key + self._include self.art = data.attrib.get('art') self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audienceRatingImage = data.attrib.get('audienceRatingImage') @@ -147,6 +154,8 @@ class Movie(Video, Playable): self.roles = self.findItems(data, media.Role) self.writers = self.findItems(data, media.Writer) self.labels = self.findItems(data, media.Label) + self.chapters = self.findItems(data, media.Chapter) + self.similar = self.findItems(data, media.Similar) @property def actors(self): @@ -204,7 +213,7 @@ class Show(Video): """ Represents a single Show (including all seasons and episodes). Attributes: - TAG (str): 'Diectory' + TAG (str): 'Directory' TYPE (str): 'show' art (str): Key to show artwork (/library/metadata//art/) banner (str): Key to banner artwork (/library/metadata//art/) @@ -223,6 +232,7 @@ class Show(Video): year (int): Year the show was released. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. roles (List<:class:`~plexapi.media.Role`>): List of role objects. + similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. """ TAG = 'Directory' TYPE = 'show' @@ -255,6 +265,7 @@ class Show(Video): self.genres = self.findItems(data, media.Genre) self.roles = self.findItems(data, media.Role) self.labels = self.findItems(data, media.Label) + self.similar = self.findItems(data, media.Similar) @property def actors(self): @@ -341,7 +352,7 @@ class Season(Video): """ Represents a single Show Season (including all episodes). Attributes: - TAG (str): 'Diectory' + TAG (str): 'Directory' TYPE (str): 'season' leafCount (int): Number of episodes in season. index (int): Season number. @@ -437,11 +448,11 @@ class Season(Video): @utils.registerPlexObject -class Episode(Video, Playable): +class Episode(Playable, Video): """ Represents a single Shows Episode. Attributes: - TAG (str): 'Diectory' + TAG (str): 'Video' TYPE (str): 'episode' art (str): Key to episode artwork (/library/metadata//art/) chapterSource (str): Unknown (media). @@ -471,11 +482,15 @@ class Episode(Video, Playable): """ TAG = 'Video' TYPE = 'episode' + _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' + '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' + '&includeConcerts=1&includePreferences=1') def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) Playable._loadData(self, data) + self._details_key = self.key + self._include self._seasonNumber = None # cached season number self.art = data.attrib.get('art') self.chapterSource = data.attrib.get('chapterSource') @@ -504,6 +519,7 @@ class Episode(Video, Playable): self.writers = self.findItems(data, media.Writer) self.labels = self.findItems(data, media.Label) self.collections = self.findItems(data, media.Collection) + self.chapters = self.findItems(data, media.Chapter) def __repr__(self): return '<%s>' % ':'.join([p for p in [