From 9d364e4860f961d21f956c96b17b2dacc9436fa1 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 13:15:15 +0100 Subject: [PATCH 1/9] init chapters. --- plexapi/media.py | 21 +++++++++++++++++++++ plexapi/video.py | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/plexapi/media.py b/plexapi/media.py index 4910fc58..e881c3d3 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -468,6 +468,27 @@ 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/video.py b/plexapi/video.py index fba13a9e..09e16601 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -111,14 +111,18 @@ 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. """ TAG = 'Video' TYPE = 'movie' + _include = '?checkFiles=1&includeExtras=1&includeRelated=1&includeRelatedCount=5&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.key = data.attrib.get('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 +151,7 @@ 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) @property def actors(self): From bb55a00f522bdd6c28f27ec3d3ad19a3d3048383 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 17:08:10 +0100 Subject: [PATCH 2/9] really need to sanity check on this one.. --- plexapi/base.py | 19 +++++++++++++++++-- plexapi/video.py | 17 +++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/plexapi/base.py b/plexapi/base.py index 4652fa4f..1aee59df 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -182,7 +182,12 @@ class PlexObject(object): def reload(self, key=None): """ Reload the data for this object from self.key. """ - key = key or self.key + if key is None: + if hasattr(self, '_details_key'): + key = self._details_key + else: + key = self.key + if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key @@ -275,7 +280,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 @@ -310,6 +315,8 @@ class PlexPartialObject(PlexObject): search result for a movie often only contain a portion of the attributes a full object (main url) for that movie contain. """ + #print(self._details_key == self._initpath) + #return self._details_key == self._initpath or not self.key or self.key == self._initpath return not self.key or self.key == self._initpath def isPartialObject(self): @@ -448,6 +455,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/video.py b/plexapi/video.py index 09e16601..ff8e0a0b 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -79,7 +79,7 @@ class Video(PlexPartialObject): @utils.registerPlexObject -class Movie(Video, Playable): +class Movie(Playable, Video): """ Represents a single Movie. Attributes: @@ -115,14 +115,18 @@ class Movie(Video, Playable): """ TAG = 'Video' TYPE = 'movie' - _include = '?checkFiles=1&includeExtras=1&includeRelated=1&includeRelatedCount=5&includeOnDeck=1&includeChapters=1&includePopularLeaves=1&includeConcerts=1&includePreferences=1' + _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.key = data.attrib.get('key') + self._include + self.key = data.attrib.get('key') + 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') @@ -442,7 +446,7 @@ class Season(Video): @utils.registerPlexObject -class Episode(Video, Playable): +class Episode(Playable, Video): """ Represents a single Shows Episode. Attributes: @@ -476,11 +480,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') @@ -509,6 +517,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 [ From eb337ce585d651ec061056192c113ad1fd4b1962 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 18:27:57 +0100 Subject: [PATCH 3/9] Add support for similar for tvshows and movies. --- plexapi/base.py | 1 + plexapi/media.py | 1 - plexapi/video.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plexapi/base.py b/plexapi/base.py index 1aee59df..d0d0a27c 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) diff --git a/plexapi/media.py b/plexapi/media.py index e881c3d3..8a72c537 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -477,7 +477,6 @@ class Chapter(PlexObject): """ TAG = 'Chapter' - def _loadData(self, data): self._data = data self.id = cast(int, data.attrib.get('id', 0)) diff --git a/plexapi/video.py b/plexapi/video.py index ff8e0a0b..2891b401 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -119,7 +119,6 @@ class Movie(Playable, Video): '&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) @@ -156,6 +155,7 @@ class Movie(Playable, Video): 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): @@ -264,6 +264,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): From 30ec806536847fbd03b1989f2a86eff0930de505 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 18:43:31 +0100 Subject: [PATCH 4/9] fix some docs. --- plexapi/base.py | 2 -- plexapi/video.py | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plexapi/base.py b/plexapi/base.py index d0d0a27c..3ec8a05c 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -316,8 +316,6 @@ class PlexPartialObject(PlexObject): search result for a movie often only contain a portion of the attributes a full object (main url) for that movie contain. """ - #print(self._details_key == self._initpath) - #return self._details_key == self._initpath or not self.key or self.key == self._initpath return not self.key or self.key == self._initpath def isPartialObject(self): diff --git a/plexapi/video.py b/plexapi/video.py index 2891b401..45c7fb4d 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -83,7 +83,7 @@ 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). @@ -112,6 +112,7 @@ class Movie(Playable, Video): 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' @@ -213,7 +214,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/) @@ -232,6 +233,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' @@ -351,7 +353,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. @@ -451,7 +453,7 @@ 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). From 3af2a5af59f71c774ba3103b410d92cf6ba5814f Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 19:02:22 +0100 Subject: [PATCH 5/9] Add support for relay --- plexapi/myplex.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index b207164c..93abd2f8 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -537,7 +537,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 @@ -615,6 +615,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): From b111e2490e466339a9b680ac086cfb4b653b04ad Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 19:11:43 +0100 Subject: [PATCH 6/9] add more missing stuff from /resources. --- plexapi/myplex.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 93abd2f8..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 @@ -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. From 4e404cb5021d84f8eef0e1e5d45fc376ec62c071 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 19:28:23 +0100 Subject: [PATCH 7/9] hopefully fix reload issue. i really need to get add creds so i can test this locally. --- plexapi/base.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plexapi/base.py b/plexapi/base.py index 3ec8a05c..f452e3e1 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -183,12 +183,7 @@ class PlexObject(object): def reload(self, key=None): """ Reload the data for this object from self.key. """ - if key is None: - if hasattr(self, '_details_key'): - key = self._details_key - else: - key = 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 From a322574dc6c14dfb3cd5c329541b07319f66498c Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 20:13:38 +0100 Subject: [PATCH 8/9] WTF. why is a key in history None?? --- plexapi/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/video.py b/plexapi/video.py index 45c7fb4d..d119f450 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')) From f6aa27eb83b3c5c6f015574b5ddb21cea00ee771 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 2 Mar 2018 20:13:51 +0100 Subject: [PATCH 9/9] ffs --- plexapi/video.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plexapi/video.py b/plexapi/video.py index d119f450..2d308510 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -125,7 +125,6 @@ class Movie(Playable, Video): Video._loadData(self, data) Playable._loadData(self, data) - self.key = data.attrib.get('key') self._details_key = self.key + self._include self.art = data.attrib.get('art') self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))