diff --git a/lib/plexapi/alert.py b/lib/plexapi/alert.py index 1a5469ab..bf6e5394 100644 --- a/lib/plexapi/alert.py +++ b/lib/plexapi/alert.py @@ -54,7 +54,7 @@ class AlertListener(threading.Thread): def stop(self): """ Stop the AlertListener thread. Once the notifier is stopped, it cannot be directly - started again. You must call :func:`plexapi.server.PlexServer.startAlertListener()` + started again. You must call :func:`~plexapi.server.PlexServer.startAlertListener` from a PlexServer instance. """ log.info('Stopping AlertListener.') diff --git a/lib/plexapi/audio.py b/lib/plexapi/audio.py index d410da29..4be482f2 100644 --- a/lib/plexapi/audio.py +++ b/lib/plexapi/audio.py @@ -10,6 +10,8 @@ class Audio(PlexPartialObject): Attributes: addedAt (datetime): Datetime this item was added to the library. + art (str): URL to artwork image. + artBlurHash (str): BlurHash string for artwork image. index (sting): Index Number (often the track number). key (str): API URL (/library/metadata/). lastViewedAt (datetime): Datetime item was last accessed. @@ -18,6 +20,7 @@ class Audio(PlexPartialObject): ratingKey (int): Unique key identifying this item. summary (str): Summary of the artist, track, or album. thumb (str): URL to thumbnail image. + thumbBlurHash (str): BlurHash string for thumbnail image. title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.) titleSort (str): Title to use when sorting (defaults to title). type (str): 'artist', 'album', or 'track'. @@ -32,6 +35,8 @@ class Audio(PlexPartialObject): 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.index = data.attrib.get('index') self.key = data.attrib.get('key') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) @@ -41,6 +46,7 @@ class Audio(PlexPartialObject): self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) 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.title) self.type = data.attrib.get('type') @@ -69,20 +75,20 @@ class Audio(PlexPartialObject): def sync(self, bitrate, client=None, clientId=None, limit=None, title=None): """ Add current audio (artist, album or track) as sync item for specified device. - See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions. + See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the - module :mod:`plexapi.sync`. - client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see - :func:`plexapi.myplex.MyPlexAccount.sync`. - clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. + module :mod:`~plexapi.sync`. + client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see + :func:`~plexapi.myplex.MyPlexAccount.sync`. + clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. limit (int): maximum count of items to sync, unlimited if `None`. - title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be + title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current media. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. """ from plexapi.sync import SyncItem, Policy, MediaSettings @@ -111,7 +117,6 @@ class Artist(Audio): Attributes: TAG (str): 'Directory' TYPE (str): 'artist' - art (str): Artist artwork (/library/metadata//art/) 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) @@ -122,17 +127,10 @@ class Artist(Audio): TAG = 'Directory' TYPE = 'artist' - _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' - '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' - '&includeMarkers=1&includeConcerts=1&includePreferences=1' - '&includeBandwidths=1&includeLoudnessRamps=1') - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) self.key = self.key.replace('/children', '') # FIX_BUG_50 - self._details_key = self.key + self._include - self.art = data.attrib.get('art') self.guid = data.attrib.get('guid') self.locations = self.listAttrs(data, 'path', etag='Location') self.countries = self.findItems(data, media.Country) @@ -187,7 +185,7 @@ class Artist(Audio): keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". - kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will + kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will be returned and the additional arguments passed in will be sent to that function. If kwargs is not specified, the media items will be downloaded and saved to disk. @@ -206,7 +204,6 @@ class Album(Audio): Attributes: TAG (str): 'Directory' TYPE (str): 'album' - art (str): Album artwork (/library/metadata//art/) genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents. key (str): API URL (/library/metadata/). originallyAvailableAt (datetime): Datetime this album was released. @@ -227,7 +224,6 @@ class Album(Audio): def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) - self.art = data.attrib.get('art') self.guid = data.attrib.get('guid') self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion')) @@ -279,7 +275,7 @@ class Album(Audio): keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". - kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will + kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will be returned and the additional arguments passed in will be sent to that function. If kwargs is not specified, the media items will be downloaded and saved to disk. @@ -301,7 +297,6 @@ class Track(Audio, Playable): Attributes: TAG (str): 'Directory' TYPE (str): 'track' - art (str): Track artwork (/library/metadata//art/) chapterSource (TYPE): Unknown duration (int): Length of this album in seconds. grandparentArt (str): Album artist artwork. @@ -332,17 +327,10 @@ class Track(Audio, Playable): TAG = 'Track' TYPE = 'track' - _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' - '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' - '&includeMarkers=1&includeConcerts=1&includePreferences=1' - '&includeBandwidths=1&includeLoudnessRamps=1') - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) Playable._loadData(self, data) - self._details_key = self.key + self._include - self.art = data.attrib.get('art') self.chapterSource = data.attrib.get('chapterSource') self.duration = utils.cast(int, data.attrib.get('duration')) self.grandparentArt = data.attrib.get('grandparentArt') diff --git a/lib/plexapi/base.py b/lib/plexapi/base.py index d553e5b1..e7cf8e53 100644 --- a/lib/plexapi/base.py +++ b/lib/plexapi/base.py @@ -44,9 +44,9 @@ class PlexObject(object): self._server = server self._data = data self._initpath = initpath or self.key - self._details_key = '' if data is not None: self._loadData(data) + self._details_key = self._buildDetailsKey() def __repr__(self): uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri')) @@ -81,7 +81,7 @@ class PlexObject(object): raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, etype)) def _buildItemOrNone(self, elem, cls=None, initpath=None): - """ Calls :func:`~plexapi.base.PlexObject._buildItem()` but returns + """ Calls :func:`~plexapi.base.PlexObject._buildItem` but returns None if elem is an unknown type. """ try: @@ -89,6 +89,22 @@ class PlexObject(object): except UnknownType: return None + def _buildDetailsKey(self, **kwargs): + """ Builds the details key with the XML include parameters. + All parameters are included by default with the option to override each parameter + or disable each parameter individually by setting it to False or 0. + """ + details_key = self.key + if hasattr(self, '_INCLUDES'): + includes = {} + for k, v in self._INCLUDES.items(): + value = kwargs.get(k, v) + if value not in [False, 0, '0']: + includes[k] = 1 if value is True else value + if includes: + details_key += '?' + urlencode(sorted(includes.items())) + return details_key + def fetchItem(self, ekey, cls=None, **kwargs): """ Load the specified key to find and build the first item with the specified tag and attrs. If no tag or attrs are specified then @@ -203,9 +219,39 @@ class PlexObject(object): results.append(elem.attrib.get(attr)) return results - def reload(self, key=None): - """ Reload the data for this object from self.key. """ - key = key or self._details_key or self.key + def reload(self, key=None, **kwargs): + """ Reload the data for this object from self.key. + + Parameters: + key (string, optional): Override the key to reload. + **kwargs (dict): A dictionary of XML include parameters to exclude or override. + All parameters are included by default with the option to override each parameter + or disable each parameter individually by setting it to False or 0. + See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters. + + Example: + + .. code-block:: python + + from plexapi.server import PlexServer + plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx') + movie = plex.library.section('Movies').get('Cars') + + # Partial reload of the movie without the `checkFiles` parameter. + # Excluding `checkFiles` will prevent the Plex server from reading the + # file to check if the file still exists and is accessible. + # The movie object will remain as a partial object. + movie.reload(checkFiles=False) + movie.isPartialObject() # Returns True + + # Full reload of the movie with all include parameters. + # The movie object will be a full object. + movie.reload() + movie.isFullObject() # Returns True + + """ + details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key + key = key or details_key or self.key if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key @@ -281,6 +327,27 @@ class PlexPartialObject(PlexObject): and if the specified value you request is None it will fetch the full object automatically and update itself. """ + _INCLUDES = { + 'checkFiles': 1, + 'includeAllConcerts': 1, + 'includeBandwidths': 1, + 'includeChapters': 1, + 'includeChildren': 1, + 'includeConcerts': 1, + 'includeExternalMedia': 1, + 'includeExtras': 1, + 'includeFields': 'thumbBlurHash,artBlurHash', + 'includeGeolocation': 1, + 'includeLoudnessRamps': 1, + 'includeMarkers': 1, + 'includeOnDeck': 1, + 'includePopularLeaves': 1, + 'includePreferences': 1, + 'includeRelated': 1, + 'includeRelatedCount': 1, + 'includeReviews': 1, + 'includeStations': 1 + } def __eq__(self, other): return other is not None and self.key == other.key @@ -332,7 +399,7 @@ class PlexPartialObject(PlexObject): """ Retruns True if this is already a full object. A full object means all attributes were populated from the api path representing only this item. For example, the search result for a movie often only contain a portion of the attributes a full - object (main url) for that movie contain. + object (main url) for that movie would contain. """ return not self.key or (self._details_key or self.key) == self._initpath @@ -608,14 +675,6 @@ class Playable(object): self.accountID = utils.cast(int, data.attrib.get('accountID')) # history self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist - def isFullObject(self): - """ Retruns True if this is already a full object. A full object means all attributes - were populated from the api path representing only this item. For example, the - search result for a movie often only contain a portion of the attributes a full - object (main url) for that movie contain. - """ - return self._details_key == self._initpath or not self.key - def getStreamURL(self, **params): """ Returns a stream url that may be used by external applications such as VLC. @@ -625,7 +684,7 @@ class Playable(object): offset, copyts, protocol, mediaIndex, platform. Raises: - :class:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL. + :exc:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL. """ if self.TYPE not in ('movie', 'episode', 'track'): raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE) @@ -690,7 +749,7 @@ class Playable(object): keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". - kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will + kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will be returned and the additional arguments passed in will be sent to that function. If kwargs is not specified, the media items will be downloaded and saved to disk. diff --git a/lib/plexapi/client.py b/lib/plexapi/client.py index e4381496..8d077238 100644 --- a/lib/plexapi/client.py +++ b/lib/plexapi/client.py @@ -53,7 +53,7 @@ class PlexClient(PlexObject): _token (str): Token used to access this client. _session (obj): Requests session object used to access this client. _proxyThroughServer (bool): Set to True after calling - :func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False). + :func:`~plexapi.client.PlexClient.proxyThroughServer` (default False). """ TAG = 'Player' key = '/resources' @@ -138,7 +138,7 @@ class PlexClient(PlexObject): value (bool): Enable or disable proxying (optional, default True). Raises: - :class:`plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server. + :exc:`plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server. """ if server: self._server = server @@ -171,7 +171,7 @@ class PlexClient(PlexObject): return ElementTree.fromstring(data) if data.strip() else None def sendCommand(self, command, proxy=None, **params): - """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily + """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query` to more easily send simple commands to the client. Returns an ElementTree object containing the response. @@ -181,7 +181,7 @@ class PlexClient(PlexObject): **params (dict): Additional GET parameters to include with the command. Raises: - :class:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. + :exc:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. """ command = command.strip('/') controller = command.split('/')[0] @@ -296,7 +296,7 @@ class PlexClient(PlexObject): **params (dict): Additional GET parameters to include with the command. Raises: - :class:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. + :exc:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self._server: raise Unsupported('A server must be specified before using this command.') @@ -466,7 +466,7 @@ class PlexClient(PlexObject): also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands Raises: - :class:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. + :exc:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self._server: raise Unsupported('A server must be specified before using this command.') diff --git a/lib/plexapi/library.py b/lib/plexapi/library.py index 589e6b47..c99919d3 100644 --- a/lib/plexapi/library.py +++ b/lib/plexapi/library.py @@ -455,7 +455,7 @@ class LibrarySection(PlexObject): return self.fetchItems(key, **kwargs) def agents(self): - """ Returns a list of available `:class:`~plexapi.media.Agent` for this library section. + """ Returns a list of available :class:`~plexapi.media.Agent` for this library section. """ return self._server.agents(utils.searchType(self.type)) @@ -517,7 +517,7 @@ class LibrarySection(PlexObject): def listChoices(self, category, libtype=None, **kwargs): """ Returns a list of :class:`~plexapi.library.FilterChoice` objects for the specified category and libtype. kwargs can be any of the same kwargs in - :func:`plexapi.library.LibraySection.search()` to help narrow down the choices + :func:`~plexapi.library.LibraySection.search` to help narrow down the choices to only those that matter in your current context. Parameters: @@ -526,7 +526,7 @@ class LibrarySection(PlexObject): **kwargs (dict): Additional kwargs to narrow down the choices. Raises: - :class:`plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category. + :exc:`plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category. """ # TODO: Should this be moved to base? if category in kwargs: @@ -573,7 +573,7 @@ class LibrarySection(PlexObject): * year: List of years to search within ([yyyy, ...]). [all] Raises: - :class:`plexapi.exceptions.BadRequest`: when applying unknown filter + :exc:`plexapi.exceptions.BadRequest`: when applying unknown filter """ # cleanup the core arguments args = {} @@ -659,20 +659,20 @@ class LibrarySection(PlexObject): def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None, **kwargs): """ Add current library section as sync item for specified device. - See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting - and :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions. + See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting + and :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: - policy (:class:`plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process + policy (:class:`~plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process watched media or not), generated automatically when method called on specific LibrarySection object. - mediaSettings (:class:`plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated + mediaSettings (:class:`~plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated automatically when method called on specific LibrarySection object. - client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see - :func:`plexapi.myplex.MyPlexAccount.sync`. - clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. - title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be + client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see + :func:`~plexapi.myplex.MyPlexAccount.sync`. + clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. + title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current media. sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`, `lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or @@ -681,10 +681,10 @@ class LibrarySection(PlexObject): `track`). Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Raises: - :class:`plexapi.exceptions.BadRequest`: when the library is not allowed to sync + :exc:`plexapi.exceptions.BadRequest`: when the library is not allowed to sync Example: @@ -784,17 +784,17 @@ class MovieSection(LibrarySection): def sync(self, videoQuality, limit=None, unwatched=False, **kwargs): """ Add current Movie library section as sync item for specified device. - See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and - :func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions. + See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and + :func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in - :mod:`plexapi.sync` module. + :mod:`~plexapi.sync` module. limit (int): maximum count of movies to sync, unlimited if `None`. unwatched (bool): if `True` watched videos wouldn't be synced. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Example: @@ -843,11 +843,11 @@ class ShowSection(LibrarySection): CONTENT_TYPE = 'video' def searchShows(self, **kwargs): - """ Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for a show. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='show', **kwargs) def searchEpisodes(self, **kwargs): - """ Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for an episode. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='episode', **kwargs) def recentlyAdded(self, libtype='episode', maxresults=50): @@ -877,17 +877,17 @@ class ShowSection(LibrarySection): def sync(self, videoQuality, limit=None, unwatched=False, **kwargs): """ Add current Show library section as sync item for specified device. - See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and - :func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions. + See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and + :func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in - :mod:`plexapi.sync` module. + :mod:`~plexapi.sync` module. limit (int): maximum count of episodes to sync, unlimited if `None`. unwatched (bool): if `True` watched videos wouldn't be synced. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Example: @@ -941,15 +941,15 @@ class MusicSection(LibrarySection): return self.fetchItems(key) def searchArtists(self, **kwargs): - """ Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='artist', **kwargs) def searchAlbums(self, **kwargs): - """ Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='album', **kwargs) def searchTracks(self, **kwargs): - """ Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for a track. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='track', **kwargs) def all(self, libtype='artist', **kwargs): @@ -971,16 +971,16 @@ class MusicSection(LibrarySection): def sync(self, bitrate, limit=None, **kwargs): """ Add current Music library section as sync item for specified device. - See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and - :func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions. + See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and + :func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions. Parameters: bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the - module :mod:`plexapi.sync`. + module :mod:`~plexapi.sync`. limit (int): maximum count of tracks to sync, unlimited if `None`. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Example: @@ -1023,11 +1023,11 @@ class PhotoSection(LibrarySection): METADATA_TYPE = 'photo' def searchAlbums(self, title, **kwargs): - """ Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='photoalbum', title=title, **kwargs) def searchPhotos(self, title, **kwargs): - """ Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ + """ Search for a photo. See :func:`~plexapi.library.LibrarySection.search` for usage. """ return self.search(libtype='photo', title=title, **kwargs) def all(self, libtype='photoalbum', **kwargs): @@ -1043,16 +1043,16 @@ class PhotoSection(LibrarySection): def sync(self, resolution, limit=None, **kwargs): """ Add current Music library section as sync item for specified device. - See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and - :func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions. + See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and + :func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions. Parameters: resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the - module :mod:`plexapi.sync`. + module :mod:`~plexapi.sync`. limit (int): maximum count of tracks to sync, unlimited if `None`. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Example: @@ -1079,7 +1079,7 @@ class PhotoSection(LibrarySection): class FilterChoice(PlexObject): """ Represents a single filter choice. These objects are gathered when using filters while searching for library items and is the object returned in the result set of - :func:`~plexapi.library.LibrarySection.listChoices()`. + :func:`~plexapi.library.LibrarySection.listChoices`. Attributes: TAG (str): 'Directory' @@ -1140,6 +1140,8 @@ class Collections(PlexPartialObject): 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. + artBlurHash (str): BlurHash string for artwork image. childCount (int): Count of child object(s) collectionMode (str): How the items in the collection are displayed. collectionSort (str): How to sort the items in the collection. @@ -1157,6 +1159,7 @@ class Collections(PlexPartialObject): subtype (str): Media type summary (str): Summary of the collection thumb (str): URL to thumbnail image. + thumbBlurHash (str): BlurHash string for thumbnail image. title (str): Collection Title titleSort (str): Title to use when sorting (defaults to title). type (str): Hardcoded 'collection' @@ -1165,14 +1168,13 @@ class Collections(PlexPartialObject): TAG = 'Directory' TYPE = 'collection' - _include = "?includeExternalMedia=1&includePreferences=1" def _loadData(self, data): self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50 - self._details_key = self.key + self._include self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') + self.artBlurHash = data.attrib.get('artBlurHash') self.childCount = utils.cast(int, data.attrib.get('childCount')) self.collectionMode = utils.cast(int, data.attrib.get('collectionMode')) self.collectionSort = utils.cast(int, data.attrib.get('collectionSort')) @@ -1189,6 +1191,7 @@ class Collections(PlexPartialObject): 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.type = data.attrib.get('type') @@ -1226,7 +1229,7 @@ class Collections(PlexPartialObject): collection = 'plexapi.library.Collections' collection.updateMode(mode="hide") """ - mode_dict = {'default': '-2', + mode_dict = {'default': '-1', 'hide': '0', 'hideItems': '1', 'showItems': '2'} @@ -1293,3 +1296,54 @@ class Collections(PlexPartialObject): # def edit(self, **kwargs): # TODO + + +@utils.registerPlexObject +class Path(PlexObject): + """ Represents a single directory Path. + + Attributes: + TAG (str): 'Path' + + home (bool): True if the path is the home directory + key (str): API URL (/services/browse/) + network (bool): True if path is a network location + path (str): Full path to folder + title (str): Folder name + """ + TAG = 'Path' + + def _loadData(self, data): + self.home = utils.cast(bool, data.attrib.get('home')) + self.key = data.attrib.get('key') + self.network = utils.cast(bool, data.attrib.get('network')) + self.path = data.attrib.get('path') + self.title = data.attrib.get('title') + + def browse(self, includeFiles=True): + """ Alias for :func:`~plexapi.server.PlexServer.browse`. """ + return self._server.browse(self, includeFiles) + + def walk(self): + """ Alias for :func:`~plexapi.server.PlexServer.walk`. """ + for path, paths, files in self._server.walk(self): + yield path, paths, files + + +@utils.registerPlexObject +class File(PlexObject): + """ Represents a single File. + + Attributes: + TAG (str): 'File' + + key (str): API URL (/services/browse/) + path (str): Full path to file + title (str): File name + """ + TAG = 'File' + + def _loadData(self, data): + self.key = data.attrib.get('key') + self.path = data.attrib.get('path') + self.title = data.attrib.get('title') diff --git a/lib/plexapi/media.py b/lib/plexapi/media.py index e7ea057c..86223ad4 100644 --- a/lib/plexapi/media.py +++ b/lib/plexapi/media.py @@ -562,7 +562,7 @@ class MediaTag(PlexObject): tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of person for Directors and Roles (ex: Animation, Stephen Graham, etc). : Attributes only applicable in search results from - PlexServer :func:`~plexapi.server.PlexServer.search()`. They provide details of which + PlexServer :func:`~plexapi.server.PlexServer.search`. They provide details of which library section the tag was found as well as the url to dig deeper into the results. * key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081). @@ -589,7 +589,7 @@ class MediaTag(PlexObject): def items(self, *args, **kwargs): """ Return the list of items within this tag. This function is only applicable - in search results from PlexServer :func:`~plexapi.server.PlexServer.search()`. + in search results from PlexServer :func:`~plexapi.server.PlexServer.search`. """ if not self.key: raise BadRequest('Key is not defined for this tag: %s' % self.tag) diff --git a/lib/plexapi/myplex.py b/lib/plexapi/myplex.py index e9bac9c0..84064894 100644 --- a/lib/plexapi/myplex.py +++ b/lib/plexapi/myplex.py @@ -544,7 +544,7 @@ class MyPlexAccount(PlexObject): return self.query(url, method=self._session.put, data=params) def syncItems(self, client=None, clientId=None): - """ Returns an instance of :class:`plexapi.sync.SyncList` for specified client. + """ Returns an instance of :class:`~plexapi.sync.SyncList` for specified client. Parameters: client (:class:`~plexapi.myplex.MyPlexDevice`): a client to query SyncItems for. @@ -564,22 +564,22 @@ class MyPlexAccount(PlexObject): def sync(self, sync_item, client=None, clientId=None): """ Adds specified sync item for the client. It's always easier to use methods defined directly in the media - objects, e.g. :func:`plexapi.video.Video.sync`, :func:`plexapi.audio.Audio.sync`. + objects, e.g. :func:`~plexapi.video.Video.sync`, :func:`~plexapi.audio.Audio.sync`. Parameters: client (:class:`~plexapi.myplex.MyPlexDevice`): a client for which you need to add SyncItem to. clientId (str): an identifier of a client for which you need to add SyncItem to. - sync_item (:class:`plexapi.sync.SyncItem`): prepared SyncItem object with all fields set. + sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set. If both `client` and `clientId` provided the client would be preferred. If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. Raises: - :class:`plexapi.exceptions.BadRequest`: when client with provided clientId wasn`t found. - :class:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`. + :exc:`plexapi.exceptions.BadRequest`: when client with provided clientId wasn`t found. + :exc:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`. """ if not client and not clientId: clientId = X_PLEX_IDENTIFIER @@ -686,7 +686,7 @@ class MyPlexAccount(PlexObject): class MyPlexUser(PlexObject): """ This object represents non-signed in users such as friends and linked - accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount` + accounts. NOTE: This should not be confused with the :class:`~plexapi.myplex.MyPlexAccount` which is your specific account. The raw xml for the data presented here can be found at: https://plex.tv/api/users/ @@ -885,7 +885,7 @@ class MyPlexResource(PlexObject): key (str): 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1' accessToken (str): This resources accesstoken. clientIdentifier (str): Unique ID for this resource. - connections (list): List of :class:`~myplex.ResourceConnection` objects + connections (list): List of :class:`~plexapi.myplex.ResourceConnection` objects for this resource. createdAt (datetime): Timestamp this resource first connected to your server. device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc). @@ -930,7 +930,7 @@ class MyPlexResource(PlexObject): self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username. def connect(self, ssl=None, timeout=None): - """ Returns a new :class:`~server.PlexServer` or :class:`~client.PlexClient` object. + """ Returns a new :class:`~plexapi.server.PlexServer` or :class:`~plexapi.client.PlexClient` object. Often times there is more than one address specified for a server or client. This function will prioritize local connections before remote and HTTPS before HTTP. After trying to connect to all available addresses for this resource and @@ -942,7 +942,7 @@ class MyPlexResource(PlexObject): HTTP or HTTPS connection. Raises: - :class:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. + :exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. """ # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource @@ -965,7 +965,7 @@ class MyPlexResource(PlexObject): class ResourceConnection(PlexObject): """ Represents a Resource Connection object found within the - :class:`~myplex.MyPlexResource` objects. + :class:`~plexapi.myplex.MyPlexResource` objects. Attributes: TAG (str): 'Connection' @@ -1049,7 +1049,7 @@ class MyPlexDevice(PlexObject): at least one connection was successful, the PlexClient object is built and returned. Raises: - :class:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device. + :exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device. """ cls = PlexServer if 'server' in self.provides else PlexClient listargs = [[cls, url, self.token, timeout] for url in self.connections] @@ -1063,10 +1063,10 @@ class MyPlexDevice(PlexObject): self._server.query(key, self._server._session.delete) def syncItems(self): - """ Returns an instance of :class:`plexapi.sync.SyncList` for current device. + """ Returns an instance of :class:`~plexapi.sync.SyncList` for current device. Raises: - :class:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`. + :exc:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`. """ if 'sync-target' not in self.provides: raise BadRequest('Requested syncList for device which do not provides sync-target') @@ -1082,12 +1082,12 @@ class MyPlexPinLogin(object): This helper class supports a polling, threaded and callback approach. - The polling approach expects the developer to periodically check if the PIN login was - successful using :func:`plexapi.myplex.MyPlexPinLogin.checkLogin`. + successful using :func:`~plexapi.myplex.MyPlexPinLogin.checkLogin`. - The threaded approach expects the developer to call - :func:`plexapi.myplex.MyPlexPinLogin.run` and then at a later time call - :func:`plexapi.myplex.MyPlexPinLogin.waitForLogin` to wait for and check the result. + :func:`~plexapi.myplex.MyPlexPinLogin.run` and then at a later time call + :func:`~plexapi.myplex.MyPlexPinLogin.waitForLogin` to wait for and check the result. - The callback approach is an extension of the threaded approach and expects the developer - to pass the `callback` parameter to the call to :func:`plexapi.myplex.MyPlexPinLogin.run`. + to pass the `callback` parameter to the call to :func:`~plexapi.myplex.MyPlexPinLogin.run`. The callback will be called when the thread waiting for the PIN login to succeed either finishes or expires. The parameter passed to the callback is the received authentication token or `None` if the login expired. diff --git a/lib/plexapi/photo.py b/lib/plexapi/photo.py index 68b77a58..80720495 100644 --- a/lib/plexapi/photo.py +++ b/lib/plexapi/photo.py @@ -168,20 +168,20 @@ class Photo(PlexPartialObject): def sync(self, resolution, client=None, clientId=None, limit=None, title=None): """ Add current photo as sync item for specified device. - See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions. + See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the - module :mod:`plexapi.sync`. - client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see - :func:`plexapi.myplex.MyPlexAccount.sync`. - clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. + module :mod:`~plexapi.sync`. + client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see + :func:`~plexapi.myplex.MyPlexAccount.sync`. + clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. limit (int): maximum count of items to sync, unlimited if `None`. - title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be + title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current photo. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. """ from plexapi.sync import SyncItem, Policy, MediaSettings diff --git a/lib/plexapi/playlist.py b/lib/plexapi/playlist.py index 43c63c6f..319d9123 100644 --- a/lib/plexapi/playlist.py +++ b/lib/plexapi/playlist.py @@ -163,7 +163,7 @@ class Playlist(PlexPartialObject, Playable): **kwargs (dict): is passed to the filters. For a example see the search method. Returns: - :class:`plexapi.playlist.Playlist`: an instance of created Playlist. + :class:`~plexapi.playlist.Playlist`: an instance of created Playlist. """ if smart: return cls._createSmart(server, title, section, limit, **kwargs) @@ -217,29 +217,29 @@ class Playlist(PlexPartialObject, Playable): def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None, unwatched=False, title=None): """ Add current playlist as sync item for specified device. - See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions. + See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in - :mod:`plexapi.sync` module. Used only when playlist contains video. + :mod:`~plexapi.sync` module. Used only when playlist contains video. photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in - the module :mod:`plexapi.sync`. Used only when playlist contains photos. + the module :mod:`~plexapi.sync`. Used only when playlist contains photos. audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values - from the module :mod:`plexapi.sync`. Used only when playlist contains audio. - client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see - :func:`plexapi.myplex.MyPlexAccount.sync`. - clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. + from the module :mod:`~plexapi.sync`. Used only when playlist contains audio. + client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see + :func:`~plexapi.myplex.MyPlexAccount.sync`. + clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. limit (int): maximum count of items to sync, unlimited if `None`. unwatched (bool): if `True` watched videos wouldn't be synced. - title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be + title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current photo. Raises: - :class:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync. - :class:`plexapi.exceptions.Unsupported`: when playlist content is unsupported. + :exc:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync. + :exc:`plexapi.exceptions.Unsupported`: when playlist content is unsupported. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. """ if not self.allowSync: diff --git a/lib/plexapi/server.py b/lib/plexapi/server.py index ebc41a25..7b06bf92 100644 --- a/lib/plexapi/server.py +++ b/lib/plexapi/server.py @@ -8,7 +8,7 @@ from plexapi.base import PlexObject from plexapi.client import PlexClient from plexapi.compat import ElementTree, urlencode from plexapi.exceptions import BadRequest, NotFound, Unauthorized -from plexapi.library import Library, Hub +from plexapi.library import Hub, Library, Path, File from plexapi.settings import Settings from plexapi.playlist import Playlist from plexapi.playqueue import PlayQueue @@ -185,7 +185,7 @@ class PlexServer(PlexObject): return Account(self, data) def agents(self, mediaType=None): - """ Returns the `:class:`~plexapi.media.Agent` objects this server has available. """ + """ Returns the :class:`~plexapi.media.Agent` objects this server has available. """ key = '/system/agents' if mediaType: key += '?mediaType=%s' % mediaType @@ -233,6 +233,53 @@ class PlexServer(PlexObject): log.warning('Unable to fetch client ports from myPlex: %s', err) return ports + def browse(self, path=None, includeFiles=True): + """ Browse the system file path using the Plex API. + Returns list of :class:`~plexapi.library.Path` and :class:`~plexapi.library.File` objects. + + Parameters: + path (:class:`~plexapi.library.Path` or str, optional): Full path to browse. + includeFiles (bool): True to include files when browsing (Default). + False to only return folders. + """ + if isinstance(path, Path): + key = path.key + elif path is not None: + base64path = utils.base64str(path) + key = '/services/browse/%s' % base64path + else: + key = '/services/browse' + if includeFiles: + key += '?includeFiles=1' + return self.fetchItems(key) + + def walk(self, path=None): + """ Walk the system file tree using the Plex API similar to `os.walk`. + Yields a 3-tuple `(path, paths, files)` where + `path` is a string of the directory path, + `paths` is a list of :class:`~plexapi.library.Path` objects, and + `files` is a list of :class:`~plexapi.library.File` objects. + + Parameters: + path (:class:`~plexapi.library.Path` or str, optional): Full path to walk. + """ + paths = [] + files = [] + for item in self.browse(path): + if isinstance(item, Path): + paths.append(item) + elif isinstance(item, File): + files.append(item) + + if isinstance(path, Path): + path = path.path + + yield path or '', paths, files + + for _path in paths: + for path, paths, files in self.walk(_path): + yield path, paths, files + def clients(self): """ Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """ items = [] @@ -256,7 +303,7 @@ class PlexServer(PlexObject): name (str): Name of the client to return. Raises: - :class:`plexapi.exceptions.NotFound`: Unknown client name + :exc:`plexapi.exceptions.NotFound`: Unknown client name """ for client in self.clients(): if client and client.title == name: @@ -379,7 +426,7 @@ class PlexServer(PlexObject): title (str): Title of the playlist to return. Raises: - :class:`plexapi.exceptions.NotFound`: Invalid playlist title + :exc:`plexapi.exceptions.NotFound`: Invalid playlist title """ return self.fetchItem('/playlists', title=title) @@ -480,8 +527,8 @@ class PlexServer(PlexObject): Parameters: callback (func): Callback function to call on recieved messages. - raises: - :class:`plexapi.exception.Unsupported`: Websocket-client not installed. + Raises: + :exc:`plexapi.exception.Unsupported`: Websocket-client not installed. """ notifier = AlertListener(self, callback) notifier.start() diff --git a/lib/plexapi/settings.py b/lib/plexapi/settings.py index 1511c0bb..dddaab90 100644 --- a/lib/plexapi/settings.py +++ b/lib/plexapi/settings.py @@ -21,7 +21,10 @@ class Settings(PlexObject): def __getattr__(self, attr): if attr.startswith('_'): - return self.__dict__[attr] + try: + return self.__dict__[attr] + except KeyError: + raise AttributeError return self.get(attr).value def __setattr__(self, attr, value): diff --git a/lib/plexapi/sync.py b/lib/plexapi/sync.py index 0f739860..f88fefb2 100644 --- a/lib/plexapi/sync.py +++ b/lib/plexapi/sync.py @@ -78,7 +78,7 @@ class SyncItem(PlexObject): self.location = data.find('Location').attrib.get('uri', '') def server(self): - """ Returns :class:`plexapi.myplex.MyPlexResource` with server of current item. """ + """ Returns :class:`~plexapi.myplex.MyPlexResource` with server of current item. """ server = [s for s in self._server.resources() if s.clientIdentifier == self.machineIdentifier] if len(server) == 0: raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier) @@ -201,7 +201,7 @@ class MediaSettings(object): videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in this module. Raises: - :class:`plexapi.exceptions.BadRequest`: when provided unknown video quality. + :exc:`plexapi.exceptions.BadRequest`: when provided unknown video quality. """ if videoQuality == VIDEO_QUALITY_ORIGINAL: return MediaSettings('', '', '') @@ -231,7 +231,7 @@ class MediaSettings(object): module. Raises: - :class:`plexapi.exceptions.BadRequest` when provided unknown video quality. + :exc:`plexapi.exceptions.BadRequest` when provided unknown video quality. """ if resolution in PHOTO_QUALITIES: return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution) diff --git a/lib/plexapi/utils.py b/lib/plexapi/utils.py index 344622b9..9636b4be 100644 --- a/lib/plexapi/utils.py +++ b/lib/plexapi/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import base64 import logging import os import re @@ -147,7 +148,7 @@ def searchType(libtype): libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track, collection) Raises: - :class:`plexapi.exceptions.NotFound`: Unknown libtype + :exc:`plexapi.exceptions.NotFound`: Unknown libtype """ libtype = compat.ustr(libtype) if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]: @@ -399,3 +400,7 @@ def getAgentIdentifier(section, agent): agents += identifiers raise NotFound('Couldnt find "%s" in agents list (%s)' % (agent, ', '.join(agents))) + + +def base64str(text): + return base64.b64encode(text.encode('utf-8')).decode('utf-8') diff --git a/lib/plexapi/video.py b/lib/plexapi/video.py index 2ca35aa9..68e1e312 100644 --- a/lib/plexapi/video.py +++ b/lib/plexapi/video.py @@ -13,6 +13,8 @@ class Video(PlexPartialObject): Attributes: addedAt (datetime): Datetime this item was added to the library. + art (str): URL to artwork image. + artBlurHash (str): BlurHash string for artwork image. key (str): API URL (/library/metadata/). lastViewedAt (datetime): Datetime item was last accessed. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. @@ -20,6 +22,7 @@ class Video(PlexPartialObject): ratingKey (int): Unique key identifying this item. summary (str): Summary of the artist, track, or album. thumb (str): URL to thumbnail image. + thumbBlurHash (str): BlurHash string for thumbnail image. title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.) titleSort (str): Title to use when sorting (defaults to title). type (str): 'artist', 'album', or 'track'. @@ -32,6 +35,8 @@ class Video(PlexPartialObject): 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.key = data.attrib.get('key', '') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.librarySectionID = data.attrib.get('librarySectionID') @@ -40,6 +45,7 @@ class Video(PlexPartialObject): self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) 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.title) self.type = data.attrib.get('type') @@ -201,21 +207,21 @@ class Video(PlexPartialObject): def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None): """ Add current video (movie, tv-show, season or episode) as sync item for specified device. - See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions. + See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in - :mod:`plexapi.sync` module. - client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see - :func:`plexapi.myplex.MyPlexAccount.sync`. - clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. + :mod:`~plexapi.sync` module. + client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see + :func:`~plexapi.myplex.MyPlexAccount.sync`. + clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. limit (int): maximum count of items to sync, unlimited if `None`. unwatched (bool): if `True` watched videos wouldn't be synced. - title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be + title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current media. Returns: - :class:`plexapi.sync.SyncItem`: an instance of created syncItem. + :class:`~plexapi.sync.SyncItem`: an instance of created syncItem. """ from plexapi.sync import SyncItem, Policy, MediaSettings @@ -277,17 +283,12 @@ class Movie(Playable, Video): TAG = 'Video' TYPE = 'movie' METADATA_TYPE = 'movie' - _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' - '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' - '&includeConcerts=1&includePreferences=1' - '&includeBandwidths=1') def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) Playable._loadData(self, data) - self._details_key = self.key + self._include self.art = data.attrib.get('art') self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audienceRatingImage = data.attrib.get('audienceRatingImage') @@ -343,7 +344,7 @@ class Movie(Playable, Video): savepath (str): Defaults to current working dir. keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. - **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. + **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`. """ filepaths = [] locations = [i for i in self.iterParts() if i] @@ -460,14 +461,14 @@ class Show(Video): def episode(self, title=None, season=None, episode=None): """ Find a episode using a title or season and episode. - Parameters: + 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). - Raises: - :class:`plexapi.exceptions.BadRequest`: If season and episode is missing. - :class:`plexapi.exceptions.NotFound`: If the episode is missing. + Raises: + :exc:`plexapi.exceptions.BadRequest`: If season and episode is missing. + :exc:`plexapi.exceptions.NotFound`: If the episode is missing. """ if title: key = '/library/metadata/%s/allLeaves' % self.ratingKey @@ -488,7 +489,7 @@ class Show(Video): return self.episodes(viewCount=0) def get(self, title=None, season=None, episode=None): - """ Alias to :func:`~plexapi.video.Show.episode()`. """ + """ Alias to :func:`~plexapi.video.Show.episode`. """ return self.episode(title, season, episode) def download(self, savepath=None, keep_original_name=False, **kwargs): @@ -498,7 +499,7 @@ class Show(Video): savepath (str): Defaults to current working dir. keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. - **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. + **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`. """ filepaths = [] for episode in self.episodes(): @@ -585,7 +586,7 @@ class Season(Video): return self.fetchItem(key, parentIndex=self.index, index=episode) def get(self, title=None, episode=None): - """ Alias to :func:`~plexapi.video.Season.episode()`. """ + """ Alias to :func:`~plexapi.video.Season.episode`. """ return self.episode(title, episode) def show(self): @@ -607,7 +608,7 @@ class Season(Video): savepath (str): Defaults to current working dir. keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. - **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. + **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`. """ filepaths = [] for episode in self.episodes(): @@ -656,16 +657,10 @@ class Episode(Playable, Video): TYPE = 'episode' METADATA_TYPE = 'episode' - _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' - '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' - '&includeMarkers=1&includeConcerts=1&includePreferences=1' - '&includeBandwidths=1') - def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) Playable._loadData(self, data) - self._details_key = self.key + self._include self._seasonNumber = None # cached season number art = data.attrib.get('art') self.art = art if art and str(self.ratingKey) in art else None