Bump plexapi from 4.15.15 to 4.15.16 (#2383)

* Bump plexapi from 4.15.15 to 4.15.16

Bumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.15.15 to 4.15.16.
- [Release notes](https://github.com/pkkid/python-plexapi/releases)
- [Commits](https://github.com/pkkid/python-plexapi/compare/4.15.15...4.15.16)

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

---------

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] 2024-09-20 20:24:00 -07:00 committed by GitHub
parent de697cb2ca
commit cc1a325eac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 153 additions and 45 deletions

View file

@ -193,6 +193,7 @@ class Artist(
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
"""
TAG = 'Directory'
TYPE = 'artist'
@ -213,6 +214,7 @@ class Artist(
self.similar = self.findItems(data, media.Similar)
self.styles = self.findItems(data, media.Style)
self.theme = data.attrib.get('theme')
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
def __iter__(self):
for album in self.albums():
@ -281,6 +283,21 @@ class Artist(
filepaths += track.download(_savepath, keep_original_name, **kwargs)
return filepaths
def popularTracks(self):
""" Returns a list of :class:`~plexapi.audio.Track` popular tracks by the artist. """
filters = {
'album.subformat!': 'Compilation,Live',
'artist.id': self.ratingKey,
'group': 'title',
'ratingCount>>': 0,
}
return self.section().search(
libtype='track',
filters=filters,
sort='ratingCount:desc',
limit=100
)
def station(self):
""" Returns a :class:`~plexapi.playlist.Playlist` artist radio station or `None`. """
key = f'{self.key}?includeStations=1'
@ -325,6 +342,7 @@ class Album(
studio (str): Studio that released the album.
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
subformats (List<:class:`~plexapi.media.Subformat`>): List of subformat objects.
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
viewedLeafCount (int): Number of items marked as played in the album view.
year (int): Year the album was released.
"""
@ -354,6 +372,7 @@ class Album(
self.studio = data.attrib.get('studio')
self.styles = self.findItems(data, media.Style)
self.subformats = self.findItems(data, media.Subformat)
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
self.year = utils.cast(int, data.attrib.get('year'))

View file

@ -3,7 +3,7 @@ import re
from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, TypeVar, Union
import weakref
from functools import cached_property
from urllib.parse import urlencode
from urllib.parse import parse_qsl, urlencode, urlparse
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
@ -391,10 +391,9 @@ class PlexObject:
Parameters:
key (string, optional): Override the key to reload.
**kwargs (dict): A dictionary of XML include parameters to exclude or override.
All parameters are included by default with the option to override each parameter
or disable each parameter individually by setting it to False or 0.
**kwargs (dict): A dictionary of XML include parameters to include/exclude or override.
See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters.
Set parameter to True to include and False to exclude.
Example:
@ -402,20 +401,28 @@ class PlexObject:
from plexapi.server import PlexServer
plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx')
movie = plex.library.section('Movies').get('Cars')
# Partial reload of the movie without the `checkFiles` parameter.
# Excluding `checkFiles` will prevent the Plex server from reading the
# file to check if the file still exists and is accessible.
# The movie object will remain as a partial object.
movie.reload(checkFiles=False)
# Search results are partial objects.
movie = plex.library.section('Movies').get('Cars')
movie.isPartialObject() # Returns True
# Full reload of the movie with all include parameters.
# Partial reload of the movie without a default include parameter.
# The movie object will remain as a partial object.
movie.reload(includeMarkers=False)
movie.isPartialObject() # Returns True
# Full reload of the movie with all default include parameters.
# The movie object will be a full object.
movie.reload()
movie.isFullObject() # Returns True
# Full reload of the movie with all default and extra include parameter.
# Including `checkFiles` will tell the Plex server to check if the file
# still exists and is accessible.
# The movie object will be a full object.
movie.reload(checkFiles=True)
movie.isFullObject() # Returns True
"""
return self._reload(key=key, **kwargs)
@ -505,25 +512,25 @@ class PlexPartialObject(PlexObject):
automatically and update itself.
"""
_INCLUDES = {
'checkFiles': 1,
'includeAllConcerts': 1,
'checkFiles': 0,
'includeAllConcerts': 0,
'includeBandwidths': 1,
'includeChapters': 1,
'includeChildren': 1,
'includeConcerts': 1,
'includeExternalMedia': 1,
'includeExtras': 1,
'includeChildren': 0,
'includeConcerts': 0,
'includeExternalMedia': 0,
'includeExtras': 0,
'includeFields': 'thumbBlurHash,artBlurHash',
'includeGeolocation': 1,
'includeLoudnessRamps': 1,
'includeMarkers': 1,
'includeOnDeck': 1,
'includePopularLeaves': 1,
'includePreferences': 1,
'includeRelated': 1,
'includeRelatedCount': 1,
'includeReviews': 1,
'includeStations': 1,
'includeOnDeck': 0,
'includePopularLeaves': 0,
'includePreferences': 0,
'includeRelated': 0,
'includeRelatedCount': 0,
'includeReviews': 0,
'includeStations': 0,
}
_EXCLUDES = {
'excludeElements': (
@ -592,7 +599,11 @@ class PlexPartialObject(PlexObject):
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie would contain.
"""
return not self.key or (self._details_key or self.key) == self._initpath
parsed_key = urlparse(self._details_key or self.key)
parsed_initpath = urlparse(self._initpath)
query_key = set(parse_qsl(parsed_key.query))
query_init = set(parse_qsl(parsed_initpath.query))
return not self.key or (parsed_key.path == parsed_initpath.path and query_key <= query_init)
def isPartialObject(self):
""" Returns True if this is not a full object. """

View file

@ -197,7 +197,7 @@ class PlexClient(PlexObject):
raise NotFound(message)
else:
raise BadRequest(message)
data = response.text.encode('utf8')
data = utils.cleanXMLString(response.text).encode('utf8')
return ElementTree.fromstring(data) if data.strip() else None
def sendCommand(self, command, proxy=None, **params):

View file

@ -60,6 +60,7 @@ class Collection(
title (str): Name of the collection.
titleSort (str): Title to use when sorting (defaults to title).
type (str): 'collection'
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
updatedAt (datetime): Datetime the collection was updated.
userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars).
"""
@ -102,6 +103,7 @@ class Collection(
self.title = data.attrib.get('title')
self.titleSort = data.attrib.get('titleSort', self.title)
self.type = data.attrib.get('type')
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.userRating = utils.cast(float, data.attrib.get('userRating'))
self._items = None # cache for self.items

View file

@ -4,6 +4,6 @@
# Library version
MAJOR_VERSION = 4
MINOR_VERSION = 15
PATCH_VERSION = 15
PATCH_VERSION = 16
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"

View file

@ -2823,7 +2823,8 @@ class FilteringType(PlexObject):
additionalFields.extend([
('duration', 'integer', 'Duration'),
('viewOffset', 'integer', 'View Offset'),
('label', 'tag', 'Label')
('label', 'tag', 'Label'),
('ratingCount', 'integer', 'Rating Count'),
])
elif self.type == 'collection':
additionalFields.extend([

View file

@ -106,12 +106,16 @@ class MediaPart(PlexObject):
Attributes:
TAG (str): 'Part'
accessible (bool): True if the file is accessible.
Requires reloading the media with ``checkFiles=True``.
Refer to :func:`~plexapi.base.PlexObject.reload`.
audioProfile (str): The audio profile of the file.
container (str): The container type of the file (ex: avi).
decision (str): Unknown.
deepAnalysisVersion (int): The Plex deep analysis version for the file.
duration (int): The duration of the file in milliseconds.
exists (bool): True if the file exists.
Requires reloading the media with ``checkFiles=True``.
Refer to :func:`~plexapi.base.PlexObject.reload`.
file (str): The path to this file on disk (ex: /media/Movies/Cars (2006)/Cars (2006).mkv)
has64bitOffsets (bool): True if the file has 64 bit offsets.
hasThumbnail (bool): True if the file (track) has an embedded thumbnail.
@ -999,6 +1003,28 @@ class Review(PlexObject):
self.text = data.attrib.get('text')
@utils.registerPlexObject
class UltraBlurColors(PlexObject):
""" Represents a single UltraBlurColors media tag.
Attributes:
TAG (str): 'UltraBlurColors'
bottomLeft (str): The bottom left hex color.
bottomRight (str): The bottom right hex color.
topLeft (str): The top left hex color.
topRight (str): The top right hex color.
"""
TAG = 'UltraBlurColors'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.bottomLeft = data.attrib.get('bottomLeft')
self.bottomRight = data.attrib.get('bottomRight')
self.topLeft = data.attrib.get('topLeft')
self.topRight = data.attrib.get('topRight')
class BaseResource(PlexObject):
""" Base class for all Art, Poster, and Theme objects.

View file

@ -14,8 +14,8 @@ class AdvancedSettingsMixin:
def preferences(self):
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, settings.Preferences, rtag='Preferences')
key = f'{self.key}?includePreferences=1'
return self.fetchItems(key, cls=settings.Preferences, rtag='Preferences')
def preference(self, pref):
""" Returns a :class:`~plexapi.settings.Preferences` object for the specified pref.
@ -240,8 +240,7 @@ class UnmatchMatchMixin:
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
key = key + '?' + urlencode(params)
data = self._server.query(key, method=self._server._session.get)
return self.findItems(data, initpath=key)
return self.fetchItems(key, cls=media.SearchResult)
def fixMatch(self, searchResult=None, auto=False, agent=None):
""" Use match result to update show metadata.
@ -278,8 +277,8 @@ class ExtrasMixin:
def extras(self):
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
from plexapi.video import Extra
data = self._server.query(self._details_key)
return self.findItems(data, Extra, rtag='Extras')
key = f'{self.key}/extras'
return self.fetchItems(key, cls=Extra)
class HubsMixin:
@ -289,8 +288,7 @@ class HubsMixin:
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
from plexapi.library import Hub
key = f'{self.key}/related'
data = self._server.query(key)
return self.findItems(data, Hub)
return self.fetchItems(key, cls=Hub)
class PlayedUnplayedMixin:

View file

@ -250,7 +250,7 @@ class MyPlexAccount(PlexObject):
return response.json()
elif 'text/plain' in response.headers.get('Content-Type', ''):
return response.text.strip()
data = response.text.encode('utf8')
data = utils.cleanXMLString(response.text).encode('utf8')
return ElementTree.fromstring(data) if data.strip() else None
def ping(self):

View file

@ -768,7 +768,7 @@ class PlexServer(PlexObject):
raise NotFound(message)
else:
raise BadRequest(message)
data = response.text.encode('utf8')
data = utils.cleanXMLString(response.text).encode('utf8')
return ElementTree.fromstring(data) if data.strip() else None
def search(self, query, mediatype=None, limit=None, sectionId=None):

View file

@ -6,6 +6,7 @@ import logging
import os
import re
import string
import sys
import time
import unicodedata
import warnings
@ -673,3 +674,45 @@ def openOrRead(file):
def sha1hash(guid):
""" Return the SHA1 hash of a guid. """
return sha1(guid.encode('utf-8')).hexdigest()
# https://stackoverflow.com/a/64570125
_illegal_XML_characters = [
(0x00, 0x08),
(0x0B, 0x0C),
(0x0E, 0x1F),
(0x7F, 0x84),
(0x86, 0x9F),
(0xFDD0, 0xFDDF),
(0xFFFE, 0xFFFF),
]
if sys.maxunicode >= 0x10000: # not narrow build
_illegal_XML_characters.extend(
[
(0x1FFFE, 0x1FFFF),
(0x2FFFE, 0x2FFFF),
(0x3FFFE, 0x3FFFF),
(0x4FFFE, 0x4FFFF),
(0x5FFFE, 0x5FFFF),
(0x6FFFE, 0x6FFFF),
(0x7FFFE, 0x7FFFF),
(0x8FFFE, 0x8FFFF),
(0x9FFFE, 0x9FFFF),
(0xAFFFE, 0xAFFFF),
(0xBFFFE, 0xBFFFF),
(0xCFFFE, 0xCFFFF),
(0xDFFFE, 0xDFFFF),
(0xEFFFE, 0xEFFFF),
(0xFFFFE, 0xFFFFF),
(0x10FFFE, 0x10FFFF),
]
)
_illegal_XML_ranges = [
fr'{chr(low)}-{chr(high)}'
for (low, high) in _illegal_XML_characters
]
_illegal_XML_re = re.compile(fr'[{"".join(_illegal_XML_ranges)}]')
def cleanXMLString(s):
return _illegal_XML_re.sub('', s)

View file

@ -375,6 +375,7 @@ class Movie(
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
useOriginalTitle (int): Setting that indicates if the original title is used for the movie
(-1 = Library default, 0 = No, 1 = Yes).
viewOffset (int): View offset in milliseconds.
@ -420,6 +421,7 @@ class Movie(
self.studio = data.attrib.get('studio')
self.tagline = data.attrib.get('tagline')
self.theme = data.attrib.get('theme')
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.writers = self.findItems(data, media.Writer)
@ -456,8 +458,8 @@ class Movie(
def reviews(self):
""" Returns a list of :class:`~plexapi.media.Review` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, media.Review, rtag='Video')
key = f'{self.key}?includeReviews=1'
return self.fetchItems(key, cls=media.Review, rtag='Video')
def editions(self):
""" Returns a list of :class:`~plexapi.video.Movie` objects
@ -543,6 +545,7 @@ class Show(
(-1 = Account default, 0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled).
tagline (str): Show tag line.
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
useOriginalTitle (int): Setting that indicates if the original title is used for the show
(-1 = Library default, 0 = No, 1 = Yes).
viewedLeafCount (int): Number of items marked as played in the show view.
@ -592,6 +595,7 @@ class Show(
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
self.tagline = data.attrib.get('tagline')
self.theme = data.attrib.get('theme')
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
self.year = utils.cast(int, data.attrib.get('year'))
@ -614,8 +618,8 @@ class Show(
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
If show is unwatched, return will likely be the first episode.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)
key = f'{self.key}?includeOnDeck=1'
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)
def season(self, title=None, season=None):
""" Returns the season with the specified title or number.
@ -735,6 +739,7 @@ class Season(
subtitleLanguage (str): Setting that indicates the preferred subtitle language.
subtitleMode (int): Setting that indicates the auto-select subtitle mode.
(-1 = Series default, 0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled).
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
viewedLeafCount (int): Number of items marked as played in the season view.
year (int): Year the season was released.
"""
@ -766,6 +771,7 @@ class Season(
self.ratings = self.findItems(data, media.Rating)
self.subtitleLanguage = data.attrib.get('subtitleLanguage', '')
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
self.year = utils.cast(int, data.attrib.get('year'))
@ -796,8 +802,8 @@ class Season(
""" Returns season's On Deck :class:`~plexapi.video.Video` object or `None`.
Will only return a match if the show's On Deck episode is in this season.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)
key = f'{self.key}?includeOnDeck=1'
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)
def episode(self, title=None, episode=None):
""" Returns the episode with the given title or number.
@ -914,6 +920,7 @@ class Episode(
skipParent (bool): True if the show's seasons are set to hidden.
sourceURI (str): Remote server URI (server://<machineIdentifier>/com.plexapp.plugins.library)
(remote playlist item only).
ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object.
viewOffset (int): View offset in milliseconds.
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
year (int): Year the episode was released.
@ -958,6 +965,7 @@ class Episode(
self.roles = self.findItems(data, media.Role)
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
self.sourceURI = data.attrib.get('source') # remote playlist item
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.writers = self.findItems(data, media.Writer)
self.year = utils.cast(int, data.attrib.get('year'))

View file

@ -26,7 +26,7 @@ musicbrainzngs==0.7.1
packaging==24.1
paho-mqtt==2.1.0
platformdirs==4.2.2
plexapi==4.15.15
plexapi==4.15.16
portend==3.2.0
profilehooks==1.12.0
PyJWT==2.9.0