Update python-plexapi-4.7.2

This commit is contained in:
JonnyWong16 2021-10-14 22:44:12 -07:00
parent c8b02b06d6
commit 0770a301c7
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
12 changed files with 283 additions and 42 deletions

View file

@ -6,6 +6,7 @@ from platform import uname
from uuid import getnode from uuid import getnode
from plexapi.config import PlexConfig, reset_base_headers from plexapi.config import PlexConfig, reset_base_headers
import plexapi.const as const
from plexapi.utils import SecretsFilter from plexapi.utils import SecretsFilter
# Load User Defined Config # Load User Defined Config
@ -15,7 +16,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
# PlexAPI Settings # PlexAPI Settings
PROJECT = 'PlexAPI' PROJECT = 'PlexAPI'
VERSION = '4.6.1' VERSION = __version__ = const.__version__
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int) TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int) X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool) X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)

View file

@ -28,6 +28,7 @@ class Audio(PlexPartialObject):
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
listType (str): Hardcoded as 'audio' (useful for search filters). listType (str): Hardcoded as 'audio' (useful for search filters).
moods (List<:class:`~plexapi.media.Mood`>): List of mood objects. moods (List<:class:`~plexapi.media.Mood`>): List of mood objects.
musicAnalysisVersion (int): The Plex music analysis version for the item.
ratingKey (int): Unique key identifying the item. ratingKey (int): Unique key identifying the item.
summary (str): Summary of the artist, album, or track. summary (str): Summary of the artist, album, or track.
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>). thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
@ -59,6 +60,7 @@ class Audio(PlexPartialObject):
self.librarySectionTitle = data.attrib.get('librarySectionTitle') self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.listType = 'audio' self.listType = 'audio'
self.moods = self.findItems(data, media.Mood) self.moods = self.findItems(data, media.Mood)
self.musicAnalysisVersion = utils.cast(int, data.attrib.get('musicAnalysisVersion'))
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.summary = data.attrib.get('summary') self.summary = data.attrib.get('summary')
self.thumb = data.attrib.get('thumb') self.thumb = data.attrib.get('thumb')
@ -78,6 +80,11 @@ class Audio(PlexPartialObject):
""" Returns str, default title for a new syncItem. """ """ Returns str, default title for a new syncItem. """
return self.title return self.title
@property
def hasSonicAnalysis(self):
""" Returns True if the audio has been sonically analyzed. """
return self.musicAnalysisVersion == 1
def sync(self, bitrate, client=None, clientId=None, limit=None, title=None): 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. """ 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.
@ -227,6 +234,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
TAG (str): 'Directory' TAG (str): 'Directory'
TYPE (str): 'album' TYPE (str): 'album'
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
formats (List<:class:`~plexapi.media.Format`>): List of format objects.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects. labels (List<:class:`~plexapi.media.Label`>): List of label objects.
@ -241,6 +249,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
rating (float): Album rating (7.9; 9.8; 8.1). rating (float): Album rating (7.9; 9.8; 8.1).
studio (str): Studio that released the album. studio (str): Studio that released the album.
styles (List<:class:`~plexapi.media.Style`>): List of style objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects.
subformats (List<:class:`~plexapi.media.Subformat`>): List of subformat objects.
viewedLeafCount (int): Number of items marked as played in the album view. viewedLeafCount (int): Number of items marked as played in the album view.
year (int): Year the album was released. year (int): Year the album was released.
""" """
@ -251,6 +260,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
""" Load attribute values from Plex XML response. """ """ Load attribute values from Plex XML response. """
Audio._loadData(self, data) Audio._loadData(self, data)
self.collections = self.findItems(data, media.Collection) self.collections = self.findItems(data, media.Collection)
self.formats = self.findItems(data, media.Format)
self.genres = self.findItems(data, media.Genre) self.genres = self.findItems(data, media.Genre)
self.key = self.key.replace('/children', '') # FIX_BUG_50 self.key = self.key.replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label) self.labels = self.findItems(data, media.Label)
@ -265,6 +275,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
self.rating = utils.cast(float, data.attrib.get('rating')) self.rating = utils.cast(float, data.attrib.get('rating'))
self.studio = data.attrib.get('studio') self.studio = data.attrib.get('studio')
self.styles = self.findItems(data, media.Style) self.styles = self.findItems(data, media.Style)
self.subformats = self.findItems(data, media.Subformat)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
self.year = utils.cast(int, data.attrib.get('year')) self.year = utils.cast(int, data.attrib.get('year'))
@ -415,3 +426,7 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
def _defaultSyncTitle(self): def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """ """ Returns str, default title for a new syncItem. """
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title) return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey)

