Bump plexapi from 4.15.16 to 4.16.0 (#2439)

* Bump plexapi from 4.15.16 to 4.16.0

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

---
updated-dependencies:
- dependency-name: plexapi
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update plexapi==4.16.0

---------

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-11-19 10:00:37 -08:00 committed by GitHub
parent eb2c372d82
commit 0836fb902c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 287 additions and 49 deletions

View file

@ -33,6 +33,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
distance (float): Sonic Distance of the item from the seed item.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (int): Plex index number (often the track number).
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated.
@ -65,6 +66,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
self.distance = utils.cast(float, data.attrib.get('distance'))
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

View file

@ -17,7 +17,7 @@ PlexObjectT = TypeVar("PlexObjectT", bound='PlexObject')
MediaContainerT = TypeVar("MediaContainerT", bound="MediaContainer")
USER_DONT_RELOAD_FOR_KEYS = set()
_DONT_RELOAD_FOR_KEYS = {'key'}
_DONT_RELOAD_FOR_KEYS = {'key', 'sourceURI'}
OPERATORS = {
'exact': lambda v, q: v == q,
'iexact': lambda v, q: v.lower() == q.lower(),
@ -71,7 +71,7 @@ class PlexObject:
self._details_key = self._buildDetailsKey()
def __repr__(self):
uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri'))
uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri', 'type'))
name = self._clean(self.firstAttr('title', 'name', 'username', 'product', 'tag', 'value'))
return f"<{':'.join([p for p in [self.__class__.__name__, uid, name] if p])}>"

View file

@ -39,6 +39,7 @@ class Collection(
contentRating (str) Content rating (PG-13; NR; TV-G).
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (int): Plex index number for the collection.
key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
@ -82,6 +83,7 @@ class Collection(
self.contentRating = data.attrib.get('contentRating')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label)

View file

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

View file

@ -1740,7 +1740,7 @@ class LibrarySection(PlexObject):
def _edit(self, items=None, **kwargs):
""" Actually edit multiple objects. """
if isinstance(self._edits, dict):
if isinstance(self._edits, dict) and items is None:
self._edits.update(kwargs)
return self

View file

@ -26,6 +26,7 @@ class Media(PlexObject):
height (int): The height of the media in pixels (ex: 256).
id (int): The unique ID for this media on the server.
has64bitOffsets (bool): True if video has 64 bit offsets.
hasVoiceActivity (bool): True if video has voice activity analyzed.
optimizedForStreaming (bool): True if video is optimized for streaming.
parts (List<:class:`~plexapi.media.MediaPart`>): List of media part objects.
proxyType (int): Equals 42 for optimized versions.
@ -61,6 +62,7 @@ class Media(PlexObject):
self.height = utils.cast(int, data.attrib.get('height'))
self.id = utils.cast(int, data.attrib.get('id'))
self.has64bitOffsets = utils.cast(bool, data.attrib.get('has64bitOffsets'))
self.hasVoiceActivity = utils.cast(bool, data.attrib.get('hasVoiceActivity', '0'))
self.optimizedForStreaming = utils.cast(bool, data.attrib.get('optimizedForStreaming'))
self.parts = self.findItems(data, MediaPart)
self.proxyType = utils.cast(int, data.attrib.get('proxyType'))
@ -441,6 +443,7 @@ class SubtitleStream(MediaPartStream):
Attributes:
TAG (str): 'Stream'
STREAMTYPE (int): 3
canAutoSync (bool): True if the subtitle stream can be auto synced.
container (str): The container of the subtitle stream.
forced (bool): True if this is a forced subtitle.
format (str): The format of the subtitle stream (ex: srt).
@ -459,6 +462,7 @@ class SubtitleStream(MediaPartStream):
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
super(SubtitleStream, self)._loadData(data)
self.canAutoSync = utils.cast(bool, data.attrib.get('canAutoSync'))
self.container = data.attrib.get('container')
self.forced = utils.cast(bool, data.attrib.get('forced', '0'))
self.format = data.attrib.get('format')
@ -954,6 +958,26 @@ class Guid(PlexObject):
self.id = data.attrib.get('id')
@utils.registerPlexObject
class Image(PlexObject):
""" Represents a single Image media tag.
Attributes:
TAG (str): 'Image'
alt (str): The alt text for the image.
type (str): The type of image (e.g. coverPoster, background, snapshot).
url (str): The API URL (/library/metadata/<ratingKey>/thumb/<thumbid>).
"""
TAG = 'Image'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.alt = data.attrib.get('alt')
self.type = data.attrib.get('type')
self.url = data.attrib.get('url')
@utils.registerPlexObject
class Rating(PlexObject):
""" Represents a single Rating media tag.
@ -1074,6 +1098,11 @@ class Art(BaseResource):
TAG = 'Photo'
class Logo(BaseResource):
""" Represents a single Logo object. """
TAG = 'Photo'
class Poster(BaseResource):
""" Represents a single Poster object. """
TAG = 'Photo'

View file

@ -403,6 +403,63 @@ class ArtMixin(ArtUrlMixin, ArtLockMixin):
return self
class LogoUrlMixin:
""" Mixin for Plex objects that can have a logo url. """
@property
def logoUrl(self):
""" Return the logo url for the Plex object. """
image = next((i for i in self.images if i.type == 'clearLogo'), None)
return self._server.url(image.url, includeToken=True) if image else None
class LogoLockMixin:
""" Mixin for Plex objects that can have a locked logo. """
def lockLogo(self):
""" Lock the logo for a Plex object. """
raise NotImplementedError('Logo cannot be locked through the API.')
def unlockLogo(self):
""" Unlock the logo for a Plex object. """
raise NotImplementedError('Logo cannot be unlocked through the API.')
class LogoMixin(LogoUrlMixin, LogoLockMixin):
""" Mixin for Plex objects that can have logos. """
def logos(self):
""" Returns list of available :class:`~plexapi.media.Logo` objects. """
return self.fetchItems(f'/library/metadata/{self.ratingKey}/clearLogos', cls=media.Logo)
def uploadLogo(self, url=None, filepath=None):
""" Upload a logo from a url or filepath.
Parameters:
url (str): The full URL to 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}/clearLogos?url={quote_plus(url)}'
self._server.query(key, method=self._server._session.post)
elif filepath:
key = f'/library/metadata/{self.ratingKey}/clearLogos'
data = openOrRead(filepath)
self._server.query(key, method=self._server._session.post, data=data)
return self
def setLogo(self, logo):
""" Set the logo for a Plex object.
Raises:
:exc:`~plexapi.exceptions.NotImplementedError`: Logo cannot be set through the API.
"""
raise NotImplementedError(
'Logo cannot be set through the API. '
'Re-upload the logo using "uploadLogo" to set it.'
)
class PosterUrlMixin:
""" Mixin for Plex objects that can have a poster url. """
@ -513,6 +570,11 @@ class ThemeMixin(ThemeUrlMixin, ThemeLockMixin):
return self
def setTheme(self, theme):
""" Set the theme for a Plex object.
Raises:
:exc:`~plexapi.exceptions.NotImplementedError`: Themes cannot be set through the API.
"""
raise NotImplementedError(
'Themes cannot be set through the API. '
'Re-upload the theme using "uploadTheme" to set it.'

View file

@ -283,10 +283,10 @@ class MyPlexAccount(PlexObject):
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.
Parameters:
name (str): Name to match against.
name (str): Name or machine identifier to match against.
"""
for resource in self.resources():
if resource.name.lower() == name.lower():
if resource.name.lower() == name.lower() or resource.clientIdentifier == name:
return resource
raise NotFound(f'Unable to find resource {name}')

View file

@ -30,6 +30,7 @@ class Photoalbum(
composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo album (local://229674).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (sting): Plex index number for the photo album.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo album was last rated.
@ -57,6 +58,7 @@ class Photoalbum(
self.composite = data.attrib.get('composite')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
@ -164,6 +166,7 @@ class Photo(
createdAtTZOffset (int): Unknown (-25200).
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (sting): Plex index number for the photo.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo was last rated.
@ -204,6 +207,7 @@ class Photo(
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

View file

@ -190,6 +190,20 @@ class Playlist(
if self._items is None:
key = f'{self.key}/items'
items = self.fetchItems(key)
# Cache server connections to avoid reconnecting for each item
_servers = {}
for item in items:
if item.sourceURI:
serverID = item.sourceURI.split('/')[2]
if serverID not in _servers:
try:
_servers[serverID] = self._server.myPlexAccount().resource(serverID).connect()
except NotFound:
# Override the server connection with None if the server is not found
_servers[serverID] = None
item._server = _servers[serverID]
self._items = items
return self._items

View file

@ -90,6 +90,8 @@ TAGTYPES = {
'theme': 317,
'studio': 318,
'network': 319,
'showOrdering': 322,
'clearLogo': 323,
'place': 400,
}
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}

View file

@ -9,7 +9,7 @@ from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
WatchlistMixin
)
@ -26,6 +26,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
artBlurHash (str): BlurHash string for artwork image.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated.
lastViewedAt (datetime): Datetime the item was last played.
@ -53,6 +54,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
self.artBlurHash = data.attrib.get('artBlurHash')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
@ -332,7 +334,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
class Movie(
Video, Playable,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
MovieEditMixins,
WatchlistMixin
):
@ -447,6 +449,11 @@ class Movie(
""" Returns True if the movie has a credits marker. """
return any(marker.type == 'credits' for marker in self.markers)
@property
def hasVoiceActivity(self):
""" Returns True if any of the media has voice activity analyzed. """
return any(media.hasVoiceActivity for media in self.media)
@property
def hasPreviewThumbnails(self):
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """
@ -489,7 +496,7 @@ class Movie(
class Show(
Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
ShowEditMixins,
WatchlistMixin
):
@ -1077,6 +1084,11 @@ class Episode(
""" Returns True if the episode has a credits marker. """
return any(marker.type == 'credits' for marker in self.markers)
@property
def hasVoiceActivity(self):
""" Returns True if any of the media has voice activity analyzed. """
return any(media.hasVoiceActivity for media in self.media)
@property
def hasPreviewThumbnails(self):
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """