mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 21:21:15 -07:00
Bump plexapi from 4.13.1 to 4.13.2 (#1939)
* Bump plexapi from 4.13.1 to 4.13.2 Bumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.13.1 to 4.13.2. - [Release notes](https://github.com/pkkid/python-plexapi/releases) - [Commits](https://github.com/pkkid/python-plexapi/compare/4.13.1...4.13.2) --- updated-dependencies: - dependency-name: plexapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * Update plexapi==4.13.2 Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci]
This commit is contained in:
parent
8cd5b0b775
commit
31f6b02149
14 changed files with 250 additions and 107 deletions
|
@ -6,6 +6,7 @@ from xml.etree import ElementTree
|
||||||
|
|
||||||
from plexapi import log, utils
|
from plexapi import log, utils
|
||||||
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
||||||
|
from plexapi.utils import cached_property
|
||||||
|
|
||||||
USER_DONT_RELOAD_FOR_KEYS = set()
|
USER_DONT_RELOAD_FOR_KEYS = set()
|
||||||
_DONT_RELOAD_FOR_KEYS = {'key'}
|
_DONT_RELOAD_FOR_KEYS = {'key'}
|
||||||
|
@ -666,6 +667,13 @@ class PlexPartialObject(PlexObject):
|
||||||
"""
|
"""
|
||||||
return self._getWebURL(base=base)
|
return self._getWebURL(base=base)
|
||||||
|
|
||||||
|
def playQueue(self, *args, **kwargs):
|
||||||
|
""" Returns a new :class:`~plexapi.playqueue.PlayQueue` from this media item.
|
||||||
|
See :func:`~plexapi.playqueue.PlayQueue.create` for available parameters.
|
||||||
|
"""
|
||||||
|
from plexapi.playqueue import PlayQueue
|
||||||
|
return PlayQueue.create(self._server, self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Playable:
|
class Playable:
|
||||||
""" 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.
|
||||||
|
@ -841,7 +849,6 @@ class PlexSession(object):
|
||||||
user = data.find('User')
|
user = data.find('User')
|
||||||
self._username = user.attrib.get('title')
|
self._username = user.attrib.get('title')
|
||||||
self._userId = utils.cast(int, user.attrib.get('id'))
|
self._userId = utils.cast(int, user.attrib.get('id'))
|
||||||
self._user = None # Cache for user object
|
|
||||||
|
|
||||||
# For backwards compatibility
|
# For backwards compatibility
|
||||||
self.players = [self.player] if self.player else []
|
self.players = [self.player] if self.player else []
|
||||||
|
@ -849,18 +856,16 @@ class PlexSession(object):
|
||||||
self.transcodeSessions = [self.transcodeSession] if self.transcodeSession else []
|
self.transcodeSessions = [self.transcodeSession] if self.transcodeSession else []
|
||||||
self.usernames = [self._username] if self._username else []
|
self.usernames = [self._username] if self._username else []
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def user(self):
|
def user(self):
|
||||||
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
|
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
|
||||||
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.
|
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.
|
||||||
"""
|
"""
|
||||||
if self._user is None:
|
|
||||||
myPlexAccount = self._server.myPlexAccount()
|
myPlexAccount = self._server.myPlexAccount()
|
||||||
if self._userId == 1:
|
if self._userId == 1:
|
||||||
self._user = myPlexAccount
|
return myPlexAccount
|
||||||
else:
|
|
||||||
self._user = myPlexAccount.user(self._username)
|
return myPlexAccount.user(self._username)
|
||||||
return self._user
|
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
""" Reload the data for the session.
|
""" Reload the data for the session.
|
||||||
|
|
|
@ -11,7 +11,6 @@ from plexapi.mixins import (
|
||||||
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
LabelMixin
|
LabelMixin
|
||||||
)
|
)
|
||||||
from plexapi.playqueue import PlayQueue
|
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@ -427,10 +426,6 @@ class Collection(
|
||||||
""" Delete the collection. """
|
""" Delete the collection. """
|
||||||
super(Collection, self).delete()
|
super(Collection, self).delete()
|
||||||
|
|
||||||
def playQueue(self, *args, **kwargs):
|
|
||||||
""" Returns a new :class:`~plexapi.playqueue.PlayQueue` from the collection. """
|
|
||||||
return PlayQueue.create(self._server, self.items(), *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, server, title, section, items):
|
def _create(cls, server, title, section, items):
|
||||||
""" Create a regular collection. """
|
""" Create a regular collection. """
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
# Library version
|
# Library version
|
||||||
MAJOR_VERSION = 4
|
MAJOR_VERSION = 4
|
||||||
MINOR_VERSION = 13
|
MINOR_VERSION = 13
|
||||||
PATCH_VERSION = 1
|
PATCH_VERSION = 2
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
|
|
|
@ -7,7 +7,7 @@ from plexapi import X_PLEX_CONTAINER_SIZE, log, media, utils
|
||||||
from plexapi.base import OPERATORS, PlexObject
|
from plexapi.base import OPERATORS, PlexObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.settings import Setting
|
from plexapi.settings import Setting
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import cached_property, deprecated
|
||||||
|
|
||||||
|
|
||||||
class Library(PlexObject):
|
class Library(PlexObject):
|
||||||
|
@ -418,7 +418,6 @@ class LibrarySection(PlexObject):
|
||||||
self._filterTypes = None
|
self._filterTypes = None
|
||||||
self._fieldTypes = None
|
self._fieldTypes = None
|
||||||
self._totalViewSize = None
|
self._totalViewSize = None
|
||||||
self._totalSize = None
|
|
||||||
self._totalDuration = None
|
self._totalDuration = None
|
||||||
self._totalStorage = None
|
self._totalStorage = None
|
||||||
|
|
||||||
|
@ -456,12 +455,10 @@ class LibrarySection(PlexObject):
|
||||||
item.librarySectionID = librarySectionID
|
item.librarySectionID = librarySectionID
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def totalSize(self):
|
def totalSize(self):
|
||||||
""" Returns the total number of items in the library for the default library type. """
|
""" Returns the total number of items in the library for the default library type. """
|
||||||
if self._totalSize is None:
|
return self.totalViewSize(includeCollections=False)
|
||||||
self._totalSize = self.totalViewSize(includeCollections=False)
|
|
||||||
return self._totalSize
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def totalDuration(self):
|
def totalDuration(self):
|
||||||
|
@ -644,12 +641,12 @@ class LibrarySection(PlexObject):
|
||||||
guidLookup = {}
|
guidLookup = {}
|
||||||
for item in library.all():
|
for item in library.all():
|
||||||
guidLookup[item.guid] = item
|
guidLookup[item.guid] = item
|
||||||
guidLookup.update({guid.id for guid in item.guids}}
|
guidLookup.update({guid.id: item for guid in item.guids}}
|
||||||
|
|
||||||
result1 = guidLookup['plex://show/5d9c086c46115600200aa2fe']
|
result1 = guidLookup['plex://show/5d9c086c46115600200aa2fe']
|
||||||
result2 = guidLookup['imdb://tt0944947']
|
result2 = guidLookup['imdb://tt0944947']
|
||||||
result4 = guidLookup['tmdb://1399']
|
result3 = guidLookup['tmdb://1399']
|
||||||
result5 = guidLookup['tvdb://121361']
|
result4 = guidLookup['tvdb://121361']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1671,13 +1668,13 @@ class LibrarySection(PlexObject):
|
||||||
return self.search(libtype='collection', **kwargs)
|
return self.search(libtype='collection', **kwargs)
|
||||||
|
|
||||||
def createPlaylist(self, title, items=None, smart=False, limit=None,
|
def createPlaylist(self, title, items=None, smart=False, limit=None,
|
||||||
sort=None, filters=None, **kwargs):
|
sort=None, filters=None, m3ufilepath=None, **kwargs):
|
||||||
""" Alias for :func:`~plexapi.server.PlexServer.createPlaylist` using this
|
""" Alias for :func:`~plexapi.server.PlexServer.createPlaylist` using this
|
||||||
:class:`~plexapi.library.LibrarySection`.
|
:class:`~plexapi.library.LibrarySection`.
|
||||||
"""
|
"""
|
||||||
return self._server.createPlaylist(
|
return self._server.createPlaylist(
|
||||||
title, section=self, items=items, smart=smart, limit=limit,
|
title, section=self, items=items, smart=smart, limit=limit,
|
||||||
sort=sort, filters=filters, **kwargs)
|
sort=sort, filters=filters, m3ufilepath=m3ufilepath, **kwargs)
|
||||||
|
|
||||||
def playlist(self, title):
|
def playlist(self, title):
|
||||||
""" Returns the playlist with the specified title.
|
""" Returns the playlist with the specified title.
|
||||||
|
|
|
@ -672,6 +672,7 @@ class MediaTag(PlexObject):
|
||||||
role (str): The name of the character role for :class:`~plexapi.media.Role` only.
|
role (str): The name of the character role for :class:`~plexapi.media.Role` only.
|
||||||
tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
|
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).
|
person for Directors and Roles (ex: Animation, Stephen Graham, etc).
|
||||||
|
tagKey (str): Plex GUID for the actor/actress for :class:`~plexapi.media.Role` only.
|
||||||
thumb (str): URL to thumbnail image for :class:`~plexapi.media.Role` only.
|
thumb (str): URL to thumbnail image for :class:`~plexapi.media.Role` only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -687,6 +688,7 @@ class MediaTag(PlexObject):
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.role = data.attrib.get('role')
|
self.role = data.attrib.get('role')
|
||||||
self.tag = data.attrib.get('tag')
|
self.tag = data.attrib.get('tag')
|
||||||
|
self.tagKey = data.attrib.get('tagKey')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
|
||||||
parent = self._parent()
|
parent = self._parent()
|
||||||
|
@ -879,12 +881,15 @@ class Writer(MediaTag):
|
||||||
FILTER = 'writer'
|
FILTER = 'writer'
|
||||||
|
|
||||||
|
|
||||||
class GuidTag(PlexObject):
|
@utils.registerPlexObject
|
||||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
class Guid(PlexObject):
|
||||||
|
""" Represents a single Guid media tag.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
TAG (str): 'Guid'
|
||||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB, MBID).
|
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB, MBID).
|
||||||
"""
|
"""
|
||||||
|
TAG = 'Guid'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
|
@ -893,13 +898,25 @@ class GuidTag(PlexObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Guid(GuidTag):
|
class Rating(PlexObject):
|
||||||
""" Represents a single Guid media tag.
|
""" Represents a single Rating media tag.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Guid'
|
TAG (str): 'Rating'
|
||||||
|
image (str): The uri for the rating image
|
||||||
|
(e.g. ``imdb://image.rating``, ``rottentomatoes://image.rating.ripe``,
|
||||||
|
``rottentomatoes://image.rating.upright``, ``themoviedb://image.rating``).
|
||||||
|
type (str): The type of rating (e.g. audience or critic).
|
||||||
|
value (float): The rating value.
|
||||||
"""
|
"""
|
||||||
TAG = 'Guid'
|
TAG = 'Rating'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
""" Load attribute values from Plex XML response. """
|
||||||
|
self._data = data
|
||||||
|
self.image = data.attrib.get('image')
|
||||||
|
self.type = data.attrib.get('type')
|
||||||
|
self.value = utils.cast(float, data.attrib.get('value'))
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
|
@ -908,7 +925,7 @@ class Review(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Review'
|
TAG (str): 'Review'
|
||||||
filter (str): filter for reviews?
|
filter (str): The library filter for the review.
|
||||||
id (int): The ID of the review.
|
id (int): The ID of the review.
|
||||||
image (str): The image uri for the review.
|
image (str): The image uri for the review.
|
||||||
link (str): The url to the online review.
|
link (str): The url to the online review.
|
||||||
|
@ -983,18 +1000,34 @@ class Chapter(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Chapter'
|
TAG (str): 'Chapter'
|
||||||
|
end (int): The end time of the chapter in milliseconds.
|
||||||
|
filter (str): The library filter for the chapter.
|
||||||
|
id (int): The ID of the chapter.
|
||||||
|
index (int): The index of the chapter.
|
||||||
|
tag (str): The name of the chapter.
|
||||||
|
title (str): The title of the chapter.
|
||||||
|
thumb (str): The URL to retrieve the chapter thumbnail.
|
||||||
|
start (int): The start time of the chapter in milliseconds.
|
||||||
"""
|
"""
|
||||||
TAG = 'Chapter'
|
TAG = 'Chapter'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
name = self._clean(self.firstAttr('tag'))
|
||||||
|
start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
|
||||||
|
end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end')))
|
||||||
|
offsets = f'{start}-{end}'
|
||||||
|
return f"<{':'.join([self.__class__.__name__, name, offsets])}>"
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
|
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||||
|
self.filter = data.attrib.get('filter')
|
||||||
self.id = utils.cast(int, data.attrib.get('id', 0))
|
self.id = utils.cast(int, data.attrib.get('id', 0))
|
||||||
self.filter = data.attrib.get('filter') # I couldn't filter on it anyways
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.tag = data.attrib.get('tag')
|
self.tag = data.attrib.get('tag')
|
||||||
self.title = self.tag
|
self.title = self.tag
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
||||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
|
@ -1003,6 +1036,10 @@ class Marker(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Marker'
|
TAG (str): 'Marker'
|
||||||
|
end (int): The end time of the marker in milliseconds.
|
||||||
|
id (int): The ID of the marker.
|
||||||
|
type (str): The type of marker.
|
||||||
|
start (int): The start time of the marker in milliseconds.
|
||||||
"""
|
"""
|
||||||
TAG = 'Marker'
|
TAG = 'Marker'
|
||||||
|
|
||||||
|
@ -1015,10 +1052,10 @@ class Marker(PlexObject):
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
|
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||||
self.id = utils.cast(int, data.attrib.get('id'))
|
self.id = utils.cast(int, data.attrib.get('id'))
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
||||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
|
@ -1027,13 +1064,15 @@ class Field(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Field'
|
TAG (str): 'Field'
|
||||||
|
locked (bool): True if the field is locked.
|
||||||
|
name (str): The name of the field.
|
||||||
"""
|
"""
|
||||||
TAG = 'Field'
|
TAG = 'Field'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
self.name = data.attrib.get('name')
|
|
||||||
self.locked = utils.cast(bool, data.attrib.get('locked'))
|
self.locked = utils.cast(bool, data.attrib.get('locked'))
|
||||||
|
self.name = data.attrib.get('name')
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
|
|
|
@ -5,7 +5,7 @@ from urllib.parse import parse_qsl, quote_plus, unquote, urlencode, urlsplit
|
||||||
|
|
||||||
from plexapi import media, settings, utils
|
from plexapi import media, settings, utils
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated, openOrRead
|
||||||
|
|
||||||
|
|
||||||
class AdvancedSettingsMixin:
|
class AdvancedSettingsMixin:
|
||||||
|
@ -341,14 +341,14 @@ class ArtMixin(ArtUrlMixin):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
url (str): The full URL to the image to upload.
|
url (str): The full URL to the image to upload.
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload or file-like object.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}'
|
key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = f'/library/metadata/{self.ratingKey}/arts'
|
key = f'/library/metadata/{self.ratingKey}/arts'
|
||||||
data = open(filepath, 'rb').read()
|
data = openOrRead(filepath)
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -392,14 +392,14 @@ class BannerMixin(BannerUrlMixin):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
url (str): The full URL to the image to upload.
|
url (str): The full URL to the image to upload.
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload or file-like object.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = f'/library/metadata/{self.ratingKey}/banners?url={quote_plus(url)}'
|
key = f'/library/metadata/{self.ratingKey}/banners?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = f'/library/metadata/{self.ratingKey}/banners'
|
key = f'/library/metadata/{self.ratingKey}/banners'
|
||||||
data = open(filepath, 'rb').read()
|
data = openOrRead(filepath)
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -448,14 +448,14 @@ class PosterMixin(PosterUrlMixin):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
url (str): The full URL to the image to upload.
|
url (str): The full URL to the image to upload.
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload or file-like object.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}'
|
key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = f'/library/metadata/{self.ratingKey}/posters'
|
key = f'/library/metadata/{self.ratingKey}/posters'
|
||||||
data = open(filepath, 'rb').read()
|
data = openOrRead(filepath)
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -494,22 +494,24 @@ class ThemeMixin(ThemeUrlMixin):
|
||||||
""" Returns list of available :class:`~plexapi.media.Theme` objects. """
|
""" Returns list of available :class:`~plexapi.media.Theme` objects. """
|
||||||
return self.fetchItems(f'/library/metadata/{self.ratingKey}/themes', cls=media.Theme)
|
return self.fetchItems(f'/library/metadata/{self.ratingKey}/themes', cls=media.Theme)
|
||||||
|
|
||||||
def uploadTheme(self, url=None, filepath=None):
|
def uploadTheme(self, url=None, filepath=None, timeout=None):
|
||||||
""" Upload a theme from url or filepath.
|
""" Upload a theme from url or filepath.
|
||||||
|
|
||||||
Warning: Themes cannot be deleted using PlexAPI!
|
Warning: Themes cannot be deleted using PlexAPI!
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
url (str): The full URL to the theme to upload.
|
url (str): The full URL to the theme to upload.
|
||||||
filepath (str): The full file path to the theme to upload.
|
filepath (str): The full file path to the theme to upload or file-like object.
|
||||||
|
timeout (int, optional): Timeout, in seconds, to use when uploading themes to the server.
|
||||||
|
(default config.TIMEOUT).
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = f'/library/metadata/{self.ratingKey}/themes?url={quote_plus(url)}'
|
key = f'/library/metadata/{self.ratingKey}/themes?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post, timeout=timeout)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = f'/library/metadata/{self.ratingKey}/themes'
|
key = f'/library/metadata/{self.ratingKey}/themes'
|
||||||
data = open(filepath, 'rb').read()
|
data = openOrRead(filepath)
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data, timeout=timeout)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def setTheme(self, theme):
|
def setTheme(self, theme):
|
||||||
|
|
|
@ -32,6 +32,7 @@ class MyPlexAccount(PlexObject):
|
||||||
session (requests.Session, optional): Use your own session object if you want to
|
session (requests.Session, optional): Use your own session object if you want to
|
||||||
cache the http responses from PMS
|
cache the http responses from PMS
|
||||||
timeout (int): timeout in seconds on initial connect to myplex (default config.TIMEOUT).
|
timeout (int): timeout in seconds on initial connect to myplex (default config.TIMEOUT).
|
||||||
|
code (str): Two-factor authentication code to use when logging in.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
SIGNIN (str): 'https://plex.tv/users/sign_in.xml'
|
SIGNIN (str): 'https://plex.tv/users/sign_in.xml'
|
||||||
|
@ -88,19 +89,21 @@ class MyPlexAccount(PlexObject):
|
||||||
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
|
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
|
||||||
key = 'https://plex.tv/users/account'
|
key = 'https://plex.tv/users/account'
|
||||||
|
|
||||||
def __init__(self, username=None, password=None, token=None, session=None, timeout=None):
|
def __init__(self, username=None, password=None, token=None, session=None, timeout=None, code=None):
|
||||||
self._token = token or CONFIG.get('auth.server_token')
|
self._token = token or CONFIG.get('auth.server_token')
|
||||||
self._session = session or requests.Session()
|
self._session = session or requests.Session()
|
||||||
self._sonos_cache = []
|
self._sonos_cache = []
|
||||||
self._sonos_cache_timestamp = 0
|
self._sonos_cache_timestamp = 0
|
||||||
data, initpath = self._signin(username, password, timeout)
|
data, initpath = self._signin(username, password, code, timeout)
|
||||||
super(MyPlexAccount, self).__init__(self, data, initpath)
|
super(MyPlexAccount, self).__init__(self, data, initpath)
|
||||||
|
|
||||||
def _signin(self, username, password, timeout):
|
def _signin(self, username, password, code, timeout):
|
||||||
if self._token:
|
if self._token:
|
||||||
return self.query(self.key), self.key
|
return self.query(self.key), self.key
|
||||||
username = username or CONFIG.get('auth.myplex_username')
|
username = username or CONFIG.get('auth.myplex_username')
|
||||||
password = password or CONFIG.get('auth.myplex_password')
|
password = password or CONFIG.get('auth.myplex_password')
|
||||||
|
if code:
|
||||||
|
password += code
|
||||||
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password), timeout=timeout)
|
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password), timeout=timeout)
|
||||||
return data, self.SIGNIN
|
return data, self.SIGNIN
|
||||||
|
|
||||||
|
@ -390,12 +393,13 @@ class MyPlexAccount(PlexObject):
|
||||||
url = self.HOMEUSER.format(userId=user.id)
|
url = self.HOMEUSER.format(userId=user.id)
|
||||||
return self.query(url, self._session.delete)
|
return self.query(url, self._session.delete)
|
||||||
|
|
||||||
def switchHomeUser(self, user):
|
def switchHomeUser(self, user, pin=None):
|
||||||
""" Returns a new :class:`~plexapi.myplex.MyPlexAccount` object switched to the given home user.
|
""" Returns a new :class:`~plexapi.myplex.MyPlexAccount` object switched to the given home user.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
user (:class:`~plexapi.myplex.MyPlexUser` or str): :class:`~plexapi.myplex.MyPlexUser`,
|
user (:class:`~plexapi.myplex.MyPlexUser` or str): :class:`~plexapi.myplex.MyPlexUser`,
|
||||||
username, or email of the home user to switch to.
|
username, or email of the home user to switch to.
|
||||||
|
pin (str): PIN for the home user (required if the home user has a PIN set).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -410,9 +414,12 @@ class MyPlexAccount(PlexObject):
|
||||||
"""
|
"""
|
||||||
user = user if isinstance(user, MyPlexUser) else self.user(user)
|
user = user if isinstance(user, MyPlexUser) else self.user(user)
|
||||||
url = f'{self.HOMEUSERS}/{user.id}/switch'
|
url = f'{self.HOMEUSERS}/{user.id}/switch'
|
||||||
data = self.query(url, self._session.post)
|
params = {}
|
||||||
|
if pin:
|
||||||
|
params['pin'] = pin
|
||||||
|
data = self.query(url, self._session.post, params=params)
|
||||||
userToken = data.attrib.get('authenticationToken')
|
userToken = data.attrib.get('authenticationToken')
|
||||||
return MyPlexAccount(token=userToken)
|
return MyPlexAccount(token=userToken, session=self._session)
|
||||||
|
|
||||||
def setPin(self, newPin, currentPin=None):
|
def setPin(self, newPin, currentPin=None):
|
||||||
""" Set a new Plex Home PIN for the account.
|
""" Set a new Plex Home PIN for the account.
|
||||||
|
@ -861,7 +868,12 @@ class MyPlexAccount(PlexObject):
|
||||||
results += subresults[:maxresults - len(results)]
|
results += subresults[:maxresults - len(results)]
|
||||||
params['X-Plex-Container-Start'] += params['X-Plex-Container-Size']
|
params['X-Plex-Container-Start'] += params['X-Plex-Container-Size']
|
||||||
|
|
||||||
return self._toOnlineMetadata(results)
|
# totalSize is available in first response, update maxresults from it
|
||||||
|
totalSize = utils.cast(int, data.attrib.get('totalSize'))
|
||||||
|
if maxresults > totalSize:
|
||||||
|
maxresults = totalSize
|
||||||
|
|
||||||
|
return self._toOnlineMetadata(results, **kwargs)
|
||||||
|
|
||||||
def onWatchlist(self, item):
|
def onWatchlist(self, item):
|
||||||
""" Returns True if the item is on the user's watchlist.
|
""" Returns True if the item is on the user's watchlist.
|
||||||
|
@ -1005,21 +1017,24 @@ class MyPlexAccount(PlexObject):
|
||||||
data = {'code': pin}
|
data = {'code': pin}
|
||||||
self.query(self.LINK, self._session.put, headers=headers, data=data)
|
self.query(self.LINK, self._session.put, headers=headers, data=data)
|
||||||
|
|
||||||
def _toOnlineMetadata(self, objs):
|
def _toOnlineMetadata(self, objs, **kwargs):
|
||||||
""" Convert a list of media objects to online metadata objects. """
|
""" Convert a list of media objects to online metadata objects. """
|
||||||
# TODO: Add proper support for metadata.provider.plex.tv
|
# TODO: Add proper support for metadata.provider.plex.tv
|
||||||
# Temporary workaround to allow reloading and browsing of online media objects
|
# Temporary workaround to allow reloading and browsing of online media objects
|
||||||
server = PlexServer(self.METADATA, self._token)
|
server = PlexServer(self.METADATA, self._token, session=self._session)
|
||||||
|
|
||||||
|
includeUserState = int(bool(kwargs.pop('includeUserState', True)))
|
||||||
|
|
||||||
if not isinstance(objs, list):
|
if not isinstance(objs, list):
|
||||||
objs = [objs]
|
objs = [objs]
|
||||||
|
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
obj._server = server
|
obj._server = server
|
||||||
|
|
||||||
# Parse details key to modify query string
|
# Parse details key to modify query string
|
||||||
url = urlsplit(obj._details_key)
|
url = urlsplit(obj._details_key)
|
||||||
query = dict(parse_qsl(url.query))
|
query = dict(parse_qsl(url.query))
|
||||||
query['includeUserState'] = 1
|
query['includeUserState'] = includeUserState
|
||||||
query.pop('includeFields', None)
|
query.pop('includeFields', None)
|
||||||
obj._details_key = urlunsplit((url.scheme, url.netloc, url.path, urlencode(query), url.fragment))
|
obj._details_key = urlunsplit((url.scheme, url.netloc, url.path, urlencode(query), url.fragment))
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,8 @@ from urllib.parse import quote_plus, unquote
|
||||||
from plexapi import media, 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, MusicSection
|
||||||
from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin
|
from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin
|
||||||
from plexapi.playqueue import PlayQueue
|
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@ -330,10 +329,6 @@ class Playlist(
|
||||||
""" Delete the playlist. """
|
""" Delete the playlist. """
|
||||||
self._server.query(self.key, method=self._server._session.delete)
|
self._server.query(self.key, method=self._server._session.delete)
|
||||||
|
|
||||||
def playQueue(self, *args, **kwargs):
|
|
||||||
""" Returns a new :class:`~plexapi.playqueue.PlayQueue` from the playlist. """
|
|
||||||
return PlayQueue.create(self._server, self, *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, server, title, items):
|
def _create(cls, server, title, items):
|
||||||
""" Create a regular playlist. """
|
""" Create a regular playlist. """
|
||||||
|
@ -375,15 +370,32 @@ class Playlist(
|
||||||
data = server.query(key, method=server._session.post)[0]
|
data = server.query(key, method=server._session.post)[0]
|
||||||
return cls(server, data, initpath=key)
|
return cls(server, data, initpath=key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _createFromM3U(cls, server, title, section, m3ufilepath):
|
||||||
|
""" Create a playlist from uploading an m3u file. """
|
||||||
|
if not isinstance(section, LibrarySection):
|
||||||
|
section = server.library.section(section)
|
||||||
|
|
||||||
|
if not isinstance(section, MusicSection):
|
||||||
|
raise BadRequest('Can only create playlists from m3u files in a music library.')
|
||||||
|
|
||||||
|
args = {'sectionID': section.key, 'path': m3ufilepath}
|
||||||
|
key = f"/playlists/upload{utils.joinArgs(args)}"
|
||||||
|
server.query(key, method=server._session.post)
|
||||||
|
try:
|
||||||
|
return server.playlists(sectionId=section.key, guid__endswith=m3ufilepath)[0].edit(title=title).reload()
|
||||||
|
except IndexError:
|
||||||
|
raise BadRequest('Failed to create playlist from m3u file.') from None
|
||||||
|
|
||||||
@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,
|
||||||
libtype=None, sort=None, filters=None, **kwargs):
|
libtype=None, sort=None, filters=None, m3ufilepath=None, **kwargs):
|
||||||
""" Create a playlist.
|
""" Create a playlist.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on.
|
server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on.
|
||||||
title (str): Title of the playlist.
|
title (str): Title of the playlist.
|
||||||
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only,
|
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
|
||||||
the library section to create the playlist in.
|
the library section to create the playlist in.
|
||||||
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
|
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
|
||||||
: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.
|
||||||
|
@ -396,17 +408,23 @@ class Playlist(
|
||||||
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
||||||
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
|
m3ufilepath (str): Music playlists only, the full file path to an m3u file to import.
|
||||||
|
Note: This will overwrite any playlist previously created from the same m3u file.
|
||||||
**kwargs (dict): Smart playlists only, additional custom filters to apply to the
|
**kwargs (dict): Smart playlists only, additional custom filters to apply to the
|
||||||
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
|
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
|
||||||
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
|
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
|
||||||
|
:class:`plexapi.exceptions.BadRequest`: When attempting to import m3u file into non-music library.
|
||||||
|
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
: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 m3ufilepath:
|
||||||
|
return cls._createFromM3U(server, title, section, m3ufilepath)
|
||||||
|
elif smart:
|
||||||
return cls._createSmart(server, title, section, limit, libtype, 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)
|
||||||
|
|
|
@ -150,8 +150,8 @@ class PlayQueue(PlexObject):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
|
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
|
||||||
items (:class:`~plexapi.base.Playable` or :class:`~plexapi.playlist.Playlist`):
|
items (:class:`~plexapi.base.PlexPartialObject`):
|
||||||
A media item, list of media items, or Playlist.
|
A media item or a list of media items.
|
||||||
startItem (:class:`~plexapi.base.Playable`, optional):
|
startItem (:class:`~plexapi.base.Playable`, optional):
|
||||||
Media item in the PlayQueue where playback should begin.
|
Media item in the PlayQueue where playback should begin.
|
||||||
shuffle (int, optional): Start the playqueue shuffled.
|
shuffle (int, optional): Start the playqueue shuffled.
|
||||||
|
@ -174,16 +174,13 @@ class PlayQueue(PlexObject):
|
||||||
uri_args = quote_plus(f"/library/metadata/{item_keys}")
|
uri_args = quote_plus(f"/library/metadata/{item_keys}")
|
||||||
args["uri"] = f"library:///directory/{uri_args}"
|
args["uri"] = f"library:///directory/{uri_args}"
|
||||||
args["type"] = items[0].listType
|
args["type"] = items[0].listType
|
||||||
elif items.type == "playlist":
|
|
||||||
args["type"] = items.playlistType
|
|
||||||
if items.radio:
|
|
||||||
args["uri"] = f"server://{server.machineIdentifier}/{server.library.identifier}{items.key}"
|
|
||||||
else:
|
else:
|
||||||
|
if items.type == "playlist":
|
||||||
|
args["type"] = items.playlistType
|
||||||
args["playlistID"] = items.ratingKey
|
args["playlistID"] = items.ratingKey
|
||||||
else:
|
else:
|
||||||
uuid = items.section().uuid
|
|
||||||
args["type"] = items.listType
|
args["type"] = items.listType
|
||||||
args["uri"] = f"library://{uuid}/item/{items.key}"
|
args["uri"] = f"server://{server.machineIdentifier}/{server.library.identifier}{items.key}"
|
||||||
|
|
||||||
if startItem:
|
if startItem:
|
||||||
args["key"] = startItem.key
|
args["key"] = startItem.key
|
||||||
|
|
|
@ -17,7 +17,7 @@ from plexapi.media import Conversion, Optimized
|
||||||
from plexapi.playlist import Playlist
|
from plexapi.playlist import Playlist
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.settings import Settings
|
from plexapi.settings import Settings
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import cached_property, deprecated
|
||||||
from requests.status_codes import _codes as codes
|
from requests.status_codes import _codes as codes
|
||||||
|
|
||||||
# Need these imports to populate utils.PLEXOBJECTS
|
# Need these imports to populate utils.PLEXOBJECTS
|
||||||
|
@ -109,8 +109,6 @@ class PlexServer(PlexObject):
|
||||||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||||
self._session = session or requests.Session()
|
self._session = session or requests.Session()
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._library = None # cached library
|
|
||||||
self._settings = None # cached settings
|
|
||||||
self._myPlexAccount = None # cached myPlexAccount
|
self._myPlexAccount = None # cached myPlexAccount
|
||||||
self._systemAccounts = None # cached list of SystemAccount
|
self._systemAccounts = None # cached list of SystemAccount
|
||||||
self._systemDevices = None # cached list of SystemDevice
|
self._systemDevices = None # cached list of SystemDevice
|
||||||
|
@ -173,27 +171,22 @@ class PlexServer(PlexObject):
|
||||||
def _uriRoot(self):
|
def _uriRoot(self):
|
||||||
return f'server://{self.machineIdentifier}/com.plexapp.plugins.library'
|
return f'server://{self.machineIdentifier}/com.plexapp.plugins.library'
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def library(self):
|
def library(self):
|
||||||
""" Library to browse or search your media. """
|
""" Library to browse or search your media. """
|
||||||
if not self._library:
|
|
||||||
try:
|
try:
|
||||||
data = self.query(Library.key)
|
data = self.query(Library.key)
|
||||||
self._library = Library(self, data)
|
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
data = self.query('/library/sections/')
|
|
||||||
# Only the owner has access to /library
|
# Only the owner has access to /library
|
||||||
# so just return the library without the data.
|
# so just return the library without the data.
|
||||||
|
data = self.query('/library/sections/')
|
||||||
return Library(self, data)
|
return Library(self, data)
|
||||||
return self._library
|
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def settings(self):
|
def settings(self):
|
||||||
""" Returns a list of all server settings. """
|
""" Returns a list of all server settings. """
|
||||||
if not self._settings:
|
|
||||||
data = self.query(Settings.key)
|
data = self.query(Settings.key)
|
||||||
self._settings = Settings(self, data)
|
return Settings(self, data)
|
||||||
return self._settings
|
|
||||||
|
|
||||||
def account(self):
|
def account(self):
|
||||||
""" Returns the :class:`~plexapi.server.Account` object this server belongs to. """
|
""" Returns the :class:`~plexapi.server.Account` object this server belongs to. """
|
||||||
|
@ -318,7 +311,7 @@ class PlexServer(PlexObject):
|
||||||
"""
|
"""
|
||||||
if self._myPlexAccount is None:
|
if self._myPlexAccount is None:
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
self._myPlexAccount = MyPlexAccount(token=self._token)
|
self._myPlexAccount = MyPlexAccount(token=self._token, session=self._session)
|
||||||
return self._myPlexAccount
|
return self._myPlexAccount
|
||||||
|
|
||||||
def _myPlexClientPorts(self):
|
def _myPlexClientPorts(self):
|
||||||
|
@ -454,19 +447,42 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`~plexapi.collection.Collection`: A new instance of the created Collection.
|
:class:`~plexapi.collection.Collection`: A new instance of the created Collection.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Create a regular collection
|
||||||
|
movies = plex.library.section("Movies")
|
||||||
|
movie1 = movies.get("Big Buck Bunny")
|
||||||
|
movie2 = movies.get("Sita Sings the Blues")
|
||||||
|
collection = plex.createCollection(
|
||||||
|
title="Favorite Movies",
|
||||||
|
section=movies,
|
||||||
|
items=[movie1, movie2]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a smart collection
|
||||||
|
collection = plex.createCollection(
|
||||||
|
title="Recently Aired Comedy TV Shows",
|
||||||
|
section="TV Shows",
|
||||||
|
smart=True,
|
||||||
|
sort="episode.originallyAvailableAt:desc",
|
||||||
|
filters={"episode.originallyAvailableAt>>": "4w", "genre": "comedy"}
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
return Collection.create(
|
return Collection.create(
|
||||||
self, title, section, items=items, smart=smart, limit=limit,
|
self, title, section, items=items, smart=smart, limit=limit,
|
||||||
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,
|
||||||
libtype=None, sort=None, filters=None, **kwargs):
|
libtype=None, sort=None, filters=None, m3ufilepath=None, **kwargs):
|
||||||
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`.
|
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the playlist.
|
title (str): Title of the playlist.
|
||||||
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only,
|
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
|
||||||
library section to create the playlist in.
|
the library section to create the playlist in.
|
||||||
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
|
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
|
||||||
: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.
|
||||||
|
@ -478,19 +494,51 @@ class PlexServer(PlexObject):
|
||||||
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
||||||
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
|
m3ufilepath (str): Music playlists only, the full file path to an m3u file to import.
|
||||||
|
Note: This will overwrite any playlist previously created from the same m3u file.
|
||||||
**kwargs (dict): Smart playlists only, additional custom filters to apply to the
|
**kwargs (dict): Smart playlists only, additional custom filters to apply to the
|
||||||
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
|
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
|
||||||
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
|
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
|
||||||
|
:class:`plexapi.exceptions.BadRequest`: When attempting to import m3u file into non-music library.
|
||||||
|
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist.
|
:class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Create a regular playlist
|
||||||
|
episodes = plex.library.section("TV Shows").get("Game of Thrones").episodes()
|
||||||
|
playlist = plex.createPlaylist(
|
||||||
|
title="GoT Episodes",
|
||||||
|
items=episodes
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a smart playlist
|
||||||
|
playlist = plex.createPlaylist(
|
||||||
|
title="Top 10 Unwatched Movies",
|
||||||
|
section="Movies",
|
||||||
|
smart=True,
|
||||||
|
limit=10,
|
||||||
|
sort="audienceRating:desc",
|
||||||
|
filters={"audienceRating>>": 8.0, "unwatched": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a music playlist from an m3u file
|
||||||
|
playlist = plex.createPlaylist(
|
||||||
|
title="Favorite Tracks",
|
||||||
|
section="Music",
|
||||||
|
m3ufilepath="/path/to/playlist.m3u"
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
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,
|
||||||
libtype=libtype, sort=sort, filters=filters, **kwargs)
|
libtype=libtype, sort=sort, filters=filters, m3ufilepath=m3ufilepath, **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`.
|
||||||
|
|
|
@ -139,7 +139,14 @@ class Setting(PlexObject):
|
||||||
if not enumstr:
|
if not enumstr:
|
||||||
return None
|
return None
|
||||||
if ':' in enumstr:
|
if ':' in enumstr:
|
||||||
return {self._cast(k): v for k, v in [kv.split(':') for kv in enumstr.split('|')]}
|
d = {}
|
||||||
|
for kv in enumstr.split('|'):
|
||||||
|
try:
|
||||||
|
k, v = kv.split(':')
|
||||||
|
d[self._cast(k)] = v
|
||||||
|
except ValueError:
|
||||||
|
d[self._cast(kv)] = kv
|
||||||
|
return d
|
||||||
return enumstr.split('|')
|
return enumstr.split('|')
|
||||||
|
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
|
|
|
@ -24,6 +24,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
tqdm = None
|
tqdm = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import cached_property
|
||||||
|
except ImportError:
|
||||||
|
from backports.cached_property import cached_property # noqa: F401
|
||||||
|
|
||||||
log = logging.getLogger('plexapi')
|
log = logging.getLogger('plexapi')
|
||||||
|
|
||||||
# Search Types - Plex uses these to filter specific media types when searching.
|
# Search Types - Plex uses these to filter specific media types when searching.
|
||||||
|
@ -618,3 +623,10 @@ def toJson(obj, **kwargs):
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
|
return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
|
||||||
return json.dumps(obj, default=serialize, **kwargs)
|
return json.dumps(obj, default=serialize, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def openOrRead(file):
|
||||||
|
if hasattr(file, 'read'):
|
||||||
|
return file.read()
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
|
@ -323,6 +323,7 @@ class Movie(
|
||||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||||
rating (float): Movie critic rating (7.9; 9.8; 8.1).
|
rating (float): Movie critic rating (7.9; 9.8; 8.1).
|
||||||
ratingImage (str): Key to critic rating image (rottentomatoes://image.rating.rotten).
|
ratingImage (str): Key to critic rating image (rottentomatoes://image.rating.rotten).
|
||||||
|
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
|
||||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||||
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
|
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
|
||||||
|
@ -363,6 +364,7 @@ class Movie(
|
||||||
self.producers = self.findItems(data, media.Producer)
|
self.producers = self.findItems(data, media.Producer)
|
||||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
self.ratingImage = data.attrib.get('ratingImage')
|
self.ratingImage = data.attrib.get('ratingImage')
|
||||||
|
self.ratings = self.findItems(data, media.Rating)
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
self.studio = data.attrib.get('studio')
|
self.studio = data.attrib.get('studio')
|
||||||
|
@ -459,6 +461,7 @@ class Show(
|
||||||
originallyAvailableAt (datetime): Datetime the show was released.
|
originallyAvailableAt (datetime): Datetime the show was released.
|
||||||
originalTitle (str): The original title of the show.
|
originalTitle (str): The original title of the show.
|
||||||
rating (float): Show rating (7.9; 9.8; 8.1).
|
rating (float): Show rating (7.9; 9.8; 8.1).
|
||||||
|
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
|
||||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||||
showOrdering (str): Setting that indicates the episode ordering for the show
|
showOrdering (str): Setting that indicates the episode ordering for the show
|
||||||
(None = Library default).
|
(None = Library default).
|
||||||
|
@ -503,6 +506,7 @@ class Show(
|
||||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
self.originalTitle = data.attrib.get('originalTitle')
|
self.originalTitle = data.attrib.get('originalTitle')
|
||||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
|
self.ratings = self.findItems(data, media.Rating)
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
self.showOrdering = data.attrib.get('showOrdering')
|
self.showOrdering = data.attrib.get('showOrdering')
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
|
@ -639,6 +643,7 @@ class Season(
|
||||||
parentTheme (str): URL to show theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
|
parentTheme (str): URL to show theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
|
||||||
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||||
parentTitle (str): Name of the show for the season.
|
parentTitle (str): Name of the show for the season.
|
||||||
|
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
|
||||||
viewedLeafCount (int): Number of items marked as played in the season view.
|
viewedLeafCount (int): Number of items marked as played in the season view.
|
||||||
year (int): Year the season was released.
|
year (int): Year the season was released.
|
||||||
"""
|
"""
|
||||||
|
@ -663,6 +668,7 @@ class Season(
|
||||||
self.parentTheme = data.attrib.get('parentTheme')
|
self.parentTheme = data.attrib.get('parentTheme')
|
||||||
self.parentThumb = data.attrib.get('parentThumb')
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
|
self.ratings = self.findItems(data, media.Rating)
|
||||||
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'))
|
||||||
|
|
||||||
|
@ -800,6 +806,7 @@ class Episode(
|
||||||
parentYear (int): Year the season was released.
|
parentYear (int): Year the season was released.
|
||||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||||
rating (float): Episode rating (7.9; 9.8; 8.1).
|
rating (float): Episode rating (7.9; 9.8; 8.1).
|
||||||
|
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
|
||||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||||
skipParent (bool): True if the show's seasons are set to hidden.
|
skipParent (bool): True if the show's seasons are set to hidden.
|
||||||
viewOffset (int): View offset in milliseconds.
|
viewOffset (int): View offset in milliseconds.
|
||||||
|
@ -845,6 +852,7 @@ class Episode(
|
||||||
self.parentYear = utils.cast(int, data.attrib.get('parentYear'))
|
self.parentYear = utils.cast(int, data.attrib.get('parentYear'))
|
||||||
self.producers = self.findItems(data, media.Producer)
|
self.producers = self.findItems(data, media.Producer)
|
||||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
|
self.ratings = self.findItems(data, media.Rating)
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
||||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||||
|
|
|
@ -27,7 +27,7 @@ MarkupSafe==2.1.1
|
||||||
musicbrainzngs==0.7.1
|
musicbrainzngs==0.7.1
|
||||||
packaging==22.0
|
packaging==22.0
|
||||||
paho-mqtt==1.6.1
|
paho-mqtt==1.6.1
|
||||||
plexapi==4.13.1
|
plexapi==4.13.2
|
||||||
portend==3.1.0
|
portend==3.1.0
|
||||||
profilehooks==1.12.0
|
profilehooks==1.12.0
|
||||||
PyJWT==2.6.0
|
PyJWT==2.6.0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue