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:
dependabot[bot] 2022-12-22 10:44:51 -08:00 committed by GitHub
parent 8cd5b0b775
commit 31f6b02149
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 250 additions and 107 deletions

View file

@ -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.

View file

@ -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. """

View file

@ -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}"

View file

@ -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.

View file

@ -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

View file

@ -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):

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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`.

View file

@ -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):

View file

@ -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()

View file

@ -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))

View file

@ -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