Update plexapi==4.13.4

This commit is contained in:
JonnyWong16 2023-03-10 11:06:27 -08:00
parent b7da2dedf3
commit eb7495e930
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
18 changed files with 187 additions and 75 deletions

View file

@ -175,7 +175,6 @@ def from_bytes(
prioritized_encodings.append("utf_8") prioritized_encodings.append("utf_8")
for encoding_iana in prioritized_encodings + IANA_SUPPORTED: for encoding_iana in prioritized_encodings + IANA_SUPPORTED:
if cp_isolation and encoding_iana not in cp_isolation: if cp_isolation and encoding_iana not in cp_isolation:
continue continue
@ -318,7 +317,9 @@ def from_bytes(
bom_or_sig_available and strip_sig_or_bom is False bom_or_sig_available and strip_sig_or_bom is False
): ):
break break
except UnicodeDecodeError as e: # Lazy str loading may have missed something there except (
UnicodeDecodeError
) as e: # Lazy str loading may have missed something there
logger.log( logger.log(
TRACE, TRACE,
"LazyStr Loading: After MD chunk decode, code page %s does not fit given bytes sequence at ALL. %s", "LazyStr Loading: After MD chunk decode, code page %s does not fit given bytes sequence at ALL. %s",

View file

@ -140,7 +140,6 @@ def alphabet_languages(
source_have_accents = any(is_accentuated(character) for character in characters) source_have_accents = any(is_accentuated(character) for character in characters)
for language, language_characters in FREQUENCIES.items(): for language, language_characters in FREQUENCIES.items():
target_have_accents, target_pure_latin = get_target_features(language) target_have_accents, target_pure_latin = get_target_features(language)
if ignore_non_latin and target_pure_latin is False: if ignore_non_latin and target_pure_latin is False:

View file

@ -147,7 +147,6 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:
x_ = [] x_ = []
for my_file in args.files: for my_file in args.files:
matches = from_fp(my_file, threshold=args.threshold, explain=args.verbose) matches = from_fp(my_file, threshold=args.threshold, explain=args.verbose)
best_guess = matches.best() best_guess = matches.best()
@ -222,7 +221,6 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:
) )
if args.normalize is True: if args.normalize is True:
if best_guess.encoding.startswith("utf") is True: if best_guess.encoding.startswith("utf") is True:
print( print(
'"{}" file does not need to be normalized, as it already came from unicode.'.format( '"{}" file does not need to be normalized, as it already came from unicode.'.format(

View file

@ -1,10 +1,13 @@
from typing import Dict, Optional, Union from typing import Any, Dict, Optional, Union
from warnings import warn
from .api import from_bytes from .api import from_bytes
from .constant import CHARDET_CORRESPONDENCE from .constant import CHARDET_CORRESPONDENCE
def detect(byte_str: bytes) -> Dict[str, Optional[Union[str, float]]]: def detect(
byte_str: bytes, should_rename_legacy: bool = False, **kwargs: Any
) -> Dict[str, Optional[Union[str, float]]]:
""" """
chardet legacy method chardet legacy method
Detect the encoding of the given byte string. It should be mostly backward-compatible. Detect the encoding of the given byte string. It should be mostly backward-compatible.
@ -13,7 +16,14 @@ def detect(byte_str: bytes) -> Dict[str, Optional[Union[str, float]]]:
further information. Not planned for removal. further information. Not planned for removal.
:param byte_str: The byte sequence to examine. :param byte_str: The byte sequence to examine.
:param should_rename_legacy: Should we rename legacy encodings
to their more modern equivalents?
""" """
if len(kwargs):
warn(
f"charset-normalizer disregard arguments '{','.join(list(kwargs.keys()))}' in legacy function detect()"
)
if not isinstance(byte_str, (bytearray, bytes)): if not isinstance(byte_str, (bytearray, bytes)):
raise TypeError( # pragma: nocover raise TypeError( # pragma: nocover
"Expected object of type bytes or bytearray, got: " "Expected object of type bytes or bytearray, got: "
@ -34,10 +44,11 @@ def detect(byte_str: bytes) -> Dict[str, Optional[Union[str, float]]]:
if r is not None and encoding == "utf_8" and r.bom: if r is not None and encoding == "utf_8" and r.bom:
encoding += "_sig" encoding += "_sig"
if should_rename_legacy is False and encoding in CHARDET_CORRESPONDENCE:
encoding = CHARDET_CORRESPONDENCE[encoding]
return { return {
"encoding": encoding "encoding": encoding,
if encoding not in CHARDET_CORRESPONDENCE
else CHARDET_CORRESPONDENCE[encoding],
"language": language, "language": language,
"confidence": confidence, "confidence": confidence,
} }

View file

@ -311,7 +311,6 @@ def range_scan(decoded_sequence: str) -> List[str]:
def cp_similarity(iana_name_a: str, iana_name_b: str) -> float: def cp_similarity(iana_name_a: str, iana_name_b: str) -> float:
if is_multi_byte_encoding(iana_name_a) or is_multi_byte_encoding(iana_name_b): if is_multi_byte_encoding(iana_name_a) or is_multi_byte_encoding(iana_name_b):
return 0.0 return 0.0
@ -351,7 +350,6 @@ def set_logging_handler(
level: int = logging.INFO, level: int = logging.INFO,
format_string: str = "%(asctime)s | %(levelname)s | %(message)s", format_string: str = "%(asctime)s | %(levelname)s | %(message)s",
) -> None: ) -> None:
logger = logging.getLogger(name) logger = logging.getLogger(name)
logger.setLevel(level) logger.setLevel(level)
@ -371,7 +369,6 @@ def cut_sequence_chunks(
is_multi_byte_decoder: bool, is_multi_byte_decoder: bool,
decoded_payload: Optional[str] = None, decoded_payload: Optional[str] = None,
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
if decoded_payload and is_multi_byte_decoder is False: if decoded_payload and is_multi_byte_decoder is False:
for i in offsets: for i in offsets:
chunk = decoded_payload[i : i + chunk_size] chunk = decoded_payload[i : i + chunk_size]
@ -397,7 +394,6 @@ def cut_sequence_chunks(
# multi-byte bad cutting detector and adjustment # multi-byte bad cutting detector and adjustment
# not the cleanest way to perform that fix but clever enough for now. # not the cleanest way to perform that fix but clever enough for now.
if is_multi_byte_decoder and i > 0: if is_multi_byte_decoder and i > 0:
chunk_partial_size_chk: int = min(chunk_size, 16) chunk_partial_size_chk: int = min(chunk_size, 16)
if ( if (

View file

@ -2,5 +2,5 @@
Expose version Expose version
""" """
__version__ = "3.0.1" __version__ = "3.1.0"
VERSION = __version__.split(".") VERSION = __version__.split(".")

View file

@ -23,7 +23,7 @@ X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bo
# Plex Header Configuration # Plex Header Configuration
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller') X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platform', uname()[0])) X_PLEX_PLATFORM = CONFIG.get('header.platform', uname()[0])
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2]) X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT) X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
X_PLEX_VERSION = CONFIG.get('header.version', VERSION) X_PLEX_VERSION = CONFIG.get('header.version', VERSION)

View file

@ -8,14 +8,14 @@ from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin, TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
) )
from plexapi.playlist import Playlist from plexapi.playlist import Playlist
class Audio(PlexPartialObject, PlayedUnplayedMixin): class Audio(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin):
""" Base class for all audio objects including :class:`~plexapi.audio.Artist`, """ Base class for all audio objects including :class:`~plexapi.audio.Artist`,
:class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`. :class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`.

View file

@ -695,38 +695,45 @@ class Playable:
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue
def getStreamURL(self, **params): def getStreamURL(self, **kwargs):
""" Returns a stream url that may be used by external applications such as VLC. """ Returns a stream url that may be used by external applications such as VLC.
Parameters: Parameters:
**params (dict): optional parameters to manipulate the playback when accessing **kwargs (dict): optional parameters to manipulate the playback when accessing
the stream. A few known parameters include: maxVideoBitrate, videoResolution the stream. A few known parameters include: maxVideoBitrate, videoResolution
offset, copyts, protocol, mediaIndex, platform. offset, copyts, protocol, mediaIndex, partIndex, platform.
Raises: Raises:
:exc:`~plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL. :exc:`~plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
""" """
if self.TYPE not in ('movie', 'episode', 'track', 'clip'): if self.TYPE not in ('movie', 'episode', 'track', 'clip'):
raise Unsupported(f'Fetching stream URL for {self.TYPE} is unsupported.') raise Unsupported(f'Fetching stream URL for {self.TYPE} is unsupported.')
mvb = params.get('maxVideoBitrate')
vr = params.get('videoResolution', '') mvb = kwargs.pop('maxVideoBitrate', None)
vr = kwargs.pop('videoResolution', '')
protocol = kwargs.pop('protocol', None)
params = { params = {
'path': self.key, 'path': self.key,
'offset': params.get('offset', 0), 'mediaIndex': kwargs.pop('mediaIndex', 0),
'copyts': params.get('copyts', 1), 'partIndex': kwargs.pop('mediaIndex', 0),
'protocol': params.get('protocol'), 'protocol': protocol,
'mediaIndex': params.get('mediaIndex', 0), 'fastSeek': kwargs.pop('fastSeek', 1),
'X-Plex-Platform': params.get('platform', 'Chrome'), 'copyts': kwargs.pop('copyts', 1),
'offset': kwargs.pop('offset', 0),
'maxVideoBitrate': max(mvb, 64) if mvb else None, 'maxVideoBitrate': max(mvb, 64) if mvb else None,
'videoResolution': vr if re.match(r'^\d+x\d+$', vr) else None 'videoResolution': vr if re.match(r'^\d+x\d+$', vr) else None,
'X-Plex-Platform': kwargs.pop('platform', 'Chrome')
} }
params.update(kwargs)
# remove None values # remove None values
params = {k: v for k, v in params.items() if v is not None} params = {k: v for k, v in params.items() if v is not None}
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video' streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
# sort the keys since the randomness fucks with my tests.. ext = 'mpd' if protocol == 'dash' else 'm3u8'
sorted_params = sorted(params.items(), key=lambda val: val[0])
return self._server.url( return self._server.url(
f'/{streamtype}/:/transcode/universal/start.m3u8?{urlencode(sorted_params)}', f'/{streamtype}/:/transcode/universal/start.{ext}?{urlencode(params)}',
includeToken=True includeToken=True
) )
@ -795,8 +802,8 @@ class Playable:
""" Set the watched progress for this video. """ Set the watched progress for this video.
Note that setting the time to 0 will not work. Note that setting the time to 0 will not work.
Use :func:`~plexapi.mixins.PlayedMixin.markPlayed` or Use :func:`~plexapi.mixins.PlayedUnplayedMixin.markPlayed` or
:func:`~plexapi.mixins.PlayedMixin.markUnplayed` to achieve :func:`~plexapi.mixins.PlayedUnplayedMixin.markUnplayed` to achieve
that goal. that goal.
Parameters: Parameters:

View file

@ -8,7 +8,7 @@ from plexapi.library import LibrarySection, ManagedHub
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
LabelMixin LabelMixin
) )
from plexapi.utils import deprecated from plexapi.utils import deprecated
@ -19,7 +19,7 @@ class Collection(
PlexPartialObject, PlexPartialObject,
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
LabelMixin LabelMixin
): ):
""" Represents a single Collection. """ Represents a single Collection.