View file

@ -505,6 +505,17 @@ class PlexPartialObject(PlexObject):
""" Returns True if this is not a full object. """ """ Returns True if this is not a full object. """
return not self.isFullObject() return not self.isFullObject()
def _edit(self, **kwargs):
""" Actually edit an object. """
if 'id' not in kwargs:
kwargs['id'] = self.ratingKey
if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self.type)
part = '/library/sections/%s/all?%s' % (self.librarySectionID,
urlencode(kwargs))
self._server.query(part, method=self._server._session.put)
def edit(self, **kwargs): def edit(self, **kwargs):
""" Edit an object. """ Edit an object.
@ -517,14 +528,7 @@ class PlexPartialObject(PlexObject):
'collection[0].tag.tag': 'Super', 'collection[0].tag.tag': 'Super',
'collection.locked': 0} 'collection.locked': 0}
""" """
if 'id' not in kwargs: self._edit(**kwargs)
kwargs['id'] = self.ratingKey
if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self.type)
part = '/library/sections/%s/all?%s' % (self.librarySectionID,
urlencode(kwargs))
self._server.query(part, method=self._server._session.put)
def _edit_tags(self, tag, items, locked=True, remove=False): def _edit_tags(self, tag, items, locked=True, remove=False):
""" Helper to edit tags. """ Helper to edit tags.
@ -575,12 +579,28 @@ class PlexPartialObject(PlexObject):
def history(self, maxresults=9999999, mindate=None): def history(self, maxresults=9999999, mindate=None):
""" Get Play History for a media item. """ Get Play History for a media item.
Parameters: Parameters:
maxresults (int): Only return the specified number of results (optional). maxresults (int): Only return the specified number of results (optional).
mindate (datetime): Min datetime to return results from. mindate (datetime): Min datetime to return results from.
""" """
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey) return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters.
Private method to allow overriding parameters from subclasses.
"""
return self._server._buildWebURL(base=base, endpoint='details', key=self.key)
def getWebURL(self, base=None):
""" Returns the Plex Web URL for a media item.
Parameters:
base (str): The base URL before the fragment (``#!``).
Default is https://app.plex.tv/desktop.
"""
return self._getWebURL(base=base)
class Playable(object): class Playable(object):
""" This is a general place to store functions specific to media that is Playable. """ This is a general place to store functions specific to media that is Playable.

View file

@ -511,7 +511,9 @@ class PlexClient(PlexObject):
playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media) playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media)
self.sendCommand('playback/playMedia', **dict({ self.sendCommand('playback/playMedia', **dict({
'providerIdentifier': 'com.plexapp.plugins.library',
'machineIdentifier': self._server.machineIdentifier, 'machineIdentifier': self._server.machineIdentifier,
'protocol': server_url[0],
'address': server_url[1].strip('/'), 'address': server_url[1].strip('/'),
'port': server_port, 'port': server_port,
'offset': offset, 'offset': offset,

View file

@ -442,7 +442,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
smart (bool): True to create a smart collection. Default False. smart (bool): True to create a smart collection. Default False.
limit (int): Smart collections only, limit the number of items in the collection. limit (int): Smart collections only, limit the number of items in the collection.
libtype (str): Smart collections only, the specific type of content to filter libtype (str): Smart collections only, the specific type of content to filter
(movie, show, season, episode, artist, album, track, photoalbum, photo, collection). (movie, show, season, episode, artist, album, track, photoalbum, photo).
sort (str or list, optional): Smart collections only, a string of comma separated sort fields sort (str or list, optional): Smart collections only, a string of comma separated sort fields
or a list of sort fields in the format ``column:dir``. or a list of sort fields in the format ``column:dir``.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.

9
lib/plexapi/const.py Normal file
View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""Constants used by plexapi."""
# Library version
MAJOR_VERSION = 4
MINOR_VERSION = 7
PATCH_VERSION = 2
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"

View file

@ -456,10 +456,45 @@ class LibrarySection(PlexObject):
Parameters: Parameters:
title (str): Title of the item to return. title (str): Title of the item to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The title is not found in the library.
""" """
key = '/library/sections/%s/all?title=%s' % (self.key, quote(title, safe='')) key = '/library/sections/%s/all?includeGuids=1&title=%s' % (self.key, quote(title, safe=''))
return self.fetchItem(key, title__iexact=title) return self.fetchItem(key, title__iexact=title)
def getGuid(self, guid):
""" Returns the media item with the specified external IMDB, TMDB, or TVDB ID.
Note: This search uses a PlexAPI operator so performance may be slow. All items from the
entire Plex library need to be retrieved for each guid search. It is recommended to create
your own lookup dictionary if you are searching for a lot of external guids.
Parameters:
guid (str): The external guid of the item to return.
Examples: IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
Example:
.. code-block:: python
# This will retrieve all items in the entire library 3 times
result1 = library.getGuid('imdb://tt0944947')
result2 = library.getGuid('tmdb://1399')
result3 = library.getGuid('tvdb://121361')
# This will only retrieve all items in the library once to create a lookup dictionary
guidLookup = {guid.id: item for item in library.all() for guid in item.guids}
result1 = guidLookup['imdb://tt0944947']
result2 = guidLookup['tmdb://1399']
result3 = guidLookup['tvdb://121361']
"""
key = '/library/sections/%s/all?includeGuids=1' % self.key
return self.fetchItem(key, Guid__id__iexact=guid)
def all(self, libtype=None, **kwargs): def all(self, libtype=None, **kwargs):
""" Returns a list of all items from this library section. """ Returns a list of all items from this library section.
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting. See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting.
@ -979,6 +1014,8 @@ class LibrarySection(PlexObject):
""" """
args = {} args = {}
filter_args = [] filter_args = []
args['includeGuids'] = int(bool(kwargs.pop('includeGuids', True)))
for field, values in list(kwargs.items()): for field, values in list(kwargs.items()):
if field.split('__')[-1] not in OPERATORS: if field.split('__')[-1] not in OPERATORS:
filter_args.append(self._validateFilterField(field, values, libtype)) filter_args.append(self._validateFilterField(field, values, libtype))
@ -1405,10 +1442,14 @@ class LibrarySection(PlexObject):
Parameters: Parameters:
title (str): Title of the item to return. title (str): Title of the item to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: Unable to find collection.
""" """
results = self.collections(title__iexact=title) try:
if results: return self.collections(title=title, title__iexact=title)[0]
return results[0] except IndexError:
raise NotFound('Unable to find collection with title "%s".' % title) from None
def collections(self, **kwargs): def collections(self, **kwargs):
""" Returns a list of collections from this library section. """ Returns a list of collections from this library section.
@ -1430,15 +1471,19 @@ class LibrarySection(PlexObject):
Parameters: Parameters:
title (str): Title of the item to return. title (str): Title of the item to return.
"""
results = self.playlists(title__iexact=title)
if results:
return results[0]
def playlists(self, **kwargs): Raises:
:exc:`~plexapi.exceptions.NotFound`: Unable to find playlist.
"""
try:
return self.playlists(title=title, title__iexact=title)[0]
except IndexError:
raise NotFound('Unable to find playlist with title "%s".' % title) from None
def playlists(self, sort=None, **kwargs):
""" Returns a list of playlists from this library section. """ """ Returns a list of playlists from this library section. """
key = '/playlists?type=15&playlistType=%s&sectionID=%s' % (self.CONTENT_TYPE, self.key) return self._server.playlists(
return self.fetchItems(key, **kwargs) playlistType=self.CONTENT_TYPE, sectionId=self.key, sort=sort, **kwargs)
@deprecated('use "listFields" instead') @deprecated('use "listFields" instead')
def filterFields(self, mediaType=None): def filterFields(self, mediaType=None):
@ -1448,6 +1493,23 @@ class LibrarySection(PlexObject):
def listChoices(self, category, libtype=None, **kwargs): def listChoices(self, category, libtype=None, **kwargs):
return self.listFilterChoices(field=category, libtype=libtype) return self.listFilterChoices(field=category, libtype=libtype)
def getWebURL(self, base=None, tab=None, key=None):
""" Returns the Plex Web URL for the library.
Parameters:
base (str): The base URL before the fragment (``#!``).
Default is https://app.plex.tv/desktop.
tab (str): The library tab (recommended, library, collections, playlists, timeline).
key (str): A hub key.
"""
params = {'source': self.key}
if tab is not None:
params['pivot'] = tab
if key is not None:
params['key'] = key
params['pageType'] = 'list'
return self._server._buildWebURL(base=base, **params)
class MovieSection(LibrarySection): class MovieSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies. """ Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
@ -1849,6 +1911,7 @@ class Hub(PlexObject):
self.style = data.attrib.get('style') self.style = data.attrib.get('style')
self.title = data.attrib.get('title') self.title = data.attrib.get('title')
self.type = data.attrib.get('type') self.type = data.attrib.get('type')
self._section = None # cache for self.section
def __len__(self): def __len__(self):
return self.size return self.size
@ -1860,6 +1923,13 @@ class Hub(PlexObject):
self.more = False self.more = False
self.size = len(self.items) self.size = len(self.items)
def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this hub belongs to.
"""
if self._section is None:
self._section = self._server.library.sectionByID(self.librarySectionID)
return self._section
class HubMediaTag(PlexObject): class HubMediaTag(PlexObject):
""" Base class of hub media tag search results. """ Base class of hub media tag search results.

View file

@ -174,7 +174,7 @@ class MediaPart(PlexObject):
return [stream for stream in self.streams if isinstance(stream, SubtitleStream)] return [stream for stream in self.streams if isinstance(stream, SubtitleStream)]
def lyricStreams(self): def lyricStreams(self):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """ """ Returns a list of :class:`~plexapi.media.LyricStream` objects in this MediaPart. """
return [stream for stream in self.streams if isinstance(stream, LyricStream)] return [stream for stream in self.streams if isinstance(stream, LyricStream)]
def setDefaultAudioStream(self, stream): def setDefaultAudioStream(self, stream):
@ -731,6 +731,18 @@ class Director(MediaTag):
FILTER = 'director' FILTER = 'director'
@utils.registerPlexObject
class Format(MediaTag):
""" Represents a single Format media tag.
Attributes:
TAG (str): 'Format'
FILTER (str): 'format'
"""
TAG = 'Format'
FILTER = 'format'
@utils.registerPlexObject @utils.registerPlexObject
class Genre(MediaTag): class Genre(MediaTag):
""" Represents a single Genre media tag. """ Represents a single Genre media tag.
@ -815,6 +827,18 @@ class Style(MediaTag):
FILTER = 'style' FILTER = 'style'
@utils.registerPlexObject
class Subformat(MediaTag):
""" Represents a single Subformat media tag.
Attributes:
TAG (str): 'Subformat'
FILTER (str): 'subformat'
"""
TAG = 'Subformat'
FILTER = 'subformat'
@utils.registerPlexObject @utils.registerPlexObject
class Tag(MediaTag): class Tag(MediaTag):
""" Represents a single Tag media tag. """ Represents a single Tag media tag.

View file

@ -97,6 +97,14 @@ class ArtMixin(ArtUrlMixin):
""" """
art.select() art.select()
def lockArt(self):
""" Lock the background artwork for a Plex object. """
self._edit(**{'art.locked': 1})
def unlockArt(self):
""" Unlock the background artwork for a Plex object. """
self._edit(**{'art.locked': 0})
class BannerUrlMixin(object): class BannerUrlMixin(object):
""" Mixin for Plex objects that can have a banner url. """ """ Mixin for Plex objects that can have a banner url. """
@ -138,6 +146,14 @@ class BannerMixin(BannerUrlMixin):
""" """
banner.select() banner.select()
def lockBanner(self):
""" Lock the banner for a Plex object. """
self._edit(**{'banner.locked': 1})
def unlockBanner(self):
""" Unlock the banner for a Plex object. """
self._edit(**{'banner.locked': 0})
class PosterUrlMixin(object): class PosterUrlMixin(object):
""" Mixin for Plex objects that can have a poster url. """ """ Mixin for Plex objects that can have a poster url. """
@ -184,6 +200,14 @@ class PosterMixin(PosterUrlMixin):
""" """
poster.select() poster.select()
def lockPoster(self):
""" Lock the poster for a Plex object. """
self._edit(**{'thumb.locked': 1})
def unlockPoster(self):
""" Unlock the poster for a Plex object. """
self._edit(**{'thumb.locked': 0})
class RatingMixin(object): class RatingMixin(object):
""" Mixin for Plex objects that can have user star ratings. """ """ Mixin for Plex objects that can have user star ratings. """
@ -577,7 +601,9 @@ class SmartFilterMixin(object):
key += '=' key += '='
value = value[1:] value = value[1:]
if key == 'type': if key == 'includeGuids':
filters['includeGuids'] = int(value)
elif key == 'type':
filters['libtype'] = utils.reverseSearchType(value) filters['libtype'] = utils.reverseSearchType(value)
elif key == 'sort': elif key == 'sort':
filters['sort'] = value.split(',') filters['sort'] = value.split(',')

View file

@ -137,6 +137,10 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
filepaths.append(filepath) filepaths.append(filepath)
return filepaths return filepaths
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.key, legacy=1)
@utils.registerPlexObject @utils.registerPlexObject
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin): class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin):
@ -301,3 +305,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
if filepath: if filepath:
filepaths.append(filepath) filepaths.append(filepath)
return filepaths return filepaths
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey, legacy=1)

View file

@ -2,7 +2,7 @@
import re import re
from urllib.parse import quote_plus, unquote from urllib.parse import quote_plus, unquote
from plexapi import utils from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
@ -24,6 +24,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
content (str): The filter URI string for smart playlists. content (str): The filter URI string for smart playlists.
duration (int): Duration of the playlist in milliseconds. duration (int): Duration of the playlist in milliseconds.
durationInSeconds (int): Duration of the playlist in seconds. durationInSeconds (int): Duration of the playlist in seconds.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the playlist (com.plexapp.agents.none://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). guid (str): Plex GUID for the playlist (com.plexapp.agents.none://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
icon (str): Icon URI string for smart playlists. icon (str): Icon URI string for smart playlists.
key (str): API URL (/playlist/<ratingkey>). key (str): API URL (/playlist/<ratingkey>).
@ -48,8 +49,9 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
self.content = data.attrib.get('content') self.content = data.attrib.get('content')
self.duration = utils.cast(int, data.attrib.get('duration')) self.duration = utils.cast(int, data.attrib.get('duration'))
self.durationInSeconds = utils.cast(int, data.attrib.get('durationInSeconds')) self.durationInSeconds = utils.cast(int, data.attrib.get('durationInSeconds'))
self.icon = data.attrib.get('icon') self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.icon = data.attrib.get('icon')
self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50 self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50
self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
self.playlistType = data.attrib.get('playlistType') self.playlistType = data.attrib.get('playlistType')
@ -288,6 +290,11 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
})) }))
self._server.query(key, method=self._server._session.put) self._server.query(key, method=self._server._session.put)
def _edit(self, **kwargs):
""" Actually edit the playlist. """
key = '%s%s' % (self.key, utils.joinArgs(kwargs))
self._server.query(key, method=self._server._session.put)
def edit(self, title=None, summary=None): def edit(self, title=None, summary=None):
""" Edit the playlist. """ Edit the playlist.
@ -300,9 +307,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
args['title'] = title args['title'] = title
if summary: if summary:
args['summary'] = summary args['summary'] = summary
self._edit(**args)
key = '%s%s' % (self.key, utils.joinArgs(args))
self._server.query(key, method=self._server._session.put)
def delete(self): def delete(self):
""" Delete the playlist. """ """ Delete the playlist. """
@ -341,13 +346,15 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
return cls(server, data, initpath=key) return cls(server, data, initpath=key)
@classmethod @classmethod
def _createSmart(cls, server, title, section, limit=None, sort=None, filters=None, **kwargs): def _createSmart(cls, server, title, section, limit=None, libtype=None, sort=None, filters=None, **kwargs):
""" Create a smart playlist. """ """ Create a smart playlist. """
if not isinstance(section, LibrarySection): if not isinstance(section, LibrarySection):
section = server.library.section(section) section = server.library.section(section)
libtype = libtype or section.METADATA_TYPE
searchKey = section._buildSearchKey( searchKey = section._buildSearchKey(
sort=sort, libtype=section.METADATA_TYPE, limit=limit, filters=filters, **kwargs) sort=sort, libtype=libtype, limit=limit, filters=filters, **kwargs)
uri = '%s%s' % (server._uriRoot(), searchKey) uri = '%s%s' % (server._uriRoot(), searchKey)
key = '/playlists%s' % utils.joinArgs({ key = '/playlists%s' % utils.joinArgs({
@ -361,7 +368,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
@classmethod @classmethod
def create(cls, server, title, section=None, items=None, smart=False, limit=None, def create(cls, server, title, section=None, items=None, smart=False, limit=None,
sort=None, filters=None, **kwargs): libtype=None, sort=None, filters=None, **kwargs):
""" Create a playlist. """ Create a playlist.
Parameters: Parameters:
@ -373,6 +380,8 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
:class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist. :class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist.
smart (bool): True to create a smart playlist. Default False. smart (bool): True to create a smart playlist. Default False.
limit (int): Smart playlists only, limit the number of items in the playlist. limit (int): Smart playlists only, limit the number of items in the playlist.
libtype (str): Smart playlists only, the specific type of content to filter
(movie, show, season, episode, artist, album, track, photoalbum, photo).
sort (str or list, optional): Smart playlists only, a string of comma separated sort fields sort (str or list, optional): Smart playlists only, a string of comma separated sort fields
or a list of sort fields in the format ``column:dir``. or a list of sort fields in the format ``column:dir``.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
@ -389,7 +398,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
:class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist. :class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist.
""" """
if smart: if smart:
return cls._createSmart(server, title, section, limit, sort, filters, **kwargs) return cls._createSmart(server, title, section, limit, libtype, sort, filters, **kwargs)
else: else:
return cls._create(server, title, items) return cls._create(server, title, items)
@ -455,3 +464,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
raise Unsupported('Unsupported playlist content') raise Unsupported('Unsupported playlist content')
return myplex.sync(sync_item, client=client, clientId=clientId) return myplex.sync(sync_item, client=client, clientId=clientId)
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='playlist', key=self.key)

View file

@ -427,7 +427,7 @@ class PlexServer(PlexObject):
smart (bool): True to create a smart collection. Default False. smart (bool): True to create a smart collection. Default False.
limit (int): Smart collections only, limit the number of items in the collection. limit (int): Smart collections only, limit the number of items in the collection.
libtype (str): Smart collections only, the specific type of content to filter libtype (str): Smart collections only, the specific type of content to filter
(movie, show, season, episode, artist, album, track, photoalbum, photo, collection). (movie, show, season, episode, artist, album, track, photoalbum, photo).
sort (str or list, optional): Smart collections only, a string of comma separated sort fields sort (str or list, optional): Smart collections only, a string of comma separated sort fields
or a list of sort fields in the format ``column:dir``. or a list of sort fields in the format ``column:dir``.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
@ -448,7 +448,7 @@ class PlexServer(PlexObject):
libtype=libtype, sort=sort, filters=filters, **kwargs) libtype=libtype, sort=sort, filters=filters, **kwargs)
def createPlaylist(self, title, section=None, items=None, smart=False, limit=None, def createPlaylist(self, title, section=None, items=None, smart=False, limit=None,
sort=None, filters=None, **kwargs): libtype=None, sort=None, filters=None, **kwargs):
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`. """ Creates and returns a new :class:`~plexapi.playlist.Playlist`.
Parameters: Parameters:
@ -459,6 +459,8 @@ class PlexServer(PlexObject):
:class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist. :class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist.
smart (bool): True to create a smart playlist. Default False. smart (bool): True to create a smart playlist. Default False.
limit (int): Smart playlists only, limit the number of items in the playlist. limit (int): Smart playlists only, limit the number of items in the playlist.
libtype (str): Smart playlists only, the specific type of content to filter
(movie, show, season, episode, artist, album, track, photoalbum, photo).
sort (str or list, optional): Smart playlists only, a string of comma separated sort fields sort (str or list, optional): Smart playlists only, a string of comma separated sort fields
or a list of sort fields in the format ``column:dir``. or a list of sort fields in the format ``column:dir``.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
@ -476,7 +478,7 @@ class PlexServer(PlexObject):
""" """
return Playlist.create( return Playlist.create(
self, title, section=section, items=items, smart=smart, limit=limit, self, title, section=section, items=items, smart=smart, limit=limit,
sort=sort, filters=filters, **kwargs) libtype=libtype, sort=sort, filters=filters, **kwargs)
def createPlayQueue(self, item, **kwargs): def createPlayQueue(self, item, **kwargs):
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`. """ Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.
@ -575,17 +577,29 @@ class PlexServer(PlexObject):
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size'] args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
return results return results
def playlists(self, playlistType=None): def playlists(self, playlistType=None, sectionId=None, title=None, sort=None, **kwargs):
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects on the server. """ Returns a list of all :class:`~plexapi.playlist.Playlist` objects on the server.
Parameters: Parameters:
playlistType (str, optional): The type of playlists to return (audio, video, photo). playlistType (str, optional): The type of playlists to return (audio, video, photo).
Default returns all playlists. Default returns all playlists.
sectionId (int, optional): The section ID (key) of the library to search within.
title (str, optional): General string query to search for. Partial string matches are allowed.
sort (str or list, optional): A string of comma separated sort fields in the format ``column:dir``.
""" """
key = '/playlists' args = {}
if playlistType: if playlistType is not None:
key = '%s?playlistType=%s' % (key, playlistType) args['playlistType'] = playlistType
return self.fetchItems(key) if sectionId is not None:
args['sectionID'] = sectionId
if title is not None:
args['title'] = title
if sort is not None:
# TODO: Automatically retrieve and validate sort field similar to LibrarySection.search()
args['sort'] = sort
key = '/playlists%s' % utils.joinArgs(args)
return self.fetchItems(key, **kwargs)
def playlist(self, title): def playlist(self, title):
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title. """ Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
@ -594,9 +608,12 @@ class PlexServer(PlexObject):
title (str): Title of the playlist to return. title (str): Title of the playlist to return.
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: Invalid playlist title. :exc:`~plexapi.exceptions.NotFound`: Unable to find playlist.
""" """
return self.fetchItem('/playlists', title=title) try:
return self.playlists(title=title, title__iexact=title)[0]
except IndexError:
raise NotFound('Unable to find playlist with title "%s".' % title) from None
def optimizedItems(self, removeAll=None): def optimizedItems(self, removeAll=None):
""" Returns list of all :class:`~plexapi.media.Optimized` objects connected to server. """ """ Returns list of all :class:`~plexapi.media.Optimized` objects connected to server. """
@ -873,6 +890,42 @@ class PlexServer(PlexObject):
key = '/statistics/resources?timespan=6' key = '/statistics/resources?timespan=6'
return self.fetchItems(key, StatisticsResources) return self.fetchItems(key, StatisticsResources)
def _buildWebURL(self, base=None, endpoint=None, **kwargs):
""" Build the Plex Web URL for the object.
Parameters:
base (str): The base URL before the fragment (``#!``).
Default is https://app.plex.tv/desktop.
endpoint (str): The Plex Web URL endpoint.
None for server, 'playlist' for playlists, 'details' for all other media types.
**kwargs (dict): Dictionary of URL parameters.
"""
if base is None:
base = 'https://app.plex.tv/desktop/'
if endpoint:
return '%s#!/server/%s/%s%s' % (
base, self.machineIdentifier, endpoint, utils.joinArgs(kwargs)
)
else:
return '%s#!/media/%s/com.plexapp.plugins.library%s' % (
base, self.machineIdentifier, utils.joinArgs(kwargs)
)
def getWebURL(self, base=None, playlistTab=None):
""" Returns the Plex Web URL for the server.
Parameters:
base (str): The base URL before the fragment (``#!``).
Default is https://app.plex.tv/desktop.
playlistTab (str): The playlist tab (audio, video, photo). Only used for the playlist URL.
"""
if playlistTab is not None:
params = {'source': 'playlists', 'pivot': 'playlists.%s' % playlistTab}
else:
params = {'key': '/hubs', 'pageType': 'hub'}
return self._buildWebURL(base=base, **params)
class Account(PlexObject): class Account(PlexObject):
""" Contains the locally cached MyPlex account information. The properties provided don't """ Contains the locally cached MyPlex account information. The properties provided don't