From 497ceaec9e2d69654b50cb711a6270fd4b954c4c Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 13:08:46 -0800 Subject: [PATCH 01/27] Add Style media tag --- plexapi/media.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plexapi/media.py b/plexapi/media.py index 5ac90a5c2..2532a68ac 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -602,6 +602,18 @@ class Mood(MediaTag): FILTER = 'mood' +@utils.registerPlexObject +class Style(MediaTag): + """ Represents a single Style media tag. + + Attributes: + TAG (str): 'Style' + FILTER (str): 'style' + """ + TAG = 'Style' + FILTER = 'style' + + @utils.registerPlexObject class Poster(PlexObject): """ Represents a Poster. From 50633d36010ba721cee48befefd84b44d163fda5 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 15:23:10 -0800 Subject: [PATCH 02/27] Update audio attributes --- plexapi/audio.py | 184 +++++++++++++++++++++++++++-------------------- 1 file changed, 105 insertions(+), 79 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 08742884f..0b0e7512d 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -10,24 +10,29 @@ class Audio(PlexPartialObject): and :class:`~plexapi.audio.Track` objects. Attributes: - addedAt (datetime): Datetime this item was added to the library. - art (str): URL to artwork image. + addedAt (datetime): Datetime the item was added to the library. + art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. - fields (list): List of :class:`~plexapi.media.Field`. - index (sting): Index Number (often the track number). + fields (List<:class:`~plexapi.media.Field`>): List of field objects. + guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c). + index (int): Plex index number (often the track number). key (str): API URL (/library/metadata/). - lastViewedAt (datetime): Datetime item was last accessed. + lastViewedAt (datetime): Datetime the item was last played. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. + librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. + librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. listType (str): Hardcoded as 'audio' (useful for search filters). - ratingKey (int): Unique key identifying this item. - summary (str): Summary of the artist, track, or album. - thumb (str): URL to thumbnail image. + moods (List<:class:`~plexapi.media.Mood`>): List of mood objects. + ratingKey (int): Unique key identifying the item. + summary (str): Summary of the artist, album, or track. + thumb (str): URL to thumbnail image (/library/metadata//thumb/). thumbBlurHash (str): BlurHash string for thumbnail image. - title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.) + title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.). titleSort (str): Title to use when sorting (defaults to title). type (str): 'artist', 'album', or 'track'. - updatedAt (datatime): Datetime this item was updated. - viewCount (int): Count of times this item was accessed. + updatedAt (datatime): Datetime the item was updated. + userRating (float): Rating of the track (0.0 - 10.0) equaling (0 stars - 5 stars). + viewCount (int): Count of times the item was played. """ METADATA_TYPE = 'track' @@ -35,15 +40,19 @@ class Audio(PlexPartialObject): def _loadData(self, data): """ Load attribute values from Plex XML response. """ self._data = data - self.listType = 'audio' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') self.fields = self.findItems(data, etag='Field') - self.index = data.attrib.get('index') + self.guid = data.attrib.get('guid') + self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.librarySectionID = data.attrib.get('librarySectionID') + self.librarySectionKey = data.attrib.get('librarySectionKey') + self.librarySectionTitle = data.attrib.get('librarySectionTitle') + self.listType = 'audio' + self.moods = self.findItems(data, media.Mood) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.summary = data.attrib.get('summary') self.thumb = data.attrib.get('thumb') @@ -52,6 +61,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0)) @property @@ -67,7 +77,7 @@ def artUrl(self): return self._server.url(art, includeToken=True) if art else None def url(self, part): - """ Returns the full URL for this audio item. Typically used for getting a specific track. """ + """ Returns the full URL for the audio item. Typically used for getting a specific track. """ return self._server.url(part, includeToken=True) if part else None def _defaultSyncTitle(self): @@ -118,12 +128,13 @@ class Artist(Audio): Attributes: TAG (str): 'Directory' TYPE (str): 'artist' - countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents. - genres (list): List of :class:`~plexapi.media.Genre` objects this artist respresents. - guid (str): Unknown (unique ID; com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en) + collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. + countries (List<:class:`~plexapi.media.Country`>): List country objects. + genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. key (str): API URL (/library/metadata/). - location (str): Filepath this artist is found on disk. - similar (list): List of :class:`~plexapi.media.Similar` artists. + locations (list): List of folder paths where the artist is found on disk. + similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. + styles (List<:class:`~plexapi.media.Style`>): List of style objects. """ TAG = 'Directory' TYPE = 'artist' @@ -131,13 +142,13 @@ class Artist(Audio): def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) - self.guid = data.attrib.get('guid') - self.key = self.key.replace('/children', '') # FIX_BUG_50 - self.locations = self.listAttrs(data, 'path', etag='Location') + self.collections = self.findItems(data, media.Collection) self.countries = self.findItems(data, media.Country) self.genres = self.findItems(data, media.Genre) + self.key = self.key.replace('/children', '') # FIX_BUG_50 + self.locations = self.listAttrs(data, 'path', etag='Location') self.similar = self.findItems(data, media.Similar) - self.collections = self.findItems(data, media.Collection) + self.styles = self.findItems(data, media.Style) def __iter__(self): for album in self.albums(): @@ -153,7 +164,7 @@ def album(self, title): return self.fetchItem(key, title__iexact=title) def albums(self, **kwargs): - """ Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """ + """ Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """ key = '%s/children' % self.key return self.fetchItems(key, **kwargs) @@ -167,7 +178,7 @@ def track(self, title): return self.fetchItem(key, title__iexact=title) def tracks(self, **kwargs): - """ Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """ + """ Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """ key = '%s/allLeaves' % self.key return self.fetchItems(key, **kwargs) @@ -176,7 +187,7 @@ def get(self, title): return self.track(title) def download(self, savepath=None, keep_original_name=False, **kwargs): - """ Downloads all tracks for this artist to the specified location. + """ Downloads all tracks for the artist to the specified location. Parameters: savepath (str): Title of the track to return. @@ -202,37 +213,51 @@ class Album(Audio): Attributes: TAG (str): 'Directory' TYPE (str): 'album' - genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents. + collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. + genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. key (str): API URL (/library/metadata/). - originallyAvailableAt (datetime): Datetime this album was released. - parentKey (str): API URL of this artist. - parentRatingKey (int): Unique key identifying artist. - parentThumb (str): URL to artist thumbnail image. - parentTitle (str): Name of the artist for this album. - studio (str): Studio that released this album. - year (int): Year this album was released. + labels (List<:class:`~plexapi.media.Label`>): List of label objects. + leafCount (int): Number of items in the album view. + loudnessAnalysisVersion (int): The Plex loudness analysis version level. + originallyAvailableAt (datetime): Datetime the album was released. + parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). + parentKey (str): API URL of the album artist (/library/metadata/). + parentRatingKey (int): Unique key identifying the album artist. + parentThumb (str): URL to album artist thumbnail image (/library/metadata//thumb/). + parentTitle (str): Name of the album artist. + rating (float): Album rating (7.9; 9.8; 8.1). + studio (str): Studio that released the album. + styles (List<:class:`~plexapi.media.Style`>): List of style objects. + viewedLeafCount (int): Number of items marked as played in the album view. + year (int): Year the album was released. """ TAG = 'Directory' TYPE = 'album' - def __iter__(self): - for track in self.tracks(): - yield track - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) - self.key = self.key.replace('/children', '') # fixes bug #50 + self.collections = self.findItems(data, media.Collection) + self.genres = self.findItems(data, media.Genre) + self.key = self.key.replace('/children', '') # FIX_BUG_50 + self.labels = self.findItems(data, media.Label) + self.leafCount = utils.cast(int, data.attrib.get('leafCount')) + self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion')) self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') + self.parentGuid = data.attrib.get('parentGuid') self.parentKey = data.attrib.get('parentKey') - self.parentRatingKey = data.attrib.get('parentRatingKey') + self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') + self.rating = utils.cast(float, data.attrib.get('rating')) self.studio = data.attrib.get('studio') + self.styles = self.findItems(data, media.Style) + self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) - self.genres = self.findItems(data, media.Genre) - self.collections = self.findItems(data, media.Collection) - self.labels = self.findItems(data, media.Label) + + def __iter__(self): + for track in self.tracks(): + yield track def track(self, title): """ Returns the :class:`~plexapi.audio.Track` that matches the specified title. @@ -244,7 +269,7 @@ def track(self, title): return self.fetchItem(key, title__iexact=title) def tracks(self, **kwargs): - """ Returns a list of :class:`~plexapi.audio.Track` objects in this album. """ + """ Returns a list of :class:`~plexapi.audio.Track` objects in the album. """ key = '%s/children' % self.key return self.fetchItems(key, **kwargs) @@ -253,11 +278,11 @@ def get(self, title): return self.track(title) def artist(self): - """ Return :func:`~plexapi.audio.Artist` of this album. """ + """ Return the album's :class:`~plexapi.audio.Artist`. """ return self.fetchItem(self.parentKey) def download(self, savepath=None, keep_original_name=False, **kwargs): - """ Downloads all tracks for this artist to the specified location. + """ Downloads all tracks for the artist to the specified location. Parameters: savepath (str): Title of the track to return. @@ -286,32 +311,27 @@ class Track(Audio, Playable): Attributes: TAG (str): 'Directory' TYPE (str): 'track' - chapterSource (TYPE): Unknown - duration (int): Length of this album in seconds. - grandparentArt (str): Album artist artwork. - grandparentKey (str): Album artist API URL. - grandparentRatingKey (str): Unique key identifying album artist. - grandparentThumb (str): URL to album artist thumbnail image. - grandparentTitle (str): Name of the album artist for this track. - guid (str): Unknown (unique ID). - media (list): List of :class:`~plexapi.media.Media` objects for this track. - moods (list): List of :class:`~plexapi.media.Mood` objects for this track. - originalTitle (str): Track artist. + chapterSource (str): Unknown + duration (int): Length of the track in milliseconds. + grandparentArt (str): URL to album artist artwork (/library/metadata//art/). + grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). + grandparentKey (str): API URL of the album artist (/library/metadata/). + grandparentRatingKey (int): Unique key identifying the album artist. + grandparentThumb (str): URL to album artist thumbnail image + (/library/metadata//thumb/). + grandparentTitle (str): Name of the album artist for the track. + media (List<:class:`~plexapi.media.Media`>): List of media objects. + originalTitle (str): The original title of the track (eg. a different language). + parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9). parentIndex (int): Album index. - parentKey (str): Album API URL. - parentRatingKey (int): Unique key identifying album. - parentThumb (str): URL to album thumbnail image. - parentTitle (str): Name of the album for this track. - primaryExtraKey (str): Unknown - ratingCount (int): Unknown - userRating (float): Rating of this track (0.0 - 10.0) equaling (0 stars - 5 stars) - viewOffset (int): Unknown - year (int): Year this track was released. - sessionKey (int): Session Key (active sessions only). - usernames (str): Username of person playing this track (active sessions only). - player (str): :class:`~plexapi.client.PlexClient` for playing track (active sessions only). - transcodeSessions (None): :class:`~plexapi.media.TranscodeSession` for playing - track (active sessions only). + parentKey (str): API URL of the album (/library/metadata/). + parentRatingKey (int): Unique key identifying the album. + parentThumb (str): URL to album thumbnail image (/library/metadata//thumb/). + parentTitle (str): Name of the album for the track. + primaryExtraKey (str) API URL for the primary extra for the track. + ratingCount (int): Number of ratings contributing to the rating score. + viewOffset (int): View offset in milliseconds. + year (int): Year the track was released. """ TAG = 'Track' TYPE = 'track' @@ -323,37 +343,43 @@ def _loadData(self, data): self.chapterSource = data.attrib.get('chapterSource') self.duration = utils.cast(int, data.attrib.get('duration')) self.grandparentArt = data.attrib.get('grandparentArt') + self.grandparentGuid = data.attrib.get('grandparentGuid') self.grandparentKey = data.attrib.get('grandparentKey') - self.grandparentRatingKey = data.attrib.get('grandparentRatingKey') + self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey')) self.grandparentThumb = data.attrib.get('grandparentThumb') self.grandparentTitle = data.attrib.get('grandparentTitle') - self.guid = data.attrib.get('guid') + self.media = self.findItems(data, media.Media) self.originalTitle = data.attrib.get('originalTitle') + self.parentGuid = data.attrib.get('parentGuid') self.parentIndex = data.attrib.get('parentIndex') self.parentKey = data.attrib.get('parentKey') - self.parentRatingKey = data.attrib.get('parentRatingKey') + self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.primaryExtraKey = data.attrib.get('primaryExtraKey') self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) - self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year')) - self.media = self.findItems(data, media.Media) - self.moods = self.findItems(data, media.Mood) def _prettyfilename(self): """ Returns a filename for use in download. """ return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title) def album(self): - """ Return this track's :class:`~plexapi.audio.Album`. """ + """ Return the track's :class:`~plexapi.audio.Album`. """ return self.fetchItem(self.parentKey) def artist(self): - """ Return this track's :class:`~plexapi.audio.Artist`. """ + """ Return the track's :class:`~plexapi.audio.Artist`. """ return self.fetchItem(self.grandparentKey) + @property + def locations(self): + """ This does not exist in plex xml response but is added to have a common + interface to get the locations of the track. + """ + return [part.file for part in self.iterParts() if part] + def _defaultSyncTitle(self): """ Returns str, default title for a new syncItem. """ return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title) From 1c5942986d63170565acc1ed71076d38874f7eb4 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 15:53:42 -0800 Subject: [PATCH 03/27] Update video attributes --- plexapi/video.py | 313 ++++++++++++++++++++++++----------------------- 1 file changed, 161 insertions(+), 152 deletions(-) diff --git a/plexapi/video.py b/plexapi/video.py index 4f04f479e..ab1ed7aa5 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -13,36 +13,42 @@ class Video(PlexPartialObject): :class:`~plexapi.video.Episode`, :class:`~plexapi.video.Clip`. Attributes: - addedAt (datetime): Datetime this item was added to the library. - art (str): URL to artwork image. + addedAt (datetime): Datetime the item was added to the library. + art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. - fields (list): List of :class:`~plexapi.media.Field`. + fields (List<:class:`~plexapi.media.Field`>): List of field objects. + guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8). key (str): API URL (/library/metadata/). - lastViewedAt (datetime): Datetime item was last accessed. + lastViewedAt (datetime): Datetime the item was last played. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. - listType (str): Hardcoded as 'audio' (useful for search filters). - ratingKey (int): Unique key identifying this item. - summary (str): Summary of the artist, track, or album. - thumb (str): URL to thumbnail image. + librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. + librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. + listType (str): Hardcoded as 'video' (useful for search filters). + ratingKey (int): Unique key identifying the item. + summary (str): Summary of the movie, show, season, episode, or clip. + thumb (str): URL to thumbnail image (/library/metadata//thumb/). thumbBlurHash (str): BlurHash string for thumbnail image. - title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.) + title (str): Name of the movie, show, season, episode, or clip. titleSort (str): Title to use when sorting (defaults to title). - type (str): 'artist', 'album', or 'track'. - updatedAt (datatime): Datetime this item was updated. - viewCount (int): Count of times this item was accessed. + type (str): 'movie', 'show', 'season', 'episode', or 'clip'. + updatedAt (datatime): Datetime the item was updated. + viewCount (int): Count of times the item was played. """ def _loadData(self, data): """ Load attribute values from Plex XML response. """ self._data = data - self.listType = 'video' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') self.fields = self.findItems(data, etag='Field') - self.key = data.attrib.get('key', '') + self.guid = data.attrib.get('guid') + self.key = data.attrib.get('key') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.librarySectionID = data.attrib.get('librarySectionID') + self.librarySectionKey = data.attrib.get('librarySectionKey') + self.librarySectionTitle = data.attrib.get('librarySectionTitle') + self.listType = 'video' self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.summary = data.attrib.get('summary') self.thumb = data.attrib.get('thumb') @@ -259,34 +265,32 @@ class Movie(Playable, Video): Attributes: TAG (str): 'Video' TYPE (str): 'movie' - art (str): Key to movie artwork (/library/metadata//art/) audienceRating (float): Audience rating (usually from Rotten Tomatoes). - audienceRatingImage (str): Key to audience rating image (rottentomatoes://image.rating.spilled) + audienceRatingImage (str): Key to audience rating image (rottentomatoes://image.rating.spilled). + chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. chapterSource (str): Chapter source (agent; media; mixed). + collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. contentRating (str) Content rating (PG-13; NR; TV-G). - duration (int): Duration of movie in milliseconds. - guid: Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en). - originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀). - originallyAvailableAt (datetime): Datetime movie was released. - primaryExtraKey (str) Primary extra key (/library/metadata/66351). - rating (float): Movie rating (7.9; 9.8; 8.1). - ratingImage (str): Key to rating image (rottentomatoes://image.rating.rotten). - studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment). - tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). - userRating (float): User rating (2.0; 8.0). - viewOffset (int): View offset in milliseconds. - year (int): Year movie was released. - collections (List<:class:`~plexapi.media.Collection`>): List of collections this media belongs. countries (List<:class:`~plexapi.media.Country`>): List of countries objects. directors (List<:class:`~plexapi.media.Director`>): List of director objects. - fields (List<:class:`~plexapi.media.Field`>): List of field objects. + duration (int): Duration of the movie in milliseconds. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. + labels (List<:class:`~plexapi.media.Label`>): List of label objects. media (List<:class:`~plexapi.media.Media`>): List of media objects. + originallyAvailableAt (datetime): Datetime the movie was released. + originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀). + primaryExtraKey (str) Primary extra key (/library/metadata/66351). producers (List<:class:`~plexapi.media.Producer`>): List of producers objects. + rating (float): Movie critic rating (7.9; 9.8; 8.1). + ratingImage (str): Key to critic rating image (rottentomatoes://image.rating.rotten). 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. + studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment). + tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). + userRating (float): User rating (2.0; 8.0). + viewOffset (int): View offset in milliseconds. + writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. + year (int): Year movie was released. """ TAG = 'Video' TYPE = 'movie' @@ -296,37 +300,32 @@ def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) Playable._loadData(self, data) - - self.art = data.attrib.get('art') self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audienceRatingImage = data.attrib.get('audienceRatingImage') + self.chapters = self.findItems(data, media.Chapter) self.chapterSource = data.attrib.get('chapterSource') + self.collections = self.findItems(data, media.Collection) self.contentRating = data.attrib.get('contentRating') + self.countries = self.findItems(data, media.Country) + self.directors = self.findItems(data, media.Director) self.duration = utils.cast(int, data.attrib.get('duration')) - self.guid = data.attrib.get('guid') + self.genres = self.findItems(data, media.Genre) + self.labels = self.findItems(data, media.Label) + self.media = self.findItems(data, media.Media) + self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') self.originalTitle = data.attrib.get('originalTitle') - self.originallyAvailableAt = utils.toDatetime( - data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') self.primaryExtraKey = data.attrib.get('primaryExtraKey') + self.producers = self.findItems(data, media.Producer) self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingImage = data.attrib.get('ratingImage') + self.roles = self.findItems(data, media.Role) + self.similar = self.findItems(data, media.Similar) self.studio = data.attrib.get('studio') self.tagline = data.attrib.get('tagline') self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) - self.year = utils.cast(int, data.attrib.get('year')) - self.collections = self.findItems(data, media.Collection) - self.countries = self.findItems(data, media.Country) - self.directors = self.findItems(data, media.Director) - self.fields = self.findItems(data, media.Field) - self.genres = self.findItems(data, media.Genre) - self.media = self.findItems(data, media.Media) - self.producers = self.findItems(data, media.Producer) - 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) + self.year = utils.cast(int, data.attrib.get('year')) @property def actors(self): @@ -336,7 +335,7 @@ def actors(self): @property def locations(self): """ This does not exist in plex xml response but is added to have a common - interface to get the location of the Movie/Show/Episode + interface to get the locations of the movie. """ return [part.file for part in self.iterParts() if part] @@ -378,60 +377,56 @@ class Show(Video): Attributes: TAG (str): 'Directory' TYPE (str): 'show' - art (str): Key to show artwork (/library/metadata//art/) - banner (str): Key to banner artwork (/library/metadata//art/) - childCount (int): Unknown. + banner (str): Key to banner artwork (/library/metadata//banner/). + childCount (int): Number of seasons in the show. + collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. contentRating (str) Content rating (PG-13; NR; TV-G). - collections (List<:class:`~plexapi.media.Collection`>): List of collections this media belongs. - duration (int): Duration of show in milliseconds. - guid (str): Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en). - index (int): Plex index (?) - leafCount (int): Unknown. - locations (list): List of locations paths. - originallyAvailableAt (datetime): Datetime show was released. - rating (float): Show rating (7.9; 9.8; 8.1). - studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment). - theme (str): Key to theme resource (/library/metadata//theme/) - viewedLeafCount (int): Unknown. - year (int): Year the show was released. + duration (int): Typical duration of the show episodes in milliseconds. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. + index (int): Plex index number for the show. + key (str): API URL (/library/metadata/). + labels (List<:class:`~plexapi.media.Label`>): List of label objects. + leafCount (int): Number of items in the show view. + locations (list): List of folder paths where the show is found on disk. + originallyAvailableAt (datetime): Datetime the show was released. + rating (float): Show rating (7.9; 9.8; 8.1). roles (List<:class:`~plexapi.media.Role`>): List of role objects. similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. + studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment). + theme (str): URL to theme resource (/library/metadata//theme/). + viewedLeafCount (int): Number of items marked as played in the show view. + year (int): Year the show was released. """ TAG = 'Directory' TYPE = 'show' METADATA_TYPE = 'episode' - def __iter__(self): - for season in self.seasons(): - yield season - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) - # fix key if loaded from search - self.key = self.key.replace('/children', '') - self.art = data.attrib.get('art') self.banner = data.attrib.get('banner') self.childCount = utils.cast(int, data.attrib.get('childCount')) - self.contentRating = data.attrib.get('contentRating') self.collections = self.findItems(data, media.Collection) + self.contentRating = data.attrib.get('contentRating') self.duration = utils.cast(int, data.attrib.get('duration')) - self.guid = data.attrib.get('guid') - self.index = data.attrib.get('index') + self.genres = self.findItems(data, media.Genre) + self.index = utils.cast(int, data.attrib.get('index')) + self.key = self.key.replace('/children', '') # FIX_BUG_50 + self.labels = self.findItems(data, media.Label) self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.locations = self.listAttrs(data, 'path', etag='Location') - self.originallyAvailableAt = utils.toDatetime( - data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') + self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') self.rating = utils.cast(float, data.attrib.get('rating')) + self.roles = self.findItems(data, media.Role) + self.similar = self.findItems(data, media.Similar) self.studio = data.attrib.get('studio') self.theme = data.attrib.get('theme') self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) - 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) + + def __iter__(self): + for season in self.seasons(): + yield season @property def actors(self): @@ -440,7 +435,7 @@ def actors(self): @property def isWatched(self): - """ Returns True if this show is fully watched. """ + """ Returns True if the show is fully watched. """ return bool(self.viewedLeafCount == self.leafCount) def preferences(self): @@ -492,7 +487,7 @@ def onDeck(self): return self.findItems([item for item in data.iter('OnDeck')][0])[0] def seasons(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Season` objects. """ + """ Returns a list of :class:`~plexapi.video.Season` objects in the show. """ key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey return self.fetchItems(key, **kwargs) @@ -508,7 +503,7 @@ def season(self, title=None): return self.fetchItem(key, etag='Directory', title__iexact=title) def episodes(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Episode` objects. """ + """ Returns a list of :class:`~plexapi.video.Episode` objects in the show. """ key = '/library/metadata/%s/allLeaves' % self.ratingKey return self.fetchItems(key, **kwargs) @@ -568,33 +563,41 @@ class Season(Video): Attributes: TAG (str): 'Directory' TYPE (str): 'season' - leafCount (int): Number of episodes in season. index (int): Season number. - parentKey (str): Key to this seasons :class:`~plexapi.video.Show`. - parentRatingKey (int): Unique key for this seasons :class:`~plexapi.video.Show`. - parentTitle (str): Title of this seasons :class:`~plexapi.video.Show`. - viewedLeafCount (int): Number of watched episodes in season. + key (str): API URL (/library/metadata/). + leafCount (int): Number of items in the season view. + parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6). + parentIndex (int): Plex index number for the show. + parentKey (str): API URL of the show (/library/metadata/). + parentRatingKey (int): Unique key identifying the show. + parentTheme (str): URL to show theme resource (/library/metadata//theme/). + parentThumb (str): URL to show thumbnail image (/library/metadata//thumb/). + parentTitle (str): Name of the show for the season. + viewedLeafCount (int): Number of items marked as played in the season view. """ TAG = 'Directory' TYPE = 'season' METADATA_TYPE = 'episode' - def __iter__(self): - for episode in self.episodes(): - yield episode - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) - # fix key if loaded from search - self.key = self.key.replace('/children', '') - self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.index = utils.cast(int, data.attrib.get('index')) + self.key = self.key.replace('/children', '') # FIX_BUG_50 + self.leafCount = utils.cast(int, data.attrib.get('leafCount')) + self.parentGuid = data.attrib.get('parentGuid') + self.parentIndex = data.attrib.get('parentIndex') self.parentKey = data.attrib.get('parentKey') self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) + self.parentTheme = data.attrib.get('parentTheme') + self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) + def __iter__(self): + for episode in self.episodes(): + yield episode + def __repr__(self): return '<%s>' % ':'.join([p for p in [ self.__class__.__name__, @@ -604,7 +607,7 @@ def __repr__(self): @property def isWatched(self): - """ Returns True if this season is fully watched. """ + """ Returns True if the season is fully watched. """ return bool(self.viewedLeafCount == self.leafCount) @property @@ -613,7 +616,7 @@ def seasonNumber(self): return self.index def episodes(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Episode` objects. """ + """ Returns a list of :class:`~plexapi.video.Episode` objects in the season. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, **kwargs) @@ -636,7 +639,7 @@ def get(self, title=None, episode=None): return self.episode(title, episode) def show(self): - """ Return this seasons :func:`~plexapi.video.Show`.. """ + """ Return the season's :class:`~plexapi.video.Show`. """ return self.fetchItem(int(self.parentRatingKey)) def watched(self): @@ -673,31 +676,32 @@ class Episode(Playable, Video): Attributes: TAG (str): 'Video' TYPE (str): 'episode' - art (str): Key to episode artwork (/library/metadata//art/) - chapterSource (str): Unknown (media). + chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. + chapterSource (str): Chapter source (agent; media; mixed). contentRating (str) Content rating (PG-13; NR; TV-G). - duration (int): Duration of episode in milliseconds. - grandparentArt (str): Key to this episodes :class:`~plexapi.video.Show` artwork. - grandparentKey (str): Key to this episodes :class:`~plexapi.video.Show`. - grandparentRatingKey (str): Unique key for this episodes :class:`~plexapi.video.Show`. - grandparentTheme (str): Key to this episodes :class:`~plexapi.video.Show` theme. - grandparentThumb (str): Key to this episodes :class:`~plexapi.video.Show` thumb. - grandparentTitle (str): Title of this episodes :class:`~plexapi.video.Show`. - guid (str): Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en). - index (int): Episode number. - originallyAvailableAt (datetime): Datetime episode was released. - parentIndex (str): Season number of episode. - parentKey (str): Key to this episodes :class:`~plexapi.video.Season`. - parentRatingKey (int): Unique key for this episodes :class:`~plexapi.video.Season`. - parentThumb (str): Key to this episodes thumbnail. - parentTitle (str): Name of this episode's season - title (str): Name of this Episode - rating (float): Movie rating (7.9; 9.8; 8.1). - viewOffset (int): View offset in milliseconds. - year (int): Year episode was released. directors (List<:class:`~plexapi.media.Director`>): List of director objects. + duration (int): Duration of the episode in milliseconds. + grandparentArt (str): URL to show artwork (/library/metadata//art/). + grandparentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6). + grandparentKey (str): API URL of the show (/library/metadata/). + grandparentRatingKey (int): Unique key identifying the show. + grandparentTheme (str): URL to show theme resource (/library/metadata//theme/). + grandparentThumb (str): URL to show thumbnail image (/library/metadata//thumb/). + grandparentTitle (str): Name of the show for the episode. + index (int): Episode number. + markers (List<:class:`~plexapi.media.Marker`>): List of marker objects. media (List<:class:`~plexapi.media.Media`>): List of media objects. + originallyAvailableAt (datetime): Datetime the episode was released. + parentGuid (str): Plex GUID for the season (plex://season/5d9c09e42df347001e3c2a72). + parentIndex (int): Season number of episode. + parentKey (str): API URL of the season (/library/metadata/). + parentRatingKey (int): Unique key identifying the season. + parentThumb (str): URL to season thumbnail image (/library/metadata//thumb/). + parentTitle (str): Name of the season for the episode. + rating (float): Episode rating (7.9; 9.8; 8.1). + viewOffset (int): View offset in milliseconds. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. + year (int): Year episode was released. """ TAG = 'Video' TYPE = 'episode' @@ -708,35 +712,32 @@ def _loadData(self, data): Video._loadData(self, data) Playable._loadData(self, data) self._seasonNumber = None # cached season number - self.art = data.attrib.get('art') + self.chapters = self.findItems(data, media.Chapter) self.chapterSource = data.attrib.get('chapterSource') self.contentRating = data.attrib.get('contentRating') + self.directors = self.findItems(data, media.Director) self.duration = utils.cast(int, data.attrib.get('duration')) self.grandparentArt = data.attrib.get('grandparentArt') + self.grandparentGuid = data.attrib.get('grandparentGuid') self.grandparentKey = data.attrib.get('grandparentKey') self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey')) self.grandparentTheme = data.attrib.get('grandparentTheme') self.grandparentThumb = data.attrib.get('grandparentThumb') self.grandparentTitle = data.attrib.get('grandparentTitle') - self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) + self.markers = self.findItems(data, media.Marker) + self.media = self.findItems(data, media.Media) self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') - self.parentIndex = data.attrib.get('parentIndex') + self.parentGuid = data.attrib.get('parentGuid') + self.parentIndex = utils.cast(int, data.attrib.get('parentIndex')) self.parentKey = data.attrib.get('parentKey') self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') - self.title = data.attrib.get('title') self.rating = utils.cast(float, data.attrib.get('rating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) - self.year = utils.cast(int, data.attrib.get('year')) - self.directors = self.findItems(data, media.Director) - self.media = self.findItems(data, media.Media) 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) - self.markers = self.findItems(data, media.Marker) + self.year = utils.cast(int, data.attrib.get('year')) def __repr__(self): return '<%s>' % ':'.join([p for p in [ @@ -752,13 +753,13 @@ def _prettyfilename(self): @property def locations(self): """ This does not exist in plex xml response but is added to have a common - interface to get the location of the Movie/Show + interface to get the locations of the episode. """ return [part.file for part in self.iterParts() if part] @property def seasonNumber(self): - """ Returns this episodes season number. """ + """ Returns the episodes season number. """ if self._seasonNumber is None: self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber return utils.cast(int, self._seasonNumber) @@ -770,17 +771,17 @@ def seasonEpisode(self): @property def hasIntroMarker(self): - """ Returns True if this episode has an intro marker in the xml. """ + """ Returns True if the episode has an intro marker in the xml. """ if not self.isFullObject(): self.reload() return any(marker.type == 'intro' for marker in self.markers) def season(self): - """" Return this episodes :func:`~plexapi.video.Season`.. """ + """" Return the episode's :class:`~plexapi.video.Season`. """ return self.fetchItem(self.parentKey) def show(self): - """" Return this episodes :func:`~plexapi.video.Show`.. """ + """" Return the episode's :class:`~plexapi.video.Show`. """ return self.fetchItem(int(self.grandparentRatingKey)) def _defaultSyncTitle(self): @@ -792,16 +793,19 @@ def _defaultSyncTitle(self): class Clip(Playable, Video): """Represents a single Clip. - Attributes: - TAG (str): 'Video' - TYPE (str): 'clip' - duration (int): Duration of movie in milliseconds. - extraType (int): Unknown - guid: Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en). - index (int): Plex index (?) - originallyAvailableAt (datetime): Datetime movie was released. - subtype (str): Type of clip - viewOffset (int): View offset in milliseconds. + Attributes: + TAG (str): 'Video' + TYPE (str): 'clip' + duration (int): Duration of the clip in milliseconds. + extraType (int): Unknown. + index (int): Plex index number for the clip. + media (List<:class:`~plexapi.media.Media`>): List of media objects. + originallyAvailableAt (datetime): Datetime the clip was released. + skipDetails (int): Unknown. + subtype (str): Type of clip (trailer, behindTheScenes, sceneOrSample, etc.). + thumbAspectRatio (str): Aspect ratio of the thumbnail image. + viewOffset (int): View offset in milliseconds. + year (int): Year clip was released. """ TAG = 'Video' @@ -812,16 +816,21 @@ def _loadData(self, data): """Load attribute values from Plex XML response.""" Video._loadData(self, data) Playable._loadData(self, data) + self._data = data self.duration = utils.cast(int, data.attrib.get('duration')) self.extraType = utils.cast(int, data.attrib.get('extraType')) - self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) + self.media = self.findItems(data, media.Media) self.originallyAvailableAt = data.attrib.get('originallyAvailableAt') + self.skipDetails = utils.cast(int, data.attrib.get('skipDetails')) self.subtype = data.attrib.get('subtype') + self.thumbAspectRatio = data.attrib.get('thumbAspectRatio') self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) + self.year = utils.cast(int, data.attrib.get('year')) - def section(self): - """Return the :class:`~plexapi.library.LibrarySection` this item belongs to.""" - # Clip payloads currently do not contain 'librarySectionID'. - # Return None to avoid unnecessary attribute lookup attempts. - return None + @property + def locations(self): + """ This does not exist in plex xml response but is added to have a common + interface to get the locations of the clip. + """ + return [part.file for part in self.iterParts() if part] From cf56d9a13c8c8e48ed2170564bb19033f628a7c0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:16:08 -0800 Subject: [PATCH 04/27] Update photo attributes --- plexapi/photo.py | 125 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/plexapi/photo.py b/plexapi/photo.py index 301ec319f..0898d4797 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -13,28 +13,31 @@ class Photoalbum(PlexPartialObject): Attributes: TAG (str): 'Directory' TYPE (str): 'photo' - addedAt (datetime): Datetime this item was added to the library. - art (str): Photo art (/library/metadata//art/) - composite (str): Unknown - fields (list): List of :class:`~plexapi.media.Field`. - guid (str): Unknown (unique ID) - index (sting): Index number of this album. + addedAt (datetime): Datetime the photo album was added to the library. + art (str): URL to artwork image (/library/metadata//art/). + composite (str): URL to composite image (/library/metadata//composite/) + fields (List<:class:`~plexapi.media.Field`>): List of field objects. + guid (str): Plex GUID for the photo album (local://229674). + index (sting): Plex index number for the photo album. key (str): API URL (/library/metadata/). librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. + librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. + librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. listType (str): Hardcoded as 'photo' (useful for search filters). - ratingKey (int): Unique key identifying this item. + ratingKey (int): Unique key identifying the photo album. summary (str): Summary of the photoalbum. - thumb (str): URL to thumbnail image. - title (str): Photoalbum title. (Trip to Disney World) - type (str): Unknown - updatedAt (datatime): Datetime this item was updated. + thumb (str): URL to thumbnail image (/library/metadata//thumb/). + title (str): Name of the photo album. (Trip to Disney World) + titleSort (str): Title to use when sorting (defaults to title). + type (str): 'photo' + updatedAt (datatime): Datetime the photo album was updated. + userRating (float): Rating of the photoalbum (0.0 - 10.0) equaling (0 stars - 5 stars). """ TAG = 'Directory' TYPE = 'photo' def _loadData(self, data): """ Load attribute values from Plex XML response. """ - self.listType = 'photo' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.composite = data.attrib.get('composite') @@ -43,15 +46,20 @@ def _loadData(self, data): self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') self.librarySectionID = data.attrib.get('librarySectionID') - self.ratingKey = data.attrib.get('ratingKey') + self.librarySectionKey = data.attrib.get('librarySectionKey') + self.librarySectionTitle = data.attrib.get('librarySectionTitle') + self.listType = 'photo' + self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.summary = data.attrib.get('summary') self.thumb = data.attrib.get('thumb') self.title = data.attrib.get('title') + self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) def albums(self, **kwargs): - """ Returns a list of :class:`~plexapi.photo.Photoalbum` objects in this album. """ + """ Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, etag='Directory', **kwargs) @@ -63,7 +71,7 @@ def album(self, title): raise NotFound('Unable to find album: %s' % title) def photos(self, **kwargs): - """ Returns a list of :class:`~plexapi.photo.Photo` objects in this album. """ + """ Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, etag='Photo', **kwargs) @@ -74,8 +82,20 @@ def photo(self, title): return photo raise NotFound('Unable to find photo: %s' % title) + def clips(self, **kwargs): + """ Returns a list of :class:`~plexapi.video.Clip` objects in the album. """ + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItems(key, etag='Video', **kwargs) + + def clip(self, title): + """ Returns the :class:`~plexapi.video.Clip` that matches the specified title. """ + for clip in self.clips(): + if clip.title.lower() == title.lower(): + return clip + raise NotFound('Unable to find clip: %s' % title) + def iterParts(self): - """ Iterates over the parts of this media item. """ + """ Iterates over the parts of the media item. """ for album in self.albums(): for photo in album.photos(): for part in photo.iterParts(): @@ -112,23 +132,34 @@ class Photo(PlexPartialObject, Playable): Attributes: TAG (str): 'Photo' TYPE (str): 'photo' - addedAt (datetime): Datetime this item was added to the library. + addedAt (datetime): Datetime the photo was added to the library. + createdAtAccuracy (str): Unknown (local). + createdAtTZOffset (int): Unknown (-25200). fields (list): List of :class:`~plexapi.media.Field`. - index (sting): Index number of this photo. + guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn). + index (sting): Plex index number for the photo. key (str): API URL (/library/metadata/). librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. + librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. + librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. listType (str): Hardcoded as 'photo' (useful for search filters). - media (TYPE): Unknown - originallyAvailableAt (datetime): Datetime this photo was added to Plex. - parentKey (str): Photoalbum API URL. - parentRatingKey (int): Unique key identifying the photoalbum. - ratingKey (int): Unique key identifying this item. + media (List<:class:`~plexapi.media.Media`>): List of media objects. + originallyAvailableAt (datetime): Datetime the photo was added to Plex. + parentGuid (str): Plex GUID for the photo album (local://229674). + parentIndex (int): Plex index number for the photo album. + parentKey (str): API URL of the photo album (/library/metadata/). + parentRatingKey (int): Unique key identifying the photo album. + parentThumb (str): URL to photo album thumbnail image (/library/metadata//thumb/). + parentTitle (str): Name of the photo album for the photo. + ratingKey (int): Unique key identifying the photo. summary (str): Summary of the photo. - thumb (str): URL to thumbnail image. - title (str): Photo title. - type (str): Unknown - updatedAt (datatime): Datetime this item was updated. - year (int): Year this photo was taken. + tag (List<:class:`~plexapi.media.Tag`>): List of tag objects. + thumb (str): URL to thumbnail image (/library/metadata//thumb/). + title (str): Name of the photo. + titleSort (str): Title to use when sorting (defaults to title). + type (str): 'photo' + updatedAt (datatime): Datetime the photo was updated. + year (int): Year the photo was taken. """ TAG = 'Photo' TYPE = 'photo' @@ -137,25 +168,34 @@ class Photo(PlexPartialObject, Playable): def _loadData(self, data): """ Load attribute values from Plex XML response. """ Playable._loadData(self, data) - self.listType = 'photo' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) - self.fields = self.findItems(data, etag='Field') + self.createdAtAccuracy = data.attrib.get('createdAtAccuracy') + self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset')) + self.fields = self.findItems(data, media.Field) + self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') self.librarySectionID = data.attrib.get('librarySectionID') - self.originallyAvailableAt = utils.toDatetime( - data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') + self.librarySectionKey = data.attrib.get('librarySectionKey') + self.librarySectionTitle = data.attrib.get('librarySectionTitle') + self.listType = 'photo' + self.media = self.findItems(data, media.Media) + self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') + self.parentGuid = data.attrib.get('parentGuid') + self.parentIndex = utils.cast(int, data.attrib.get('parentIndex')) self.parentKey = data.attrib.get('parentKey') - self.parentRatingKey = data.attrib.get('parentRatingKey') - self.ratingKey = data.attrib.get('ratingKey') + self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) + self.parentThumb = data.attrib.get('parentThumb') + self.parentTitle = data.attrib.get('parentTitle') + self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.summary = data.attrib.get('summary') + self.tag = self.findItems(data, media.Tag) self.thumb = data.attrib.get('thumb') self.title = data.attrib.get('title') + self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.year = utils.cast(int, data.attrib.get('year')) - self.media = self.findItems(data, media.Media) - self.tag = self.findItems(data, media.Tag) @property def thumbUrl(self): @@ -164,11 +204,11 @@ def thumbUrl(self): return self._server.url(key, includeToken=True) if key else None def photoalbum(self): - """ Return this photo's :class:`~plexapi.photo.Photoalbum`. """ + """ Return the photo's :class:`~plexapi.photo.Photoalbum`. """ return self.fetchItem(self.parentKey) def section(self): - """ Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """ + """ Returns the :class:`~plexapi.library.LibrarySection` the item belongs to. """ if hasattr(self, 'librarySectionID'): return self._server.library.sectionByID(self.librarySectionID) elif self.parentKey: @@ -176,8 +216,15 @@ def section(self): else: raise BadRequest('Unable to get section for photo, can`t find librarySectionID') + @property + def locations(self): + """ This does not exist in plex xml response but is added to have a common + interface to get the locations of the photo. + """ + return [part.file for item in self.media for part in item.parts if part] + def iterParts(self): - """ Iterates over the parts of this media item. """ + """ Iterates over the parts of the media item. """ for item in self.media: for part in item.parts: yield part From 99c447525af18c91d191e63458c7f905679bde0a Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 15:44:05 -0800 Subject: [PATCH 05/27] Update library.Collection attributes --- plexapi/library.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index c88566c3f..9ed90c649 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1413,33 +1413,32 @@ class Collections(PlexPartialObject): Attributes: TAG (str): 'Directory' TYPE (str): 'collection' - - ratingKey (int): Unique key identifying this item. - addedAt (datetime): Datetime this item was added to the library. - art (str): URL to artwork image. + addedAt (datetime): Datetime the collection was added to the library. + art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. - childCount (int): Count of child object(s) + childCount (int): Number of items in the collection. collectionMode (str): How the items in the collection are displayed. collectionSort (str): How to sort the items in the collection. contentRating (str) Content rating (PG-13; NR; TV-G). - fields (list): List of :class:`~plexapi.media.Field`. - guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). - index (int): Unknown + fields (List<:class:`~plexapi.media.Field`>): List of field objects. + guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). + index (int): Plex index number for the collection. key (str): API URL (/library/metadata/). - labels (List<:class:`~plexapi.media.Label`>): List of field objects. + labels (List<:class:`~plexapi.media.Label`>): List of label objects. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. - librarySectionKey (str): API URL (/library/sections/). - librarySectionTitle (str): Section Title - maxYear (int): Year - minYear (int): Year - subtype (str): Media type - summary (str): Summary of the collection - thumb (str): URL to thumbnail image. + librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. + librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. + maxYear (int): Maximum year for the items in the collection. + minYear (int): Minimum year for the items in the collection. + ratingKey (int): Unique key identifying the collection. + subtype (str): Media type of the items in the collection (movie, show, artist, or album). + summary (str): Summary of the collection. + thumb (str): URL to thumbnail image (/library/metadata//thumb/). thumbBlurHash (str): BlurHash string for thumbnail image. - title (str): Collection Title + title (str): Name of the collection. titleSort (str): Title to use when sorting (defaults to title). - type (str): Hardcoded 'collection' - updatedAt (datatime): Datetime this item was updated. + type (str): 'collection' + updatedAt (datatime): Datetime the collection was updated. """ @@ -1447,7 +1446,6 @@ class Collections(PlexPartialObject): TYPE = 'collection' def _loadData(self, data): - self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') @@ -1465,12 +1463,13 @@ def _loadData(self, data): self.librarySectionTitle = data.attrib.get('librarySectionTitle') self.maxYear = utils.cast(int, data.attrib.get('maxYear')) self.minYear = utils.cast(int, data.attrib.get('minYear')) + self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.subtype = data.attrib.get('subtype') self.summary = data.attrib.get('summary') self.thumb = data.attrib.get('thumb') self.thumbBlurHash = data.attrib.get('thumbBlurHash') self.title = data.attrib.get('title') - self.titleSort = data.attrib.get('titleSort') + self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) From 22bfeb082066507f84f4d23cfb5ee93337363778 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:39:54 -0800 Subject: [PATCH 06/27] Update test_audio index is integer --- tests/test_audio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_audio.py b/tests/test_audio.py index ac148c131..026ee5589 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -11,7 +11,7 @@ def test_audio_Artist_attr(artist): assert "United States of America" in [i.tag for i in artist.countries] #assert "Electronic" in [i.tag for i in artist.genres] assert utils.is_string(artist.guid, gte=5) - assert artist.index == "1" + assert artist.index == 1 assert utils.is_metadata(artist._initpath) assert utils.is_metadata(artist.key) assert utils.is_int(artist.librarySectionID) @@ -63,7 +63,7 @@ def test_audio_Artist_albums(artist): def test_audio_Album_attrs(album): assert utils.is_datetime(album.addedAt) assert isinstance(album.genres, list) - assert album.index == "1" + assert album.index == 1 assert utils.is_metadata(album._initpath) assert utils.is_metadata(album.key) assert utils.is_int(album.librarySectionID) @@ -106,7 +106,7 @@ def test_audio_Album_tracks(album): assert utils.is_metadata(track.grandparentKey) assert utils.is_int(track.grandparentRatingKey) assert track.grandparentTitle == "Broke For Free" - assert track.index == "1" + assert track.index == 1 assert utils.is_metadata(track._initpath) assert utils.is_metadata(track.key) assert track.listType == "audio" From 41342b4f7b0125dd2b1dd7b6c06b51c2bbd64870 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:39:15 -0800 Subject: [PATCH 07/27] Replace use of etag with class --- plexapi/audio.py | 2 +- plexapi/library.py | 11 +++++------ plexapi/photo.py | 2 +- plexapi/video.py | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 0b0e7512d..98b926861 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -43,7 +43,7 @@ def _loadData(self, data): self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') - self.fields = self.findItems(data, etag='Field') + self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') diff --git a/plexapi/library.py b/plexapi/library.py index 9ed90c649..64619436b 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- from urllib.parse import quote, quote_plus, unquote, urlencode -from plexapi import X_PLEX_CONTAINER_SIZE, log, utils +from plexapi import X_PLEX_CONTAINER_SIZE, log, media, utils from plexapi.base import PlexObject, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound -from plexapi.media import MediaTag from plexapi.settings import Setting from plexapi.utils import deprecated @@ -732,7 +731,7 @@ def _cleanSearchFilter(self, category, value, libtype=None): lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices} allowed = set(c.key for c in choices) for item in value: - item = str((item.id or item.tag) if isinstance(item, MediaTag) else item).lower() + item = str((item.id or item.tag) if isinstance(item, media.MediaTag) else item).lower() # find most logical choice(s) to use in url if item in allowed: result.add(item); continue if item in lookup: result.add(lookup[item]); continue @@ -757,7 +756,7 @@ def _cleanSearchSort(self, sort): def _locations(self): """ Returns a list of :class:`~plexapi.library.Location` objects """ - return self.findItems(self._data, etag='Location') + return self.findItems(self._data, Location) def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None, **kwargs): @@ -1453,11 +1452,11 @@ def _loadData(self, data): self.collectionMode = data.attrib.get('collectionMode') self.collectionSort = data.attrib.get('collectionSort') self.contentRating = data.attrib.get('contentRating') - self.fields = self.findItems(data, etag='Field') + self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50 - self.labels = self.findItems(data, etag='Label') + self.labels = self.findItems(data, media.Label) self.librarySectionID = data.attrib.get('librarySectionID') self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') diff --git a/plexapi/photo.py b/plexapi/photo.py index 0898d4797..fbc0ee22e 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -41,7 +41,7 @@ def _loadData(self, data): self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.composite = data.attrib.get('composite') - self.fields = self.findItems(data, etag='Field') + self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') diff --git a/plexapi/video.py b/plexapi/video.py index ab1ed7aa5..e89777514 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -41,7 +41,7 @@ def _loadData(self, data): self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') - self.fields = self.findItems(data, etag='Field') + self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.key = data.attrib.get('key') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) @@ -499,8 +499,8 @@ def season(self, title=None): """ key = '/library/metadata/%s/children' % self.ratingKey if isinstance(title, int): - return self.fetchItem(key, etag='Directory', index__iexact=str(title)) - return self.fetchItem(key, etag='Directory', title__iexact=title) + return self.fetchItem(key, index__iexact=str(title)) + return self.fetchItem(key, title__iexact=title) def episodes(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Episode` objects in the show. """ From 7f1e2cafab2e049a39e4b59a2251ff3181c8a4bd Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:39:45 -0800 Subject: [PATCH 08/27] Update LibrarySection doc strings --- plexapi/library.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index 64619436b..7254f3c83 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -312,26 +312,22 @@ class LibrarySection(PlexObject): """ Base class for a single library section. Attributes: - server (:class:`~plexapi.server.PlexServer`): Server this client is connected to. - initpath (str): Path requested when building this object. - agent (str): Unknown (com.plexapp.agents.imdb, etc) - allowSync (bool): True if you allow syncing content from this section. - art (str): Wallpaper artwork used to respresent this section. - composite (str): Composit image used to represent this section. - createdAt (datetime): Datetime this library section was created. + agent (str): The metadata agent used for the library section (com.plexapp.agents.imdb, etc). + allowSync (bool): True if you allow syncing content from the library section. + art (str): Background artwork used to respresent the library section. + composite (str): Composite image used to represent the library section. + createdAt (datetime): Datetime the library section was created. filters (str): Unknown key (str): Key (or ID) of this library section. language (str): Language represented in this section (en, xn, etc). - locations (str): Paths on disk where section content is stored. - refreshing (str): True if this section is currently being refreshed. + locations (List): List of folder paths added to the library section. + refreshing (bool): True if this section is currently being refreshed. scanner (str): Internal scanner used to find media (Plex Movie Scanner, Plex Premium Music Scanner, etc.) - thumb (str): Thumbnail image used to represent this section. - title (str): Title of this section. - type (str): Type of content section represents (movie, artist, photo, show). - updatedAt (datetime): Datetime this library section was last updated. - uuid (str): Unique id for this section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63) - totalSize (int): Total number of item in the library - + thumb (str): Thumbnail image used to represent the library section. + title (str): Name of the library section. + type (str): Type of content section represents (movie, show, artist, photo). + updatedAt (datetime): Datetime the library section was last updated. + uuid (str): Unique id for the section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63) """ def _loadData(self, data): @@ -391,6 +387,7 @@ def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, @property def totalSize(self): + """ Retruns the total number of item in the library. """ if self._total_size is None: part = '/library/sections/%s/all?X-Plex-Container-Start=0&X-Plex-Container-Size=1' % self.key data = self._server.query(part) From d433c0b3d4fce83064bb4c3af035c2b6e26e15b0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:40:08 -0800 Subject: [PATCH 09/27] Update locations doc strings for consistency --- plexapi/audio.py | 5 ++++- plexapi/photo.py | 5 ++++- plexapi/video.py | 11 ++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 98b926861..c4419d154 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -132,7 +132,7 @@ class Artist(Audio): countries (List<:class:`~plexapi.media.Country`>): List country objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. key (str): API URL (/library/metadata/). - locations (list): List of folder paths where the artist is found on disk. + locations (List): List of folder paths where the artist is found on disk. similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. """ @@ -377,6 +377,9 @@ def artist(self): def locations(self): """ This does not exist in plex xml response but is added to have a common interface to get the locations of the track. + + Retruns: + List of file paths where the track is found on disk. """ return [part.file for part in self.iterParts() if part] diff --git a/plexapi/photo.py b/plexapi/photo.py index fbc0ee22e..83428bb66 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -135,7 +135,7 @@ class Photo(PlexPartialObject, Playable): addedAt (datetime): Datetime the photo was added to the library. createdAtAccuracy (str): Unknown (local). createdAtTZOffset (int): Unknown (-25200). - fields (list): List of :class:`~plexapi.media.Field`. + fields (List<:class:`~plexapi.media.Field`>): List of field objects. guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn). index (sting): Plex index number for the photo. key (str): API URL (/library/metadata/). @@ -220,6 +220,9 @@ def section(self): def locations(self): """ This does not exist in plex xml response but is added to have a common interface to get the locations of the photo. + + Retruns: + List of file paths where the photo is found on disk. """ return [part.file for item in self.media for part in item.parts if part] diff --git a/plexapi/video.py b/plexapi/video.py index e89777514..33552ce5c 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -336,6 +336,9 @@ def actors(self): def locations(self): """ This does not exist in plex xml response but is added to have a common interface to get the locations of the movie. + + Retruns: + List of file paths where the movie is found on disk. """ return [part.file for part in self.iterParts() if part] @@ -387,7 +390,7 @@ class Show(Video): key (str): API URL (/library/metadata/). labels (List<:class:`~plexapi.media.Label`>): List of label objects. leafCount (int): Number of items in the show view. - locations (list): List of folder paths where the show is found on disk. + locations (List): List of folder paths where the show is found on disk. originallyAvailableAt (datetime): Datetime the show was released. rating (float): Show rating (7.9; 9.8; 8.1). roles (List<:class:`~plexapi.media.Role`>): List of role objects. @@ -754,6 +757,9 @@ def _prettyfilename(self): def locations(self): """ This does not exist in plex xml response but is added to have a common interface to get the locations of the episode. + + Retruns: + List of file paths where the episode is found on disk. """ return [part.file for part in self.iterParts() if part] @@ -832,5 +838,8 @@ def _loadData(self, data): def locations(self): """ This does not exist in plex xml response but is added to have a common interface to get the locations of the clip. + + Retruns: + List of file paths where the clip is found on disk. """ return [part.file for part in self.iterParts() if part] From 6f5cae2a7261dbd6720e1dbaf66d441ed9d3943b Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:48:58 -0800 Subject: [PATCH 10/27] More etag relacement with classes --- plexapi/photo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plexapi/photo.py b/plexapi/photo.py index 83428bb66..da430e0ea 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from urllib.parse import quote_plus -from plexapi import media, utils +from plexapi import media, utils, video from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound @@ -61,7 +61,7 @@ def _loadData(self, data): def albums(self, **kwargs): """ Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey - return self.fetchItems(key, etag='Directory', **kwargs) + return self.fetchItems(key, Photoalbum, **kwargs) def album(self, title): """ Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title. """ @@ -73,7 +73,7 @@ def album(self, title): def photos(self, **kwargs): """ Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey - return self.fetchItems(key, etag='Photo', **kwargs) + return self.fetchItems(key, Photo, **kwargs) def photo(self, title): """ Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """ @@ -85,7 +85,7 @@ def photo(self, title): def clips(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Clip` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey - return self.fetchItems(key, etag='Video', **kwargs) + return self.fetchItems(key, video.Clip, **kwargs) def clip(self, title): """ Returns the :class:`~plexapi.video.Clip` that matches the specified title. """ From 2acb75f86f3afb53ca76a5a23d7e2888efae48f8 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 21:38:48 -0800 Subject: [PATCH 11/27] Update all methods used to get an object's children --- plexapi/audio.py | 54 ++++++++++++++++++++----------- plexapi/library.py | 13 ++++++++ plexapi/photo.py | 46 ++++++++++++++++----------- plexapi/video.py | 79 ++++++++++++++++++++++++---------------------- 4 files changed, 119 insertions(+), 73 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index c4419d154..fb802fc9f 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -3,6 +3,7 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject +from plexapi.exceptions import BadRequest class Audio(PlexPartialObject): @@ -160,31 +161,40 @@ def album(self, title): Parameters: title (str): Title of the album to return. """ - key = '%s/children' % self.key - return self.fetchItem(key, title__iexact=title) + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItem(key, Album, title__iexact=title) def albums(self, **kwargs): """ Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """ - key = '%s/children' % self.key - return self.fetchItems(key, **kwargs) + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItems(key, Album, **kwargs) - def track(self, title): + def track(self, title=None, album=None, track=None): """ Returns the :class:`~plexapi.audio.Track` that matches the specified title. Parameters: title (str): Title of the track to return. + album (str): Album name (default: None; required if title not specified). + track (int): Track number (default: None; required if title not specified). + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing. """ - key = '%s/allLeaves' % self.key - return self.fetchItem(key, title__iexact=title) + key = '/library/metadata/%s/allLeaves' % self.ratingKey + if title: + return self.fetchItem(key, Track, title__iexact=title) + elif album is not None and track is not None: + return self.fetchItem(key, Track, parentTitle__iexact=album, index=track) + raise BadRequest('Missing argument: title or album and track are required') def tracks(self, **kwargs): """ Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """ - key = '%s/allLeaves' % self.key - return self.fetchItems(key, **kwargs) + key = '/library/section/%s/allLeaves' % self.ratingKey + return self.fetchItems(key, Track, **kwargs) - def get(self, title): + def get(self, title=None, album=None, track=None): """ Alias of :func:`~plexapi.audio.Artist.track`. """ - return self.track(title) + return self.track(title, album, track) def download(self, savepath=None, keep_original_name=False, **kwargs): """ Downloads all tracks for the artist to the specified location. @@ -259,23 +269,31 @@ def __iter__(self): for track in self.tracks(): yield track - def track(self, title): + def track(self, title=None, track=None): """ Returns the :class:`~plexapi.audio.Track` that matches the specified title. Parameters: title (str): Title of the track to return. + track (int): Track number (default: None; required if title not specified). + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing. """ - key = '%s/children' % self.key - return self.fetchItem(key, title__iexact=title) + key = '/library/metadata/%s/children' % self.ratingKey + if title: + return self.fetchItem(key, Track, title__iexact=title) + elif track: + return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track) + raise BadRequest('Missing argument: title or track is required') def tracks(self, **kwargs): """ Returns a list of :class:`~plexapi.audio.Track` objects in the album. """ - key = '%s/children' % self.key - return self.fetchItems(key, **kwargs) + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItems(key, Track, **kwargs) - def get(self, title): + def get(self, title=None, track=None): """ Alias of :func:`~plexapi.audio.Album.track`. """ - return self.track(title) + return self.track(title, track) def artist(self): """ Return the album's :class:`~plexapi.audio.Artist`. """ diff --git a/plexapi/library.py b/plexapi/library.py index 7254f3c83..ed6d59971 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1474,11 +1474,24 @@ def _loadData(self, data): def children(self): return self.fetchItems(self.key) + def item(self, title): + """ Returns the item in the collection that matches the specified title. + + Parameters: + title (str): Title of the item to return. + """ + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItem(key, title__iexact=title) + def items(self): """ Returns a list of all items in the collection. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key) + def get(self, title): + """ Alias to :func:`~plexapi.library.Collection.item`. """ + return self.item(title) + def __len__(self): return self.childCount diff --git a/plexapi/photo.py b/plexapi/photo.py index da430e0ea..cb95070db 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -58,41 +58,51 @@ def _loadData(self, data): self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) + def album(self, title): + """ Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title. + + Parameters: + title (str): Title of the photo album to return. + """ + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItem(key, Photoalbum, title__iexact=title) + def albums(self, **kwargs): """ Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, Photoalbum, **kwargs) - def album(self, title): - """ Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title. """ - for album in self.albums(): - if album.title.lower() == title.lower(): - return album - raise NotFound('Unable to find album: %s' % title) + def photo(self, title): + """ Returns the :class:`~plexapi.photo.Photo` that matches the specified title. + + Parameters: + title (str): Title of the photo to return. + """ + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItem(key, Photo, title__iexact=title) def photos(self, **kwargs): """ Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, Photo, **kwargs) - def photo(self, title): - """ Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """ - for photo in self.photos(): - if photo.title.lower() == title.lower(): - return photo - raise NotFound('Unable to find photo: %s' % title) + def clip(self, title): + """ Returns the :class:`~plexapi.video.Clip` that matches the specified title. + + Parameters: + title (str): Title of the clip to return. + """ + key = '/library/metadata/%s/children' % self.ratingKey + return self.fetchItem(key, video.Clip, title__iexact=title) def clips(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Clip` objects in the album. """ key = '/library/metadata/%s/children' % self.ratingKey return self.fetchItems(key, video.Clip, **kwargs) - def clip(self, title): - """ Returns the :class:`~plexapi.video.Clip` that matches the specified title. """ - for clip in self.clips(): - if clip.title.lower() == title.lower(): - return clip - raise NotFound('Unable to find clip: %s' % title) + def get(self, title): + """ Alias to :func:`~plexapi.photo.Photoalbum.photo`. """ + return self.episode(title) def iterParts(self): """ Iterates over the parts of the media item. """ diff --git a/plexapi/video.py b/plexapi/video.py index 33552ce5c..2290fe2c6 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -489,49 +489,55 @@ def onDeck(self): data = self._server.query(self._details_key) return self.findItems([item for item in data.iter('OnDeck')][0])[0] - def seasons(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Season` objects in the show. """ - key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey - return self.fetchItems(key, **kwargs) - - def season(self, title=None): + def season(self, title=None, season=None): """ Returns the season with the specified title or number. Parameters: - title (str or int): Title or Number of the season to return. + title (str): Title of the season to return. + season (int): Season number (default: None; required if title not specified). + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey - if isinstance(title, int): - return self.fetchItem(key, index__iexact=str(title)) - return self.fetchItem(key, title__iexact=title) + if title: + return self.fetchItem(key, Season, title__iexact=title) + elif season: + return self.fetchItem(key, Season, index=season) + raise BadRequest('Missing argument: title or season is required') - def episodes(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Episode` objects in the show. """ - key = '/library/metadata/%s/allLeaves' % self.ratingKey - return self.fetchItems(key, **kwargs) + def seasons(self, **kwargs): + """ Returns a list of :class:`~plexapi.video.Season` objects in the show. """ + key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey + return self.fetchItems(key, Season, **kwargs) def episode(self, title=None, season=None, episode=None): """ Find a episode using a title or season and episode. Parameters: title (str): Title of the episode to return - season (int): Season number (default:None; required if title not specified). - episode (int): Episode number (default:None; required if title not specified). + season (int): Season number (default: None; required if title not specified). + episode (int): Episode number (default: None; required if title not specified). Raises: - :exc:`~plexapi.exceptions.BadRequest`: If season and episode is missing. - :exc:`~plexapi.exceptions.NotFound`: If the episode is missing. + :exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing. """ + key = '/library/metadata/%s/allLeaves' % self.ratingKey if title: - key = '/library/metadata/%s/allLeaves' % self.ratingKey - return self.fetchItem(key, title__iexact=title) - elif season is not None and episode: - results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode] - if results: - return results[0] - raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode)) + return self.fetchItem(key, Episode, title__iexact=title) + elif season is not None and episode is not None: + return self.fetchItem(key, Episode, parentIndex=season, index=episode) raise BadRequest('Missing argument: title or season and episode are required') + def episodes(self, **kwargs): + """ Returns a list of :class:`~plexapi.video.Episode` objects in the show. """ + key = '/library/metadata/%s/allLeaves' % self.ratingKey + return self.fetchItems(key, Episode, **kwargs) + + def get(self, title=None, season=None, episode=None): + """ Alias to :func:`~plexapi.video.Show.episode`. """ + return self.episode(title, season, episode) + def watched(self): """ Returns list of watched :class:`~plexapi.video.Episode` objects. """ return self.episodes(viewCount__gt=0) @@ -540,10 +546,6 @@ def unwatched(self): """ Returns list of unwatched :class:`~plexapi.video.Episode` objects. """ return self.episodes(viewCount=0) - def get(self, title=None, season=None, episode=None): - """ Alias to :func:`~plexapi.video.Show.episode`. """ - return self.episode(title, season, episode) - def download(self, savepath=None, keep_original_name=False, **kwargs): """ Download video files to specified directory. @@ -621,21 +623,24 @@ def seasonNumber(self): def episodes(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Episode` objects in the season. """ key = '/library/metadata/%s/children' % self.ratingKey - return self.fetchItems(key, **kwargs) + return self.fetchItems(key, Episode, **kwargs) def episode(self, title=None, episode=None): """ Returns the episode with the given title or number. Parameters: title (str): Title of the episode to return. - episode (int): Episode number (default:None; required if title not specified). + episode (int): Episode number (default: None; required if title not specified). + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing. """ - if not title and not episode: - raise BadRequest('Missing argument, you need to use title or episode.') key = '/library/metadata/%s/children' % self.ratingKey if title: - return self.fetchItem(key, title=title) - return self.fetchItem(key, parentIndex=self.index, index=episode) + return self.fetchItem(key, Episode, title__iexact=title) + elif episode: + return self.fetchItem(key, Episode, parentIndex=self.index, index=episode) + raise BadRequest('Missing argument: title or episode is required') def get(self, title=None, episode=None): """ Alias to :func:`~plexapi.video.Season.episode`. """ @@ -643,7 +648,7 @@ def get(self, title=None, episode=None): def show(self): """ Return the season's :class:`~plexapi.video.Show`. """ - return self.fetchItem(int(self.parentRatingKey)) + return self.fetchItem(self.parentRatingKey) def watched(self): """ Returns list of watched :class:`~plexapi.video.Episode` objects. """ @@ -788,7 +793,7 @@ def season(self): def show(self): """" Return the episode's :class:`~plexapi.video.Show`. """ - return self.fetchItem(int(self.grandparentRatingKey)) + return self.fetchItem(self.grandparentRatingKey) def _defaultSyncTitle(self): """ Returns str, default title for a new syncItem. """ From 8206dfbade7c9c6197cfdd0876eaf4cd49375073 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:00:00 -0800 Subject: [PATCH 12/27] Fix flake8 --- plexapi/photo.py | 2 +- plexapi/video.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/photo.py b/plexapi/photo.py index cb95070db..4ad915541 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -3,7 +3,7 @@ from plexapi import media, utils, video from plexapi.base import Playable, PlexPartialObject -from plexapi.exceptions import BadRequest, NotFound +from plexapi.exceptions import BadRequest @utils.registerPlexObject diff --git a/plexapi/video.py b/plexapi/video.py index 2290fe2c6..d7f951130 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -504,7 +504,7 @@ def season(self, title=None, season=None): return self.fetchItem(key, Season, title__iexact=title) elif season: return self.fetchItem(key, Season, index=season) - raise BadRequest('Missing argument: title or season is required') + raise BadRequest('Missing argument: title or season is required') def seasons(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Season` objects in the show. """ From 3430c245e2dc3e5e4800eff35f311ebe609891c9 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:17:43 -0800 Subject: [PATCH 13/27] Make sure key defaults to blank string so fix #50 works --- plexapi/audio.py | 2 +- plexapi/library.py | 3 +-- plexapi/photo.py | 4 ++-- plexapi/video.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index fb802fc9f..3d2ba5158 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -47,7 +47,7 @@ def _loadData(self, data): self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) - 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.librarySectionKey = data.attrib.get('librarySectionKey') diff --git a/plexapi/library.py b/plexapi/library.py index ed6d59971..8768e2ef9 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1435,7 +1435,6 @@ class Collections(PlexPartialObject): titleSort (str): Title to use when sorting (defaults to title). type (str): 'collection' updatedAt (datatime): Datetime the collection was updated. - """ TAG = 'Directory' @@ -1452,7 +1451,7 @@ def _loadData(self, data): self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) - self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50 + self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50 self.labels = self.findItems(data, media.Label) self.librarySectionID = data.attrib.get('librarySectionID') self.librarySectionKey = data.attrib.get('librarySectionKey') diff --git a/plexapi/photo.py b/plexapi/photo.py index 4ad915541..1e11b1dc8 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -44,7 +44,7 @@ def _loadData(self, data): self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) - self.key = data.attrib.get('key') + self.key = data.attrib.get('key'. '') self.librarySectionID = data.attrib.get('librarySectionID') self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') @@ -184,7 +184,7 @@ def _loadData(self, data): self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) - self.key = data.attrib.get('key') + self.key = data.attrib.get('key', '') self.librarySectionID = data.attrib.get('librarySectionID') self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') diff --git a/plexapi/video.py b/plexapi/video.py index d7f951130..91d3c14ac 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -43,7 +43,7 @@ def _loadData(self, data): self.artBlurHash = data.attrib.get('artBlurHash') self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') - 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.librarySectionKey = data.attrib.get('librarySectionKey') From f41be90a3a23e7383c992009e3a6da257174b53b Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:24:46 -0800 Subject: [PATCH 14/27] More doc string clean up --- plexapi/audio.py | 10 +++++----- plexapi/photo.py | 4 ++-- plexapi/video.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 3d2ba5158..4ce76af32 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -7,8 +7,8 @@ class Audio(PlexPartialObject): - """ Base class for audio :class:`~plexapi.audio.Artist`, :class:`~plexapi.audio.Album` - and :class:`~plexapi.audio.Track` objects. + """ Base class for all audio objects including :class:`~plexapi.audio.Artist`, + :class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`. Attributes: addedAt (datetime): Datetime the item was added to the library. @@ -124,7 +124,7 @@ def sync(self, bitrate, client=None, clientId=None, limit=None, title=None): @utils.registerPlexObject class Artist(Audio): - """ Represents a single audio artist. + """ Represents a single Artist. Attributes: TAG (str): 'Directory' @@ -218,7 +218,7 @@ def download(self, savepath=None, keep_original_name=False, **kwargs): @utils.registerPlexObject class Album(Audio): - """ Represents a single audio album. + """ Represents a single Album. Attributes: TAG (str): 'Directory' @@ -324,7 +324,7 @@ def _defaultSyncTitle(self): @utils.registerPlexObject class Track(Audio, Playable): - """ Represents a single audio track. + """ Represents a single Track. Attributes: TAG (str): 'Directory' diff --git a/plexapi/photo.py b/plexapi/photo.py index 1e11b1dc8..f0932a1ab 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -8,7 +8,7 @@ @utils.registerPlexObject class Photoalbum(PlexPartialObject): - """ Represents a photoalbum (collection of photos). + """ Represents a single Photoalbum (collection of photos). Attributes: TAG (str): 'Directory' @@ -137,7 +137,7 @@ def download(self, savepath=None, keep_original_name=False, showstatus=False): @utils.registerPlexObject class Photo(PlexPartialObject, Playable): - """ Represents a single photo. + """ Represents a single Photo. Attributes: TAG (str): 'Photo' diff --git a/plexapi/video.py b/plexapi/video.py index 91d3c14ac..231056554 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -10,7 +10,7 @@ class Video(PlexPartialObject): """ Base class for all video objects including :class:`~plexapi.video.Movie`, :class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`, - :class:`~plexapi.video.Episode`, :class:`~plexapi.video.Clip`. + :class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`. Attributes: addedAt (datetime): Datetime the item was added to the library. From b7f813aacd60214709158a50855e51266c8e4d86 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:25:10 -0800 Subject: [PATCH 15/27] Add Playlist attributes doc string --- plexapi/playlist.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index cefbaabb1..a203b4fb0 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -11,8 +11,26 @@ @utils.registerPlexObject class Playlist(PlexPartialObject, Playable): - """ Represents a single Playlist object. - # TODO: Document attributes + """ Represents a single Playlist. + + Attributes: + TAG (str): 'Playlist' + TYPE (str): 'playlist' + addedAt (datetime): Datetime the playlist was added to the server. + allowSync (bool): True if you allow syncing playlists. + composite (str): URL to composite image (/playlist//composite/) + duration (int): Duration of the playlist in milliseconds. + durationInSeconds (int): Duration of the playlist in seconds. + guid (str): Plex GUID for the playlist (com.plexapp.agents.none://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). + key (str): API URL (/playlist/). + leafCount (int): Number of items in the playlist view. + playlistType (str): 'audio', 'video', or 'photo' + ratingKey (int): Unique key identifying the playlist. + smart (bool): True if the playlist is a smart playlist. + summary (str): Summary of the playlist. + title (str): Name of the playlist. + type (str): 'playlist' + updatedAt (datatime): Datetime the playlist was updated. """ TAG = 'Playlist' TYPE = 'playlist' @@ -21,12 +39,12 @@ def _loadData(self, data): """ Load attribute values from Plex XML response. """ Playable._loadData(self, data) self.addedAt = toDatetime(data.attrib.get('addedAt')) + self.allowSync = cast(bool, data.attrib.get('allowSync')) self.composite = data.attrib.get('composite') # url to thumbnail self.duration = cast(int, data.attrib.get('duration')) self.durationInSeconds = cast(int, data.attrib.get('durationInSeconds')) self.guid = data.attrib.get('guid') - self.key = data.attrib.get('key') - self.key = self.key.replace('/items', '') if self.key else self.key # FIX_BUG_50 + self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50 self.leafCount = cast(int, data.attrib.get('leafCount')) self.playlistType = data.attrib.get('playlistType') self.ratingKey = cast(int, data.attrib.get('ratingKey')) @@ -35,7 +53,6 @@ def _loadData(self, data): self.title = data.attrib.get('title') self.type = data.attrib.get('type') self.updatedAt = toDatetime(data.attrib.get('updatedAt')) - self.allowSync = cast(bool, data.attrib.get('allowSync')) self._items = None # cache for self.items def __len__(self): # pragma: no cover From 949d37bc2f06ff64680b144507e5fb49b97f27a7 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:29:26 -0800 Subject: [PATCH 16/27] Update listAttrs doc string --- plexapi/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plexapi/base.py b/plexapi/base.py index ec8886249..0c845bc28 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -227,6 +227,7 @@ def firstAttr(self, *attrs): return value def listAttrs(self, data, attr, **kwargs): + """ Return a list of values from matching attribute. """ results = [] for elem in data: kwargs['%s__exists' % attr] = True From c1a1d1616bb37ff346a38d0f10da93d3b87a9740 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:32:48 -0800 Subject: [PATCH 17/27] Add Playlist.item() method --- plexapi/playlist.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index a203b4fb0..9208a3abf 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -3,7 +3,7 @@ from plexapi import utils from plexapi.base import Playable, PlexPartialObject -from plexapi.exceptions import BadRequest, Unsupported +from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.library import LibrarySection from plexapi.playqueue import PlayQueue from plexapi.utils import cast, toDatetime @@ -91,14 +91,29 @@ def __contains__(self, other): # pragma: no cover def __getitem__(self, key): # pragma: no cover return self.items()[key] + def item(self, title): + """ Returns the item in the playlist that matches the specified title. + + Parameters: + title (str): Title of the item to return. + """ + for item in self.items(): + if item.title.lower() == title: + return item + raise NotFound('Item with title "%s" not found in the playlist') + def items(self): """ Returns a list of all items in the playlist. """ if self._items is None: - key = '%s/items' % self.key + key = '/playlist/%s/items' % self.ratingKey items = self.fetchItems(key) self._items = items return self._items + def get(self, title): + """ Alias to :func:`~plexapi.playlist.Playlist.item`. """ + return self.item(title) + def addItems(self, items): """ Add items to a playlist. """ if not isinstance(items, (list, tuple)): From 33b725d8d4de0d2abcf291ae77c95dfb19e18f2f Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 23:03:08 -0800 Subject: [PATCH 18/27] Update tests --- tests/test_audio.py | 7 +++++++ tests/test_library.py | 14 +++++++++++--- tests/test_playlist.py | 14 ++++++++++++++ tests/test_video.py | 37 ++++++++++++++++--------------------- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/tests/test_audio.py b/tests/test_audio.py index 026ee5589..4bd85168f 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -43,6 +43,9 @@ def test_audio_Artist_history(artist): def test_audio_Artist_track(artist): track = artist.track("As Colourful as Ever") assert track.title == "As Colourful as Ever" + track = artist.track(album="Layers", track=1) + assert track.parentTitle == "Layers" + assert track.index == 1 def test_audio_Artist_tracks(artist): @@ -135,6 +138,8 @@ def test_audio_Album_tracks(album): def test_audio_Album_track(album, track=None): # this is not reloaded. its not that much info missing. track = track or album.track("As Colourful As Ever") + track2 = album.track(track=1) + assert track == track2 assert utils.is_datetime(track.addedAt) assert utils.is_int(track.duration) assert utils.is_metadata(track.grandparentKey) @@ -225,6 +230,8 @@ def test_audio_Track_attrs(album): assert utils.is_datetime(track.lastViewedAt) assert utils.is_int(track.librarySectionID) assert track.listType == "audio" + assert len(track.locations) == 1 + assert len(track.locations[0]) >= 10 # Assign 0 track.media media = track.media[0] assert track.moods == [] diff --git a/tests/test_library.py b/tests/test_library.py index 6866de6c4..d1b2d883d 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -300,9 +300,17 @@ def test_library_Collection_delete(movies, movie): assert len(collections) == 0 -def test_library_Collection_children(collection): - children = collection.items() - assert len(children) == 1 +def test_library_Collection_item(collection): + item1 = collection.item("Elephants Dream") + assert item1.title == "Elephants Dream" + item2 = collection.get("Elephants Dream") + assert item2.title == "Elephants Dream" + assert item1 == item2 + + +def test_library_Collection_items(collection): + items = collection.items() + assert len(items) == 1 def test_search_with_weird_a(plex): diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 6447fd94f..672b166f2 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -50,6 +50,20 @@ def test_create_playlist(plex, show): playlist.delete() +def test_playlist_item(plex, show): + title = 'test_create_playlist_item_show' + episodes = show.episodes() + try: + playlist = plex.createPlaylist(title, episodes[:3]) + item1 = playlist.item("Winter Is Coming") + assert item1 in playlist.items() + item2 = playlist.get("Winter Is Coming") + assert item2 in playlist.items() + assert item1 == item2 + finally: + playlist.delete() + + @pytest.mark.client def test_playlist_play(plex, client, artist, album): try: diff --git a/tests/test_video.py b/tests/test_video.py index d1ba79ecd..86a69ae76 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -145,6 +145,7 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle): def test_video_Movie_attrs(movies): movie = movies.get("Sita Sings the Blues") + assert len(movie.locations) == 1 assert len(movie.locations[0]) >= 10 assert utils.is_datetime(movie.addedAt) assert utils.is_metadata(movie.art) @@ -498,6 +499,7 @@ def test_video_Show_attrs(show): assert utils.is_datetime(show.lastViewedAt) assert utils.is_int(show.leafCount) assert show.listType == "video" + assert len(show.locations) == 1 assert len(show.locations[0]) >= 10 assert utils.is_datetime(show.originallyAvailableAt) assert show.rating >= 8.0 @@ -551,13 +553,6 @@ def test_video_Show_unwatched(tvshows): assert len(unwatched) == len(episodes) - 1 -def test_video_Show_location(plex): - # This should be a part of test test_video_Show_attrs but is excluded - # because of https://github.com/mjs7231/python-plexapi/issues/97 - show = plex.library.section("TV Shows").get("The 100") - assert len(show.locations) >= 1 - - def test_video_Show_settings(show): preferences = show.preferences() assert len(preferences) >= 1 @@ -715,6 +710,8 @@ def test_video_Episode_attrs(episode): ) assert episode.year == 2011 assert episode.isWatched in [True, False] + assert len(episode.locations) == 1 + assert len(episode.locations[0]) >= 10 # Media media = episode.media[0] assert media.aspectRatio == 1.78 @@ -752,6 +749,7 @@ def test_video_Season(show): assert len(seasons) == 2 assert ["Season 1", "Season 2"] == [s.title for s in seasons[:2]] assert show.season("Season 1") == seasons[0] + assert show.season(season=1) == seasons[0] def test_video_Season_history(show): @@ -794,38 +792,35 @@ def test_video_Season_show(show): assert season.ratingKey == season_by_name.ratingKey -def test_video_Season_watched(tvshows): - show = tvshows.get("Game of Thrones") - season = show.season(1) - sne = show.season("Season 1") - assert season == sne +def test_video_Season_watched(show): + season = show.season("Season 1") season.markWatched() assert season.isWatched -def test_video_Season_unwatched(tvshows): - season = tvshows.get("Game of Thrones").season(1) +def test_video_Season_unwatched(show): + season = show.season("Season 1") season.markUnwatched() assert not season.isWatched def test_video_Season_get(show): - episode = show.season(1).get("Winter Is Coming") + episode = show.season("Season 1").get("Winter Is Coming") assert episode.title == "Winter Is Coming" def test_video_Season_episode(show): - episode = show.season(1).get("Winter Is Coming") + episode = show.season("Season ").get("Winter Is Coming") assert episode.title == "Winter Is Coming" def test_video_Season_episode_by_index(show): - episode = show.season(1).episode(episode=1) + episode = show.season("Season 1").episode(episode=1) assert episode.index == 1 def test_video_Season_episodes(show): - episodes = show.season(2).episodes() + episodes = show.season("Season 2").episodes() assert len(episodes) >= 1 @@ -859,9 +854,9 @@ def test_that_reload_return_the_same_object(plex): == tvshow_section_get_key == tvshow_section_get.reload().key ) # noqa - season_library_search = tvshow_library_search.season(1) - season_search = tvshow_search.season(1) - season_section_get = tvshow_section_get.season(1) + season_library_search = tvshow_library_search.season("Season 1") + season_search = tvshow_search.season("Season 1") + season_section_get = tvshow_section_get.season("Season 1") season_library_search_key = season_library_search.key season_search_key = season_search.key season_section_get_key = season_section_get.key From 5df75728b3e47fdddcfaad10ff597d879c7b87d7 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 23:05:08 -0800 Subject: [PATCH 19/27] Fix flake8 --- plexapi/photo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/photo.py b/plexapi/photo.py index f0932a1ab..49d640bc2 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -44,7 +44,7 @@ def _loadData(self, data): self.fields = self.findItems(data, media.Field) self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) - self.key = data.attrib.get('key'. '') + self.key = data.attrib.get('key', '') self.librarySectionID = data.attrib.get('librarySectionID') self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') From 9916297070dd5163cff98af64b5a77c209bbebfc Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 23 Dec 2020 23:12:50 -0800 Subject: [PATCH 20/27] Fix tests --- tests/test_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_video.py b/tests/test_video.py index 86a69ae76..284570c50 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -810,7 +810,7 @@ def test_video_Season_get(show): def test_video_Season_episode(show): - episode = show.season("Season ").get("Winter Is Coming") + episode = show.season("Season 1").get("Winter Is Coming") assert episode.title == "Winter Is Coming" From 1ce97102cdac1487c3122e77205965a935951db9 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 24 Dec 2020 09:08:52 -0800 Subject: [PATCH 21/27] Fix typo in library totalSize doc string --- plexapi/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/library.py b/plexapi/library.py index 8768e2ef9..76602b232 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -387,7 +387,7 @@ def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, @property def totalSize(self): - """ Retruns the total number of item in the library. """ + """ Returns the total number of items in the library. """ if self._total_size is None: part = '/library/sections/%s/all?X-Plex-Container-Start=0&X-Plex-Container-Size=1' % self.key data = self._server.query(part) From 478bf9e08347dc87409bd9634f159e10f639e409 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 24 Dec 2020 09:21:02 -0800 Subject: [PATCH 22/27] Fix video season index test --- tests/test_video.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_video.py b/tests/test_video.py index 284570c50..c40fefa88 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -749,7 +749,6 @@ def test_video_Season(show): assert len(seasons) == 2 assert ["Season 1", "Season 2"] == [s.title for s in seasons[:2]] assert show.season("Season 1") == seasons[0] - assert show.season(season=1) == seasons[0] def test_video_Season_history(show): @@ -815,7 +814,7 @@ def test_video_Season_episode(show): def test_video_Season_episode_by_index(show): - episode = show.season("Season 1").episode(episode=1) + episode = show.season(season=1).episode(episode=1) assert episode.index == 1 From 2765bee2b394c3d3d0cf1aa80ed3e8ba0a6cc2be Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 24 Dec 2020 09:21:17 -0800 Subject: [PATCH 23/27] Fix audio tracks key --- plexapi/audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 4ce76af32..d1173be2a 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -189,7 +189,7 @@ def track(self, title=None, album=None, track=None): def tracks(self, **kwargs): """ Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """ - key = '/library/section/%s/allLeaves' % self.ratingKey + key = '/library/metadata/%s/allLeaves' % self.ratingKey return self.fetchItems(key, Track, **kwargs) def get(self, title=None, album=None, track=None): From 810c5566c30a1bc01cf13d7928528899e05d7d5c Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 24 Dec 2020 09:21:29 -0800 Subject: [PATCH 24/27] Fix playlist tests --- plexapi/playlist.py | 6 +++--- tests/test_playlist.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 9208a3abf..847a56fc7 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -98,14 +98,14 @@ def item(self, title): title (str): Title of the item to return. """ for item in self.items(): - if item.title.lower() == title: + if item.title.lower() == title.lower(): return item - raise NotFound('Item with title "%s" not found in the playlist') + raise NotFound('Item with title "%s" not found in the playlist' % title) def items(self): """ Returns a list of all items in the playlist. """ if self._items is None: - key = '/playlist/%s/items' % self.ratingKey + key = '/playlists/%s/items' % self.ratingKey items = self.fetchItems(key) self._items = items return self._items diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 672b166f2..a08b7dd6a 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -2,6 +2,7 @@ import time import pytest +from plexapi.exceptions import NotFound def test_create_playlist(plex, show): @@ -51,7 +52,7 @@ def test_create_playlist(plex, show): def test_playlist_item(plex, show): - title = 'test_create_playlist_item_show' + title = 'test_playlist_item' episodes = show.episodes() try: playlist = plex.createPlaylist(title, episodes[:3]) @@ -60,6 +61,8 @@ def test_playlist_item(plex, show): item2 = playlist.get("Winter Is Coming") assert item2 in playlist.items() assert item1 == item2 + with pytest.raises(NotFound): + playlist.item("Does not exist") finally: playlist.delete() From f2e7e891cbf4e022a2d44259ee63fb237970bb59 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Wed, 30 Dec 2020 23:58:01 +0100 Subject: [PATCH 25/27] Fix a test in navigation keep compat for season(int) --- plexapi/video.py | 9 +++--- tests/test_myplex.py | 59 ++++++++++++++++++++-------------------- tests/test_navigation.py | 1 + 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/plexapi/video.py b/plexapi/video.py index 231056554..109f626e3 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -2,7 +2,7 @@ import os from urllib.parse import quote_plus, urlencode -from plexapi import media, utils, settings, library +from plexapi import library, media, settings, utils from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound @@ -500,10 +500,11 @@ def season(self, title=None, season=None): :exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey - if title: + if title and not isinstance(title, int): return self.fetchItem(key, Season, title__iexact=title) - elif season: - return self.fetchItem(key, Season, index=season) + elif season or isinstance(title, int): + idx = season or title + return self.fetchItem(key, Season, index=idx) raise BadRequest('Missing argument: title or season is required') def seasons(self, **kwargs): diff --git a/tests/test_myplex.py b/tests/test_myplex.py index cc2f99f6a..3c9555400 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -129,20 +129,19 @@ def test_myplex_inviteFriend_remove(account, plex, mocker): secs = plex.library.sections() ids = account._getSectionIds(plex.machineIdentifier, secs) - with mocker.patch.object(account, "_getSectionIds", return_value=ids): - with utils.callable_http_patch(): - - account.inviteFriend( - inv_user, - plex, - secs, - allowSync=True, - allowCameraUpload=True, - allowChannels=False, - filterMovies=vid_filter, - filterTelevision=vid_filter, - filterMusic={"label": ["foo"]}, - ) + mocker.patch.object(account, "_getSectionIds", return_value=ids) + with utils.callable_http_patch(): + account.inviteFriend( + inv_user, + plex, + secs, + allowSync=True, + allowCameraUpload=True, + allowChannels=False, + filterMovies=vid_filter, + filterTelevision=vid_filter, + filterMusic={"label": ["foo"]}, + ) assert inv_user not in [u.title for u in account.users()] @@ -157,22 +156,22 @@ def test_myplex_updateFriend(account, plex, mocker, shared_username): user = account.user(shared_username) ids = account._getSectionIds(plex.machineIdentifier, secs) - with mocker.patch.object(account, "_getSectionIds", return_value=ids): - with mocker.patch.object(account, "user", return_value=user): - with utils.callable_http_patch(): - - account.updateFriend( - shared_username, - plex, - secs, - allowSync=True, - removeSections=True, - allowCameraUpload=True, - allowChannels=False, - filterMovies=vid_filter, - filterTelevision=vid_filter, - filterMusic={"label": ["foo"]}, - ) + mocker.patch.object(account, "_getSectionIds", return_value=ids) + mocker.patch.object(account, "user", return_value=user) + with utils.callable_http_patch(): + + account.updateFriend( + shared_username, + plex, + secs, + allowSync=True, + removeSections=True, + allowCameraUpload=True, + allowChannels=False, + filterMovies=vid_filter, + filterTelevision=vid_filter, + filterMusic={"label": ["foo"]}, + ) def test_myplex_createExistingUser(account, plex, shared_username): diff --git a/tests/test_navigation.py b/tests/test_navigation.py index 00f2f083d..a9be6adfe 100644 --- a/tests/test_navigation.py +++ b/tests/test_navigation.py @@ -9,6 +9,7 @@ def test_navigate_around_show(account, plex): episode = show.episode("Pilot") assert "Season 1" in [s.title for s in seasons], "Unable to list season:" assert "Pilot" in [e.title for e in episodes], "Unable to list episode:" + assert show.season(season=1) == season assert show.season(1) == season assert show.episode("Pilot") == episode, "Unable to get show episode:" assert season.episode("Pilot") == episode, "Unable to get season episode:" From 3526b8cdebc767ca234d88dd2e990bbd9d597ec5 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Thu, 31 Dec 2020 00:35:57 +0100 Subject: [PATCH 26/27] Allow specials. --- plexapi/video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/video.py b/plexapi/video.py index 109f626e3..769819cb6 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -500,9 +500,9 @@ def season(self, title=None, season=None): :exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey - if title and not isinstance(title, int): + if title is not None and not isinstance(title, int): return self.fetchItem(key, Season, title__iexact=title) - elif season or isinstance(title, int): + elif season is not None or isinstance(title, int): idx = season or title return self.fetchItem(key, Season, index=idx) raise BadRequest('Missing argument: title or season is required') From ebdaedeba7b620194c69b45a9502b34a7a669125 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 30 Dec 2020 15:49:26 -0800 Subject: [PATCH 27/27] Fix getting season=0 or episode=0 --- plexapi/audio.py | 6 +++--- plexapi/video.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index d1173be2a..a9829a87a 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -181,7 +181,7 @@ def track(self, title=None, album=None, track=None): :exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing. """ key = '/library/metadata/%s/allLeaves' % self.ratingKey - if title: + if title is not None: return self.fetchItem(key, Track, title__iexact=title) elif album is not None and track is not None: return self.fetchItem(key, Track, parentTitle__iexact=album, index=track) @@ -280,9 +280,9 @@ def track(self, title=None, track=None): :exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey - if title: + if title is not None: return self.fetchItem(key, Track, title__iexact=title) - elif track: + elif track is not None: return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track) raise BadRequest('Missing argument: title or track is required') diff --git a/plexapi/video.py b/plexapi/video.py index 769819cb6..f14479103 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -503,8 +503,11 @@ def season(self, title=None, season=None): if title is not None and not isinstance(title, int): return self.fetchItem(key, Season, title__iexact=title) elif season is not None or isinstance(title, int): - idx = season or title - return self.fetchItem(key, Season, index=idx) + if isinstance(title, int): + index = title + else: + index = season + return self.fetchItem(key, Season, index=index) raise BadRequest('Missing argument: title or season is required') def seasons(self, **kwargs): @@ -524,7 +527,7 @@ def episode(self, title=None, season=None, episode=None): :exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing. """ key = '/library/metadata/%s/allLeaves' % self.ratingKey - if title: + if title is not None: return self.fetchItem(key, Episode, title__iexact=title) elif season is not None and episode is not None: return self.fetchItem(key, Episode, parentIndex=season, index=episode) @@ -637,9 +640,9 @@ def episode(self, title=None, episode=None): :exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey - if title: + if title is not None: return self.fetchItem(key, Episode, title__iexact=title) - elif episode: + elif episode is not None: return self.fetchItem(key, Episode, parentIndex=self.index, index=episode) raise BadRequest('Missing argument: title or episode is required')