View file

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

View file

@ -399,6 +399,10 @@ class AudioStream(MediaPartStream):
self.peak = utils.cast(float, data.attrib.get('peak')) self.peak = utils.cast(float, data.attrib.get('peak'))
self.startRamp = data.attrib.get('startRamp') self.startRamp = data.attrib.get('startRamp')
def setDefault(self):
""" Sets this audio stream as the default audio stream. """
return self._parent().setDefaultAudioStream(self)
@utils.registerPlexObject @utils.registerPlexObject
class SubtitleStream(MediaPartStream): class SubtitleStream(MediaPartStream):
@ -425,6 +429,10 @@ class SubtitleStream(MediaPartStream):
self.headerCompression = data.attrib.get('headerCompression') self.headerCompression = data.attrib.get('headerCompression')
self.transient = data.attrib.get('transient') self.transient = data.attrib.get('transient')
def setDefault(self):
""" Sets this subtitle stream as the default subtitle stream. """
return self._parent().setDefaultSubtitleStream(self)
class LyricStream(MediaPartStream): class LyricStream(MediaPartStream):
""" Represents a lyric stream within a :class:`~plexapi.media.MediaPart`. """ Represents a lyric stream within a :class:`~plexapi.media.MediaPart`.
@ -1037,9 +1045,11 @@ class Marker(PlexObject):
Attributes: Attributes:
TAG (str): 'Marker' TAG (str): 'Marker'
end (int): The end time of the marker in milliseconds. end (int): The end time of the marker in milliseconds.
final (bool): True if the marker is the final credits marker.
id (int): The ID of the marker. id (int): The ID of the marker.
type (str): The type of marker. type (str): The type of marker.
start (int): The start time of the marker in milliseconds. start (int): The start time of the marker in milliseconds.
version (int): The Plex marker version.
""" """
TAG = 'Marker' TAG = 'Marker'
@ -1053,10 +1063,25 @@ class Marker(PlexObject):
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.end = utils.cast(int, data.attrib.get('endTimeOffset')) self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
self.final = utils.cast(bool, data.attrib.get('final'))
self.id = utils.cast(int, data.attrib.get('id')) self.id = utils.cast(int, data.attrib.get('id'))
self.type = data.attrib.get('type') self.type = data.attrib.get('type')
self.start = utils.cast(int, data.attrib.get('startTimeOffset')) self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
attributes = data.find('Attributes')
self.version = attributes.attrib.get('version')
@property
def first(self):
""" Returns True if the marker in the first credits marker. """
if self.type != 'credits':
return None
first = min(
(marker for marker in self._parent().markers if marker.type == 'credits'),
key=lambda m: m.start
)
return first == self
@utils.registerPlexObject @utils.registerPlexObject
class Field(PlexObject): class Field(PlexObject):

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime from datetime import datetime
from urllib.parse import parse_qsl, quote_plus, unquote, urlencode, urlsplit from urllib.parse import parse_qsl, quote, quote_plus, unquote, urlencode, urlsplit
from plexapi import media, settings, utils from plexapi import media, settings, utils
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
@ -557,6 +557,24 @@ class EditFieldMixin:
return self._edit(**edits) return self._edit(**edits)
class AddedAtMixin(EditFieldMixin):
""" Mixin for Plex objects that can have an added at date. """
def editAddedAt(self, addedAt, locked=True):
""" Edit the added at date.
Parameters:
addedAt (int or str or datetime): The new value as a unix timestamp (int),
"YYYY-MM-DD" (str), or datetime object.
locked (bool): True (default) to lock the field, False to unlock the field.
"""
if isinstance(addedAt, str):
addedAt = int(round(datetime.strptime(addedAt, '%Y-%m-%d').timestamp()))
elif isinstance(addedAt, datetime):
addedAt = int(round(addedAt.timestamp()))
return self.editField('addedAt', addedAt, locked=locked)
class ContentRatingMixin(EditFieldMixin): class ContentRatingMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a content rating. """ """ Mixin for Plex objects that can have a content rating. """
@ -590,7 +608,7 @@ class OriginallyAvailableMixin(EditFieldMixin):
""" Edit the originally available date. """ Edit the originally available date.
Parameters: Parameters:
originallyAvailable (str or datetime): The new value (YYYY-MM-DD) or datetime object. originallyAvailable (str or datetime): The new value "YYYY-MM-DD (str) or datetime object.
locked (bool): True (default) to lock the field, False to unlock the field. locked (bool): True (default) to lock the field, False to unlock the field.
""" """
if isinstance(originallyAvailable, datetime): if isinstance(originallyAvailable, datetime):
@ -726,7 +744,7 @@ class PhotoCapturedTimeMixin(EditFieldMixin):
""" Edit the photo captured time. """ Edit the photo captured time.
Parameters: Parameters:
capturedTime (str or datetime): The new value (YYYY-MM-DD hh:mm:ss) or datetime object. capturedTime (str or datetime): The new value "YYYY-MM-DD hh:mm:ss" (str) or datetime object.
locked (bool): True (default) to lock the field, False to unlock the field. locked (bool): True (default) to lock the field, False to unlock the field.
""" """
if isinstance(capturedTime, datetime): if isinstance(capturedTime, datetime):
@ -804,7 +822,7 @@ class EditTagsMixin:
if remove: if remove:
tagname = f'{tag}[].tag.tag-' tagname = f'{tag}[].tag.tag-'
data[tagname] = ','.join([str(t) for t in items]) data[tagname] = ','.join([quote(str(t)) for t in items])
else: else:
for i, item in enumerate(items): for i, item in enumerate(items):
tagname = f'{str(tag)}[{i}].tag.tag' tagname = f'{str(tag)}[{i}].tag.tag'

View file

@ -8,7 +8,7 @@ from plexapi.exceptions import BadRequest
from plexapi.mixins import ( from plexapi.mixins import (
RatingMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin, AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin,
TagMixin TagMixin
) )
@ -18,7 +18,7 @@ class Photoalbum(
PlexPartialObject, PlexPartialObject,
RatingMixin, RatingMixin,
ArtMixin, PosterMixin, ArtMixin, PosterMixin,
SortTitleMixin, SummaryMixin, TitleMixin AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin
): ):
""" Represents a single Photoalbum (collection of photos). """ Represents a single Photoalbum (collection of photos).
@ -146,7 +146,7 @@ class Photo(
PlexPartialObject, Playable, PlexPartialObject, Playable,
RatingMixin, RatingMixin,
ArtUrlMixin, PosterUrlMixin, ArtUrlMixin, PosterUrlMixin,
PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin, AddedAtMixin, PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin,
TagMixin TagMixin
): ):
""" Represents a single Photo. """ Represents a single Photo.

View file

@ -827,7 +827,7 @@ class PlexServer(PlexObject):
return notifier return notifier
def transcodeImage(self, imageUrl, height, width, def transcodeImage(self, imageUrl, height, width,
opacity=None, saturation=None, blur=None, background=None, opacity=None, saturation=None, blur=None, background=None, blendColor=None,
minSize=True, upscale=True, imageFormat=None): minSize=True, upscale=True, imageFormat=None):
""" Returns the URL for a transcoded image. """ Returns the URL for a transcoded image.
@ -842,6 +842,7 @@ class PlexServer(PlexObject):
saturation (int, optional): Change the saturation of the image (0 to 100). saturation (int, optional): Change the saturation of the image (0 to 100).
blur (int, optional): The blur to apply to the image in pixels (e.g. 3). blur (int, optional): The blur to apply to the image in pixels (e.g. 3).
background (str, optional): The background hex colour to apply behind the opacity (e.g. '000000'). background (str, optional): The background hex colour to apply behind the opacity (e.g. '000000').
blendColor (str, optional): The hex colour to blend the image with (e.g. '000000').
minSize (bool, optional): Maintain smallest dimension. Default True. minSize (bool, optional): Maintain smallest dimension. Default True.
upscale (bool, optional): Upscale the image if required. Default True. upscale (bool, optional): Upscale the image if required. Default True.
imageFormat (str, optional): 'jpeg' (default) or 'png'. imageFormat (str, optional): 'jpeg' (default) or 'png'.
@ -861,6 +862,8 @@ class PlexServer(PlexObject):
params['blur'] = blur params['blur'] = blur
if background is not None: if background is not None:
params['background'] = str(background).strip('#') params['background'] = str(background).strip('#')
if blendColor is not None:
params['blendColor'] = str(blendColor).strip('#')
if imageFormat is not None: if imageFormat is not None:
params['format'] = imageFormat.lower() params['format'] = imageFormat.lower()

View file

@ -53,6 +53,8 @@ SEARCHTYPES = {
'optimizedVersion': 42, 'optimizedVersion': 42,
'userPlaylistItem': 1001, 'userPlaylistItem': 1001,
} }
REVERSESEARCHTYPES = {v: k for k, v in SEARCHTYPES.items()}
# Tag Types - Plex uses these to filter specific tags when searching. # Tag Types - Plex uses these to filter specific tags when searching.
TAGTYPES = { TAGTYPES = {
'tag': 0, 'tag': 0,
@ -91,6 +93,8 @@ TAGTYPES = {
'network': 319, 'network': 319,
'place': 400, 'place': 400,
} }
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}
# Plex Objects - Populated at runtime # Plex Objects - Populated at runtime
PLEXOBJECTS = {} PLEXOBJECTS = {}
@ -219,11 +223,12 @@ def searchType(libtype):
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype :exc:`~plexapi.exceptions.NotFound`: Unknown libtype
""" """
libtype = str(libtype) libtype = str(libtype)
if libtype in [str(v) for v in SEARCHTYPES.values()]: try:
return libtype
if SEARCHTYPES.get(libtype) is not None:
return SEARCHTYPES[libtype] return SEARCHTYPES[libtype]
raise NotFound(f'Unknown libtype: {libtype}') except KeyError:
if libtype in [str(k) for k in REVERSESEARCHTYPES]:
return libtype
raise NotFound(f'Unknown libtype: {libtype}') from None
def reverseSearchType(libtype): def reverseSearchType(libtype):
@ -235,13 +240,12 @@ def reverseSearchType(libtype):
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype :exc:`~plexapi.exceptions.NotFound`: Unknown libtype
""" """
if libtype in SEARCHTYPES: try:
return libtype return REVERSESEARCHTYPES[int(libtype)]
libtype = int(libtype) except (KeyError, ValueError):
for k, v in SEARCHTYPES.items(): if libtype in SEARCHTYPES:
if libtype == v: return libtype
return k raise NotFound(f'Unknown libtype: {libtype}') from None
raise NotFound(f'Unknown libtype: {libtype}')
def tagType(tag): def tagType(tag):
@ -254,11 +258,12 @@ def tagType(tag):
:exc:`~plexapi.exceptions.NotFound`: Unknown tag :exc:`~plexapi.exceptions.NotFound`: Unknown tag
""" """
tag = str(tag) tag = str(tag)
if tag in [str(v) for v in TAGTYPES.values()]: try:
return tag
if TAGTYPES.get(tag) is not None:
return TAGTYPES[tag] return TAGTYPES[tag]
raise NotFound(f'Unknown tag: {tag}') except KeyError:
if tag in [str(k) for k in REVERSETAGTYPES]:
return tag
raise NotFound(f'Unknown tag: {tag}') from None
def reverseTagType(tag): def reverseTagType(tag):
@ -270,13 +275,12 @@ def reverseTagType(tag):
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown tag :exc:`~plexapi.exceptions.NotFound`: Unknown tag
""" """
if tag in TAGTYPES: try:
return tag return REVERSETAGTYPES[int(tag)]
tag = int(tag) except (KeyError, ValueError):
for k, v in TAGTYPES.items(): if tag in TAGTYPES:
if tag == v: return tag
return k raise NotFound(f'Unknown tag: {tag}') from None
raise NotFound(f'Unknown tag: {tag}')
def threaded(callback, listargs): def threaded(callback, listargs):

View file

@ -8,14 +8,14 @@ from plexapi.exceptions import BadRequest
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin,
SummaryMixin, TaglineMixin, TitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin WatchlistMixin
) )
class Video(PlexPartialObject, PlayedUnplayedMixin): class Video(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin):
""" Base class for all video objects including :class:`~plexapi.video.Movie`, """ Base class for all video objects including :class:`~plexapi.video.Movie`,
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`, :class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
:class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`. :class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`.
@ -97,10 +97,25 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
""" Returns str, default title for a new syncItem. """ """ Returns str, default title for a new syncItem. """
return self.title return self.title
def videoStreams(self):
""" Returns a list of :class:`~plexapi.media.videoStream` objects for all MediaParts. """
streams = []
if self.isPartialObject():
self.reload()
parts = self.iterParts()
for part in parts:
streams += part.videoStreams()
return streams
def audioStreams(self): def audioStreams(self):
""" Returns a list of :class:`~plexapi.media.AudioStream` objects for all MediaParts. """ """ Returns a list of :class:`~plexapi.media.AudioStream` objects for all MediaParts. """
streams = [] streams = []
if self.isPartialObject():
self.reload()
parts = self.iterParts() parts = self.iterParts()
for part in parts: for part in parts:
streams += part.audioStreams() streams += part.audioStreams()
@ -110,6 +125,9 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """ """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
streams = [] streams = []
if self.isPartialObject():
self.reload()
parts = self.iterParts() parts = self.iterParts()
for part in parts: for part in parts:
streams += part.subtitleStreams() streams += part.subtitleStreams()
@ -311,11 +329,13 @@ class Movie(
directors (List<:class:`~plexapi.media.Director`>): List of director objects. directors (List<:class:`~plexapi.media.Director`>): List of director objects.
duration (int): Duration of the movie in milliseconds. duration (int): Duration of the movie in milliseconds.
editionTitle (str): The edition title of the movie (e.g. Director's Cut, Extended Edition, etc.). editionTitle (str): The edition title of the movie (e.g. Director's Cut, Extended Edition, etc.).
enableCreditsMarkerGeneration (int): Setting that indicates if credits markers detection is enabled.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
labels (List<:class:`~plexapi.media.Label`>): List of label objects. labels (List<:class:`~plexapi.media.Label`>): List of label objects.
languageOverride (str): Setting that indicates if a language is used to override metadata languageOverride (str): Setting that indicates if a language is used to override metadata
(eg. en-CA, None = Library default). (eg. en-CA, None = Library default).
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
media (List<:class:`~plexapi.media.Media`>): List of media objects. media (List<:class:`~plexapi.media.Media`>): List of media objects.
originallyAvailableAt (datetime): Datetime the movie was released. originallyAvailableAt (datetime): Datetime the movie was released.
originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀). originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀).
@ -353,10 +373,12 @@ class Movie(
self.directors = self.findItems(data, media.Director) self.directors = self.findItems(data, media.Director)
self.duration = utils.cast(int, data.attrib.get('duration')) self.duration = utils.cast(int, data.attrib.get('duration'))
self.editionTitle = data.attrib.get('editionTitle') self.editionTitle = data.attrib.get('editionTitle')
self.enableCreditsMarkerGeneration = utils.cast(int, data.attrib.get('enableCreditsMarkerGeneration', '-1'))
self.genres = self.findItems(data, media.Genre) self.genres = self.findItems(data, media.Genre)
self.guids = self.findItems(data, media.Guid) self.guids = self.findItems(data, media.Guid)
self.labels = self.findItems(data, media.Label) self.labels = self.findItems(data, media.Label)
self.languageOverride = data.attrib.get('languageOverride') self.languageOverride = data.attrib.get('languageOverride')
self.markers = self.findItems(data, media.Marker)
self.media = self.findItems(data, media.Media) self.media = self.findItems(data, media.Media)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
self.originalTitle = data.attrib.get('originalTitle') self.originalTitle = data.attrib.get('originalTitle')
@ -390,6 +412,11 @@ class Movie(
""" """
return [part.file for part in self.iterParts() if part] return [part.file for part in self.iterParts() if part]
@property
def hasCreditsMarker(self):
""" Returns True if the movie has a credits marker. """
return any(marker.type == 'credits' for marker in self.markers)
@property @property
def hasPreviewThumbnails(self): def hasPreviewThumbnails(self):
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """ """ Returns True if any of the media parts has generated preview (BIF) thumbnails. """
@ -432,6 +459,7 @@ class Show(
TYPE (str): 'show' TYPE (str): 'show'
audienceRating (float): Audience rating (TMDB or TVDB). audienceRating (float): Audience rating (TMDB or TVDB).
audienceRatingImage (str): Key to audience rating image (tmdb://image.rating). audienceRatingImage (str): Key to audience rating image (tmdb://image.rating).
audioLanguage (str): Setting that indicates the preferred audio language.
autoDeletionItemPolicyUnwatchedLibrary (int): Setting that indicates the number of unplayed autoDeletionItemPolicyUnwatchedLibrary (int): Setting that indicates the number of unplayed
episodes to keep for the show (0 = All episodes, 5 = 5 latest episodes, 3 = 3 latest episodes, episodes to keep for the show (0 = All episodes, 5 = 5 latest episodes, 3 = 3 latest episodes,
1 = 1 latest episode, -3 = Episodes added in the past 3 days, -7 = Episodes added in the 1 = 1 latest episode, -3 = Episodes added in the past 3 days, -7 = Episodes added in the
@ -440,10 +468,11 @@ class Show(
after being watched for the show (0 = Never, 1 = After a day, 7 = After a week, after being watched for the show (0 = Never, 1 = After a day, 7 = After a week,
100 = On next refresh). 100 = On next refresh).
banner (str): Key to banner artwork (/library/metadata/<ratingkey>/banner/<bannerid>). banner (str): Key to banner artwork (/library/metadata/<ratingkey>/banner/<bannerid>).
childCount (int): Number of seasons in the show. childCount (int): Number of seasons (including Specials) in the show.
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
contentRating (str) Content rating (PG-13; NR; TV-G). contentRating (str) Content rating (PG-13; NR; TV-G).
duration (int): Typical duration of the show episodes in milliseconds. duration (int): Typical duration of the show episodes in milliseconds.
enableCreditsMarkerGeneration (int): Setting that indicates if credits markers detection is enabled.
episodeSort (int): Setting that indicates how episodes are sorted for the show episodeSort (int): Setting that indicates how episodes are sorted for the show
(-1 = Library default, 0 = Oldest first, 1 = Newest first). (-1 = Library default, 0 = Oldest first, 1 = Newest first).
flattenSeasons (int): Setting that indicates if seasons are set to hidden for the show flattenSeasons (int): Setting that indicates if seasons are set to hidden for the show
@ -463,10 +492,14 @@ class Show(
rating (float): Show rating (7.9; 9.8; 8.1). rating (float): Show rating (7.9; 9.8; 8.1).
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects. ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
roles (List<:class:`~plexapi.media.Role`>): List of role objects. roles (List<:class:`~plexapi.media.Role`>): List of role objects.
seasonCount (int): Number of seasons (excluding Specials) in the show.
showOrdering (str): Setting that indicates the episode ordering for the show showOrdering (str): Setting that indicates the episode ordering for the show
(None = Library default). (None = Library default).
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment). studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment).
subtitleLanguage (str): Setting that indicates the preferred subtitle language.
subtitleMode (int): Setting that indicates the auto-select subtitle mode.
(-1 = Account default, 0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled).
tagline (str): Show tag line. tagline (str): Show tag line.
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>). theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
useOriginalTitle (int): Setting that indicates if the original title is used for the show useOriginalTitle (int): Setting that indicates if the original title is used for the show
@ -483,6 +516,7 @@ class Show(
Video._loadData(self, data) Video._loadData(self, data)
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
self.audienceRatingImage = data.attrib.get('audienceRatingImage') self.audienceRatingImage = data.attrib.get('audienceRatingImage')
self.audioLanguage = data.attrib.get('audioLanguage', '')
self.autoDeletionItemPolicyUnwatchedLibrary = utils.cast( self.autoDeletionItemPolicyUnwatchedLibrary = utils.cast(
int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0')) int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0'))
self.autoDeletionItemPolicyWatchedLibrary = utils.cast( self.autoDeletionItemPolicyWatchedLibrary = utils.cast(
@ -492,6 +526,7 @@ class Show(
self.collections = self.findItems(data, media.Collection) self.collections = self.findItems(data, media.Collection)
self.contentRating = data.attrib.get('contentRating') self.contentRating = data.attrib.get('contentRating')
self.duration = utils.cast(int, data.attrib.get('duration')) self.duration = utils.cast(int, data.attrib.get('duration'))
self.enableCreditsMarkerGeneration = utils.cast(int, data.attrib.get('enableCreditsMarkerGeneration', '-1'))
self.episodeSort = utils.cast(int, data.attrib.get('episodeSort', '-1')) self.episodeSort = utils.cast(int, data.attrib.get('episodeSort', '-1'))
self.flattenSeasons = utils.cast(int, data.attrib.get('flattenSeasons', '-1')) self.flattenSeasons = utils.cast(int, data.attrib.get('flattenSeasons', '-1'))
self.genres = self.findItems(data, media.Genre) self.genres = self.findItems(data, media.Genre)
@ -508,9 +543,12 @@ class Show(
self.rating = utils.cast(float, data.attrib.get('rating')) self.rating = utils.cast(float, data.attrib.get('rating'))
self.ratings = self.findItems(data, media.Rating) self.ratings = self.findItems(data, media.Rating)
self.roles = self.findItems(data, media.Role) self.roles = self.findItems(data, media.Role)
self.seasonCount = utils.cast(int, data.attrib.get('seasonCount', self.childCount))
self.showOrdering = data.attrib.get('showOrdering') self.showOrdering = data.attrib.get('showOrdering')
self.similar = self.findItems(data, media.Similar) self.similar = self.findItems(data, media.Similar)
self.studio = data.attrib.get('studio') self.studio = data.attrib.get('studio')
self.subtitleLanguage = data.attrib.get('audioLanguage', '')
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
self.tagline = data.attrib.get('tagline') self.tagline = data.attrib.get('tagline')
self.theme = data.attrib.get('theme') self.theme = data.attrib.get('theme')
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
@ -619,7 +657,7 @@ class Show(
@utils.registerPlexObject @utils.registerPlexObject
class Season( class Season(
Video, Video,
ExtrasMixin, RatingMixin, AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin, ArtMixin, PosterMixin, ThemeUrlMixin,
SummaryMixin, TitleMixin, SummaryMixin, TitleMixin,
CollectionMixin, LabelMixin CollectionMixin, LabelMixin
@ -629,6 +667,7 @@ class Season(
Attributes: Attributes:
TAG (str): 'Directory' TAG (str): 'Directory'
TYPE (str): 'season' TYPE (str): 'season'
audioLanguage (str): Setting that indicates the preferred audio language.
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
index (int): Season number. index (int): Season number.
@ -644,6 +683,9 @@ class Season(
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>). parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the show for the season. parentTitle (str): Name of the show for the season.
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects. ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
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).
viewedLeafCount (int): Number of items marked as played in the season view. viewedLeafCount (int): Number of items marked as played in the season view.
year (int): Year the season was released. year (int): Year the season was released.
""" """
@ -654,6 +696,7 @@ class Season(
def _loadData(self, data): def _loadData(self, data):
""" Load attribute values from Plex XML response. """ """ Load attribute values from Plex XML response. """
Video._loadData(self, data) Video._loadData(self, data)
self.audioLanguage = data.attrib.get('audioLanguage', '')
self.collections = self.findItems(data, media.Collection) self.collections = self.findItems(data, media.Collection)
self.guids = self.findItems(data, media.Guid) self.guids = self.findItems(data, media.Guid)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
@ -669,6 +712,8 @@ class Season(
self.parentThumb = data.attrib.get('parentThumb') self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle') self.parentTitle = data.attrib.get('parentTitle')
self.ratings = self.findItems(data, media.Rating) self.ratings = self.findItems(data, media.Rating)
self.subtitleLanguage = data.attrib.get('audioLanguage', '')
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
self.year = utils.cast(int, data.attrib.get('year')) self.year = utils.cast(int, data.attrib.get('year'))
@ -918,14 +963,19 @@ class Episode(
@property @property
def hasCommercialMarker(self): def hasCommercialMarker(self):
""" Returns True if the episode has a commercial marker in the xml. """ """ Returns True if the episode has a commercial marker. """
return any(marker.type == 'commercial' for marker in self.markers) return any(marker.type == 'commercial' for marker in self.markers)
@property @property
def hasIntroMarker(self): def hasIntroMarker(self):
""" Returns True if the episode has an intro marker in the xml. """ """ Returns True if the episode has an intro marker. """
return any(marker.type == 'intro' for marker in self.markers) return any(marker.type == 'intro' for marker in self.markers)
@property
def hasCreditsMarker(self):
""" Returns True if the episode has a credits marker. """
return any(marker.type == 'credits' for marker in self.markers)
@property @property
def hasPreviewThumbnails(self): def hasPreviewThumbnails(self):
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """ """ Returns True if any of the media parts has generated preview (BIF) thumbnails. """

View file

@ -28,7 +28,7 @@ MarkupSafe==2.1.2
musicbrainzngs==0.7.1 musicbrainzngs==0.7.1
packaging==23.0 packaging==23.0
paho-mqtt==1.6.1 paho-mqtt==1.6.1
plexapi==4.13.2 plexapi==4.13.4
portend==3.1.0 portend==3.1.0
profilehooks==1.12.0 profilehooks==1.12.0
PyJWT==2.6.0 PyJWT==2.6.0