mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11: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.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
||||
from plexapi.utils import cached_property
|
||||
|
||||
USER_DONT_RELOAD_FOR_KEYS = set()
|
||||
_DONT_RELOAD_FOR_KEYS = {'key'}
|
||||
|
@ -666,6 +667,13 @@ class PlexPartialObject(PlexObject):
|
|||
"""
|
||||
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:
|
||||
""" 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')
|
||||
self._username = user.attrib.get('title')
|
||||
self._userId = utils.cast(int, user.attrib.get('id'))
|
||||
self._user = None # Cache for user object
|
||||
|
||||
# For backwards compatibility
|
||||
self.players = [self.player] if self.player else []
|
||||
|
@ -849,18 +856,16 @@ class PlexSession(object):
|
|||
self.transcodeSessions = [self.transcodeSession] if self.transcodeSession else []
|
||||
self.usernames = [self._username] if self._username else []
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def user(self):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
|
||||
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.
|
||||
"""
|
||||
if self._user is None:
|
||||
myPlexAccount = self._server.myPlexAccount()
|
||||
if self._userId == 1:
|
||||
self._user = myPlexAccount
|
||||
else:
|
||||
self._user = myPlexAccount.user(self._username)
|
||||
return self._user
|
||||
return myPlexAccount
|
||||
|
||||
return myPlexAccount.user(self._username)
|
||||
|
||||
def reload(self):
|
||||
""" Reload the data for the session.
|
||||
|
|
|
@ -11,7 +11,6 @@ from plexapi.mixins import (
|
|||
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
LabelMixin
|
||||
)
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
||||
|
@ -427,10 +426,6 @@ class Collection(
|
|||
""" Delete the collection. """
|
||||
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
|
||||
def _create(cls, server, title, section, items):
|
||||
""" Create a regular collection. """
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
# Library version
|
||||
MAJOR_VERSION = 4
|
||||
MINOR_VERSION = 13
|
||||
PATCH_VERSION = 1
|
||||
PATCH_VERSION = 2
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_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.exceptions import BadRequest, NotFound
|
||||
from plexapi.settings import Setting
|
||||
from plexapi.utils import deprecated
|
||||
from plexapi.utils import cached_property, deprecated
|
||||
|
||||
|
||||
class Library(PlexObject):
|
||||
|
@ -418,7 +418,6 @@ class LibrarySection(PlexObject):
|
|||
self._filterTypes = None
|
||||
self._fieldTypes = None
|
||||
self._totalViewSize = None
|
||||
self._totalSize = None
|
||||
self._totalDuration = None
|
||||
self._totalStorage = None
|
||||
|
||||
|
@ -456,12 +455,10 @@ class LibrarySection(PlexObject):
|
|||
item.librarySectionID = librarySectionID
|
||||
return items
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def totalSize(self):
|
||||
""" Returns the total number of items in the library for the default library type. """
|
||||
if self._totalSize is None:
|
||||
self._totalSize = self.totalViewSize(includeCollections=False)
|
||||
return self._totalSize
|
||||
return self.totalViewSize(includeCollections=False)
|
||||
|
||||
@property
|
||||
def totalDuration(self):
|
||||
|
@ -644,12 +641,12 @@ class LibrarySection(PlexObject):
|
|||
guidLookup = {}
|
||||
for item in library.all():
|
||||
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']
|
||||
result2 = guidLookup['imdb://tt0944947']
|
||||
result4 = guidLookup['tmdb://1399']
|
||||
result5 = guidLookup['tvdb://121361']
|
||||
result3 = guidLookup['tmdb://1399']
|
||||
result4 = guidLookup['tvdb://121361']
|
||||
|
||||
"""
|
||||
|
||||
|
@ -1671,13 +1668,13 @@ class LibrarySection(PlexObject):
|
|||
return self.search(libtype='collection', **kwargs)
|
||||
|
||||
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
|
||||
:class:`~plexapi.library.LibrarySection`.
|
||||
"""
|
||||
return self._server.createPlaylist(
|
||||
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):
|
||||
""" 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.
|
||||
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).
|
||||
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.
|
||||
"""
|
||||
|
||||
|
@ -687,6 +688,7 @@ class MediaTag(PlexObject):
|
|||
self.key = data.attrib.get('key')
|
||||
self.role = data.attrib.get('role')
|
||||
self.tag = data.attrib.get('tag')
|
||||
self.tagKey = data.attrib.get('tagKey')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
parent = self._parent()
|
||||
|
@ -879,12 +881,15 @@ class Writer(MediaTag):
|
|||
FILTER = 'writer'
|
||||
|
||||
|
||||
class GuidTag(PlexObject):
|
||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||
@utils.registerPlexObject
|
||||
class Guid(PlexObject):
|
||||
""" Represents a single Guid media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Guid'
|
||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB, MBID).
|
||||
"""
|
||||
TAG = 'Guid'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
|
@ -893,13 +898,25 @@ class GuidTag(PlexObject):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Guid(GuidTag):
|
||||
""" Represents a single Guid media tag.
|
||||
class Rating(PlexObject):
|
||||
""" Represents a single Rating media tag.
|
||||
|
||||
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
|
||||
|
@ -908,7 +925,7 @@ class Review(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'Review'
|
||||
filter (str): filter for reviews?
|
||||
filter (str): The library filter for the review.
|
||||
id (int): The ID of the review.
|
||||
image (str): The image uri for the review.
|
||||
link (str): The url to the online review.
|
||||
|
@ -983,18 +1000,34 @@ class Chapter(PlexObject):
|
|||
|
||||
Attributes:
|
||||
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'
|
||||
|
||||
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):
|
||||
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.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.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.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -1003,6 +1036,10 @@ class Marker(PlexObject):
|
|||
|
||||
Attributes:
|
||||
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'
|
||||
|
||||
|
@ -1015,10 +1052,10 @@ class Marker(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.type = data.attrib.get('type')
|
||||
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -1027,13 +1064,15 @@ class Field(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'Field'
|
||||
locked (bool): True if the field is locked.
|
||||
name (str): The name of the field.
|
||||
"""
|
||||
TAG = 'Field'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
self.locked = utils.cast(bool, data.attrib.get('locked'))
|
||||
self.name = data.attrib.get('name')
|
||||
|
||||
|
||||
@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.exceptions import BadRequest, NotFound
|
||||
from plexapi.utils import deprecated
|
||||
from plexapi.utils import deprecated, openOrRead
|
||||
|
||||
|
||||
class AdvancedSettingsMixin:
|
||||
|
@ -341,14 +341,14 @@ class ArtMixin(ArtUrlMixin):
|
|||
|
||||
Parameters:
|
||||
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:
|
||||
key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}'
|
||||
self._server.query(key, method=self._server._session.post)
|
||||
elif filepath:
|
||||
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)
|
||||
return self
|
||||
|
||||
|
@ -392,14 +392,14 @@ class BannerMixin(BannerUrlMixin):
|
|||
|
||||
Parameters:
|
||||
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:
|
||||
key = f'/library/metadata/{self.ratingKey}/banners?url={quote_plus(url)}'
|
||||
self._server.query(key, method=self._server._session.post)
|
||||
elif filepath:
|
||||
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)
|
||||
return self
|
||||
|
||||
|
@ -448,14 +448,14 @@ class PosterMixin(PosterUrlMixin):
|
|||
|
||||
Parameters:
|
||||
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:
|
||||
key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}'
|
||||
self._server.query(key, method=self._server._session.post)
|
||||
elif filepath:
|
||||
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)
|
||||
return self
|
||||
|
||||
|
@ -494,22 +494,24 @@ class ThemeMixin(ThemeUrlMixin):
|
|||
""" Returns list of available :class:`~plexapi.media.Theme` objects. """
|
||||
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.
|
||||
|
||||
Warning: Themes cannot be deleted using PlexAPI!
|
||||
|
||||
Parameters:
|
||||
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:
|
||||
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:
|
||||
key = f'/library/metadata/{self.ratingKey}/themes'
|
||||
data = open(filepath, 'rb').read()
|
||||
self._server.query(key, method=self._server._session.post, data=data)
|
||||
data = openOrRead(filepath)
|
||||
self._server.query(key, method=self._server._session.post, data=data, timeout=timeout)
|
||||
return self
|
||||
|
||||
def setTheme(self, theme):
|
||||
|
|
|
@ -32,6 +32,7 @@ class MyPlexAccount(PlexObject):
|
|||
session (requests.Session, optional): Use your own session object if you want to
|
||||
cache the http responses from PMS
|
||||
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:
|
||||
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}
|
||||
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._session = session or requests.Session()
|
||||
self._sonos_cache = []
|
||||
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)
|
||||
|
||||
def _signin(self, username, password, timeout):
|
||||
def _signin(self, username, password, code, timeout):
|
||||
if self._token:
|
||||
return self.query(self.key), self.key
|
||||
username = username or CONFIG.get('auth.myplex_username')
|
||||
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)
|
||||
return data, self.SIGNIN
|
||||
|
||||
|
@ -390,12 +393,13 @@ class MyPlexAccount(PlexObject):
|
|||
url = self.HOMEUSER.format(userId=user.id)
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user (:class:`~plexapi.myplex.MyPlexUser` or str): :class:`~plexapi.myplex.MyPlexUser`,
|
||||
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:
|
||||
|
||||
|
@ -410,9 +414,12 @@ class MyPlexAccount(PlexObject):
|
|||
"""
|
||||
user = user if isinstance(user, MyPlexUser) else self.user(user)
|
||||
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')
|
||||
return MyPlexAccount(token=userToken)
|
||||
return MyPlexAccount(token=userToken, session=self._session)
|
||||
|
||||
def setPin(self, newPin, currentPin=None):
|
||||
""" Set a new Plex Home PIN for the account.
|
||||
|
@ -861,7 +868,12 @@ class MyPlexAccount(PlexObject):
|
|||
results += subresults[:maxresults - len(results)]
|
||||
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):
|
||||
""" Returns True if the item is on the user's watchlist.
|
||||
|
@ -941,7 +953,7 @@ class MyPlexAccount(PlexObject):
|
|||
}
|
||||
params = {
|
||||
'query': query,
|
||||
'limit ': limit,
|
||||
'limit': limit,
|
||||
'searchTypes': libtype,
|
||||
'includeMetadata': 1
|
||||
}
|
||||
|
@ -1005,21 +1017,24 @@ class MyPlexAccount(PlexObject):
|
|||
data = {'code': pin}
|
||||
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. """
|
||||
# TODO: Add proper support for metadata.provider.plex.tv
|
||||
# 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):
|
||||
objs = [objs]
|
||||
|
||||
for obj in objs:
|
||||
obj._server = server
|
||||
|
||||
# Parse details key to modify query string
|
||||
url = urlsplit(obj._details_key)
|
||||
query = dict(parse_qsl(url.query))
|
||||
query['includeUserState'] = 1
|
||||
query['includeUserState'] = includeUserState
|
||||
query.pop('includeFields', None)
|
||||
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.base import Playable, PlexPartialObject
|
||||
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.playqueue import PlayQueue
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
||||
|
@ -330,10 +329,6 @@ class Playlist(
|
|||
""" Delete the playlist. """
|
||||
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
|
||||
def _create(cls, server, title, items):
|
||||
""" Create a regular playlist. """
|
||||
|
@ -375,15 +370,32 @@ class Playlist(
|
|||
data = server.query(key, method=server._session.post)[0]
|
||||
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
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on.
|
||||
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.
|
||||
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.
|
||||
|
@ -396,17 +408,23 @@ class Playlist(
|
|||
See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
||||
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
|
||||
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||
|
||||
Raises:
|
||||
: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 attempting to import m3u file into non-music library.
|
||||
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
|
||||
|
||||
Returns:
|
||||
: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)
|
||||
else:
|
||||
return cls._create(server, title, items)
|
||||
|
|
|
@ -150,8 +150,8 @@ class PlayQueue(PlexObject):
|
|||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
|
||||
items (:class:`~plexapi.base.Playable` or :class:`~plexapi.playlist.Playlist`):
|
||||
A media item, list of media items, or Playlist.
|
||||
items (:class:`~plexapi.base.PlexPartialObject`):
|
||||
A media item or a list of media items.
|
||||
startItem (:class:`~plexapi.base.Playable`, optional):
|
||||
Media item in the PlayQueue where playback should begin.
|
||||
shuffle (int, optional): Start the playqueue shuffled.
|
||||
|
@ -174,16 +174,13 @@ class PlayQueue(PlexObject):
|
|||
uri_args = quote_plus(f"/library/metadata/{item_keys}")
|
||||
args["uri"] = f"library:///directory/{uri_args}"
|
||||
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:
|
||||
if items.type == "playlist":
|
||||
args["type"] = items.playlistType
|
||||
args["playlistID"] = items.ratingKey
|
||||
else:
|
||||
uuid = items.section().uuid
|
||||
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:
|
||||
args["key"] = startItem.key
|
||||
|
|
|
@ -17,7 +17,7 @@ from plexapi.media import Conversion, Optimized
|
|||
from plexapi.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
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
|
||||
|
||||
# Need these imports to populate utils.PLEXOBJECTS
|
||||
|
@ -109,8 +109,6 @@ class PlexServer(PlexObject):
|
|||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||
self._session = session or requests.Session()
|
||||
self._timeout = timeout
|
||||
self._library = None # cached library
|
||||
self._settings = None # cached settings
|
||||
self._myPlexAccount = None # cached myPlexAccount
|
||||
self._systemAccounts = None # cached list of SystemAccount
|
||||
self._systemDevices = None # cached list of SystemDevice
|
||||
|
@ -173,27 +171,22 @@ class PlexServer(PlexObject):
|
|||
def _uriRoot(self):
|
||||
return f'server://{self.machineIdentifier}/com.plexapp.plugins.library'
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def library(self):
|
||||
""" Library to browse or search your media. """
|
||||
if not self._library:
|
||||
try:
|
||||
data = self.query(Library.key)
|
||||
self._library = Library(self, data)
|
||||
except BadRequest:
|
||||
data = self.query('/library/sections/')
|
||||
# Only the owner has access to /library
|
||||
# so just return the library without the data.
|
||||
data = self.query('/library/sections/')
|
||||
return Library(self, data)
|
||||
return self._library
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def settings(self):
|
||||
""" Returns a list of all server settings. """
|
||||
if not self._settings:
|
||||
data = self.query(Settings.key)
|
||||
self._settings = Settings(self, data)
|
||||
return self._settings
|
||||
return Settings(self, data)
|
||||
|
||||
def account(self):
|
||||
""" Returns the :class:`~plexapi.server.Account` object this server belongs to. """
|
||||
|
@ -318,7 +311,7 @@ class PlexServer(PlexObject):
|
|||
"""
|
||||
if self._myPlexAccount is None:
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
self._myPlexAccount = MyPlexAccount(token=self._token)
|
||||
self._myPlexAccount = MyPlexAccount(token=self._token, session=self._session)
|
||||
return self._myPlexAccount
|
||||
|
||||
def _myPlexClientPorts(self):
|
||||
|
@ -454,19 +447,42 @@ class PlexServer(PlexObject):
|
|||
|
||||
Returns:
|
||||
: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(
|
||||
self, title, section, items=items, smart=smart, limit=limit,
|
||||
libtype=libtype, sort=sort, filters=filters, **kwargs)
|
||||
|
||||
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`.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the playlist.
|
||||
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only,
|
||||
library section to create the playlist in.
|
||||
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
|
||||
the library section to create the playlist in.
|
||||
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.
|
||||
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.
|
||||
filters (dict): Smart playlists only, a dictionary of advanced filters.
|
||||
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
|
||||
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
|
||||
|
||||
Raises:
|
||||
: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 attempting to import m3u file into non-music library.
|
||||
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
|
||||
|
||||
Returns:
|
||||
: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(
|
||||
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):
|
||||
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.
|
||||
|
|
|
@ -139,7 +139,14 @@ class Setting(PlexObject):
|
|||
if not enumstr:
|
||||
return None
|
||||
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('|')
|
||||
|
||||
def set(self, value):
|
||||
|
|
|
@ -24,6 +24,11 @@ try:
|
|||
except ImportError:
|
||||
tqdm = None
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
from backports.cached_property import cached_property # noqa: F401
|
||||
|
||||
log = logging.getLogger('plexapi')
|
||||
|
||||
# Search Types - Plex uses these to filter specific media types when searching.
|
||||
|
@ -618,3 +623,10 @@ def toJson(obj, **kwargs):
|
|||
return obj.isoformat()
|
||||
return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
|
||||
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.
|
||||
rating (float): Movie critic rating (7.9; 9.8; 8.1).
|
||||
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.
|
||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||
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.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratingImage = data.attrib.get('ratingImage')
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.studio = data.attrib.get('studio')
|
||||
|
@ -459,6 +461,7 @@ class Show(
|
|||
originallyAvailableAt (datetime): Datetime the show was released.
|
||||
originalTitle (str): The original title of the show.
|
||||
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.
|
||||
showOrdering (str): Setting that indicates the episode ordering for the show
|
||||
(None = Library default).
|
||||
|
@ -503,6 +506,7 @@ class Show(
|
|||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.originalTitle = data.attrib.get('originalTitle')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.showOrdering = data.attrib.get('showOrdering')
|
||||
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>).
|
||||
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||
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.
|
||||
year (int): Year the season was released.
|
||||
"""
|
||||
|
@ -663,6 +668,7 @@ class Season(
|
|||
self.parentTheme = data.attrib.get('parentTheme')
|
||||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
|
@ -800,6 +806,7 @@ class Episode(
|
|||
parentYear (int): Year the season was released.
|
||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||
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.
|
||||
skipParent (bool): True if the show's seasons are set to hidden.
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
|
@ -845,6 +852,7 @@ class Episode(
|
|||
self.parentYear = utils.cast(int, data.attrib.get('parentYear'))
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
|
|
|
@ -27,7 +27,7 @@ MarkupSafe==2.1.1
|
|||
musicbrainzngs==0.7.1
|
||||
packaging==22.0
|
||||
paho-mqtt==1.6.1
|
||||
plexapi==4.13.1
|
||||
plexapi==4.13.2
|
||||
portend==3.1.0
|
||||
profilehooks==1.12.0
|
||||
PyJWT==2.6.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue