diff --git a/plexapi/audio.py b/plexapi/audio.py index 08742884f..a9829a87a 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -3,31 +3,37 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject +from plexapi.exceptions import BadRequest 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 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 +41,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.key = data.attrib.get('key') + 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.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 +62,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 +78,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): @@ -113,17 +124,18 @@ 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' 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 +143,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(): @@ -149,34 +161,43 @@ 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 this artist. """ - key = '%s/children' % self.key - return self.fetchItems(key, **kwargs) + """ Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """ + 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 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) + 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 this artist. """ - key = '%s/allLeaves' % self.key - return self.fetchItems(key, **kwargs) + """ Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """ + key = '/library/metadata/%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 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. @@ -197,67 +218,89 @@ 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' 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 track(self, title): + def __iter__(self): + for track in self.tracks(): + yield track + + 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 is not None: + return self.fetchItem(key, Track, title__iexact=title) + 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') def tracks(self, **kwargs): - """ Returns a list of :class:`~plexapi.audio.Track` objects in this album. """ - key = '%s/children' % self.key - return self.fetchItems(key, **kwargs) + """ Returns a list of :class:`~plexapi.audio.Track` objects in the album. """ + 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 :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. @@ -281,37 +324,32 @@ def _defaultSyncTitle(self): @utils.registerPlexObject class Track(Audio, Playable): - """ Represents a single audio track. + """ Represents a single Track. 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 +361,46 @@ 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. + + Retruns: + List of file paths where the track is found on disk. + """ + 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) 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 diff --git a/plexapi/library.py b/plexapi/library.py index 56b822f05..9e86d9bbc 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 OPERATORS, PlexObject, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound -from plexapi.media import MediaTag from plexapi.settings import Setting from plexapi.utils import deprecated @@ -313,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): @@ -392,6 +387,7 @@ def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, @property def totalSize(self): + """ 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) @@ -728,7 +724,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 @@ -753,7 +749,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): @@ -1416,41 +1412,38 @@ 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. """ TAG = 'Directory' 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') @@ -1458,22 +1451,23 @@ 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.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') 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')) @@ -1482,11 +1476,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/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. diff --git a/plexapi/photo.py b/plexapi/photo.py index 301ec319f..49d640bc2 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -1,81 +1,111 @@ # -*- 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 +from plexapi.exceptions import BadRequest @utils.registerPlexObject class Photoalbum(PlexPartialObject): - """ Represents a photoalbum (collection of photos). + """ Represents a single Photoalbum (collection of photos). 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') - 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') + 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 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 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) + 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 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) + 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 get(self, title): + """ Alias to :func:`~plexapi.photo.Photoalbum.photo`. """ + return self.episode(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(): @@ -107,28 +137,39 @@ 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' TYPE (str): 'photo' - addedAt (datetime): Datetime this item was added to the library. - fields (list): List of :class:`~plexapi.media.Field`. - index (sting): Index number of this photo. + addedAt (datetime): Datetime the photo was added to the library. + createdAtAccuracy (str): Unknown (local). + createdAtTZOffset (int): Unknown (-25200). + 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/). 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 +178,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.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 +214,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 +226,18 @@ 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. + + 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] + 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 diff --git a/plexapi/playlist.py b/plexapi/playlist.py index cefbaabb1..847a56fc7 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 @@ -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 @@ -74,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.lower(): + return item + 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 = '%s/items' % self.key + key = '/playlists/%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)): diff --git a/plexapi/video.py b/plexapi/video.py index 4f04f479e..f14479103 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 @@ -10,39 +10,45 @@ 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 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.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')) 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,10 @@ 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. + + Retruns: + List of file paths where the movie is found on disk. """ return [part.file for part in self.iterParts() if part] @@ -378,60 +380,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 +438,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): @@ -491,49 +489,59 @@ 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. """ - 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, etag='Directory', index__iexact=str(title)) - return self.fetchItem(key, etag='Directory', title__iexact=title) + 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): + 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 episodes(self, **kwargs): - """ Returns a list of :class:`~plexapi.video.Episode` objects. """ - 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. """ - 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)) + key = '/library/metadata/%s/allLeaves' % self.ratingKey + 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) 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) @@ -542,10 +550,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. @@ -568,33 +572,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 +616,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,31 +625,34 @@ 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) + 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) + if title is not None: + return self.fetchItem(key, Episode, title__iexact=title) + elif episode is not None: + 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`. """ return self.episode(title, episode) def show(self): - """ Return this seasons :func:`~plexapi.video.Show`.. """ - return self.fetchItem(int(self.parentRatingKey)) + """ Return the season's :class:`~plexapi.video.Show`. """ + return self.fetchItem(self.parentRatingKey) def watched(self): """ Returns list of watched :class:`~plexapi.video.Episode` objects. """ @@ -673,31 +688,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 +724,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 +765,16 @@ 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. + + Retruns: + List of file paths where the episode is found on disk. """ 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,18 +786,18 @@ 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 self.fetchItem(int(self.grandparentRatingKey)) + """" Return the episode's :class:`~plexapi.video.Show`. """ + return self.fetchItem(self.grandparentRatingKey) def _defaultSyncTitle(self): """ Returns str, default title for a new syncItem. """ @@ -792,16 +808,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 +831,24 @@ 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. + + Retruns: + List of file paths where the clip is found on disk. + """ + return [part.file for part in self.iterParts() if part] diff --git a/tests/test_audio.py b/tests/test_audio.py index ac148c131..4bd85168f 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) @@ -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): @@ -63,7 +66,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 +109,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" @@ -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 e2571211b..83404f8f3 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_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:" diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 6447fd94f..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): @@ -50,6 +51,22 @@ def test_create_playlist(plex, show): playlist.delete() +def test_playlist_item(plex, show): + title = 'test_playlist_item' + 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 + with pytest.raises(NotFound): + playlist.item("Does not exist") + 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..c40fefa88 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 @@ -794,38 +791,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 1").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 +853,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