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")
for encoding_iana in prioritized_encodings + IANA_SUPPORTED:
if cp_isolation and encoding_iana not in cp_isolation:
continue
@ -318,7 +317,9 @@ def from_bytes(
bom_or_sig_available and strip_sig_or_bom is False
):
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(
TRACE,
"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)
for language, language_characters in FREQUENCIES.items():
target_have_accents, target_pure_latin = get_target_features(language)
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_ = []
for my_file in args.files:
matches = from_fp(my_file, threshold=args.threshold, explain=args.verbose)
best_guess = matches.best()
@ -222,7 +221,6 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:
)
if args.normalize is True:
if best_guess.encoding.startswith("utf") is True:
print(
'"{}" 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 .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
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.
: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)):
raise TypeError( # pragma: nocover
"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:
encoding += "_sig"
if should_rename_legacy is False and encoding in CHARDET_CORRESPONDENCE:
encoding = CHARDET_CORRESPONDENCE[encoding]
return {
"encoding": encoding
if encoding not in CHARDET_CORRESPONDENCE
else CHARDET_CORRESPONDENCE[encoding],
"encoding": encoding,
"language": language,
"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:
if is_multi_byte_encoding(iana_name_a) or is_multi_byte_encoding(iana_name_b):
return 0.0
@ -351,7 +350,6 @@ def set_logging_handler(
level: int = logging.INFO,
format_string: str = "%(asctime)s | %(levelname)s | %(message)s",
) -> None:
logger = logging.getLogger(name)
logger.setLevel(level)
@ -371,7 +369,6 @@ def cut_sequence_chunks(
is_multi_byte_decoder: bool,
decoded_payload: Optional[str] = None,
) -> Generator[str, None, None]:
if decoded_payload and is_multi_byte_decoder is False:
for i in offsets:
chunk = decoded_payload[i : i + chunk_size]
@ -397,7 +394,6 @@ def cut_sequence_chunks(
# multi-byte bad cutting detector and adjustment
# not the cleanest way to perform that fix but clever enough for now.
if is_multi_byte_decoder and i > 0:
chunk_partial_size_chk: int = min(chunk_size, 16)
if (

View file

@ -2,5 +2,5 @@
Expose version
"""
__version__ = "3.0.1"
__version__ = "3.1.0"
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
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_PRODUCT = CONFIG.get('header.product', PROJECT)
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)

View file

@ -8,14 +8,14 @@ from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
)
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`,
: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.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.
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
offset, copyts, protocol, mediaIndex, platform.
offset, copyts, protocol, mediaIndex, partIndex, platform.
Raises:
:exc:`~plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
"""
if self.TYPE not in ('movie', 'episode', 'track', 'clip'):
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 = {
'path': self.key,
'offset': params.get('offset', 0),
'copyts': params.get('copyts', 1),
'protocol': params.get('protocol'),
'mediaIndex': params.get('mediaIndex', 0),
'X-Plex-Platform': params.get('platform', 'Chrome'),
'mediaIndex': kwargs.pop('mediaIndex', 0),
'partIndex': kwargs.pop('mediaIndex', 0),
'protocol': protocol,
'fastSeek': kwargs.pop('fastSeek', 1),
'copyts': kwargs.pop('copyts', 1),
'offset': kwargs.pop('offset', 0),
'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
params = {k: v for k, v in params.items() if v is not None}
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
# sort the keys since the randomness fucks with my tests..
sorted_params = sorted(params.items(), key=lambda val: val[0])
ext = 'mpd' if protocol == 'dash' else 'm3u8'
return self._server.url(
f'/{streamtype}/:/transcode/universal/start.m3u8?{urlencode(sorted_params)}',
f'/{streamtype}/:/transcode/universal/start.{ext}?{urlencode(params)}',
includeToken=True
)
@ -795,8 +802,8 @@ class Playable:
""" Set the watched progress for this video.
Note that setting the time to 0 will not work.
Use :func:`~plexapi.mixins.PlayedMixin.markPlayed` or
:func:`~plexapi.mixins.PlayedMixin.markUnplayed` to achieve
Use :func:`~plexapi.mixins.PlayedUnplayedMixin.markPlayed` or
:func:`~plexapi.mixins.PlayedUnplayedMixin.markUnplayed` to achieve
that goal.
Parameters:

View file

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

View file

@ -4,6 +4,6 @@
# Library version
MAJOR_VERSION = 4
MINOR_VERSION = 13
PATCH_VERSION = 2
PATCH_VERSION = 4
__short_version__ = f"{MAJOR_VERSION}.{MINOR_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.startRamp = data.attrib.get('startRamp')
def setDefault(self):
""" Sets this audio stream as the default audio stream. """
return self._parent().setDefaultAudioStream(self)
@utils.registerPlexObject
class SubtitleStream(MediaPartStream):
@ -425,6 +429,10 @@ class SubtitleStream(MediaPartStream):
self.headerCompression = data.attrib.get('headerCompression')
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):
""" Represents a lyric stream within a :class:`~plexapi.media.MediaPart`.
@ -1037,9 +1045,11 @@ class Marker(PlexObject):
Attributes:
TAG (str): 'Marker'
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.
type (str): The type of marker.
start (int): The start time of the marker in milliseconds.
version (int): The Plex marker version.
"""
TAG = 'Marker'
@ -1053,10 +1063,25 @@ class Marker(PlexObject):
def _loadData(self, data):
self._data = data
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.type = data.attrib.get('type')
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
class Field(PlexObject):

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
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.exceptions import BadRequest, NotFound
@ -557,6 +557,24 @@ class EditFieldMixin:
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):
""" Mixin for Plex objects that can have a content rating. """
@ -590,7 +608,7 @@ class OriginallyAvailableMixin(EditFieldMixin):
""" Edit the originally available date.
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.
"""
if isinstance(originallyAvailable, datetime):
@ -726,7 +744,7 @@ class PhotoCapturedTimeMixin(EditFieldMixin):
""" Edit the photo captured time.
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.
"""
if isinstance(capturedTime, datetime):
@ -804,7 +822,7 @@ class EditTagsMixin:
if remove:
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:
for i, item in enumerate(items):
tagname = f'{str(tag)}[{i}].tag.tag'

View file

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

View file

@ -827,7 +827,7 @@ class PlexServer(PlexObject):
return notifier
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):
""" 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).
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').
blendColor (str, optional): The hex colour to blend the image with (e.g. '000000').
minSize (bool, optional): Maintain smallest dimension. Default True.
upscale (bool, optional): Upscale the image if required. Default True.
imageFormat (str, optional): 'jpeg' (default) or 'png'.
@ -861,6 +862,8 @@ class PlexServer(PlexObject):
params['blur'] = blur
if background is not None:
params['background'] = str(background).strip('#')
if blendColor is not None:
params['blendColor'] = str(blendColor).strip('#')
if imageFormat is not None:
params['format'] = imageFormat.lower()

View file

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

View file

@ -8,14 +8,14 @@ from plexapi.exceptions import BadRequest
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
SummaryMixin, TaglineMixin, TitleMixin,
AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin,
StudioMixin, SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin
)
class Video(PlexPartialObject, PlayedUnplayedMixin):
class Video(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin):
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
: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. """
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):
""" Returns a list of :class:`~plexapi.media.AudioStream` objects for all MediaParts. """
streams = []
if self.isPartialObject():
self.reload()
parts = self.iterParts()
for part in parts:
streams += part.audioStreams()
@ -110,6 +125,9 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
streams = []
if self.isPartialObject():
self.reload()
parts = self.iterParts()
for part in parts:
streams += part.subtitleStreams()
@ -311,11 +329,13 @@ class Movie(
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
duration (int): Duration of the movie in milliseconds.
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.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
languageOverride (str): Setting that indicates if a language is used to override metadata
(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.
originallyAvailableAt (datetime): Datetime the movie was released.
originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀).
@ -353,10 +373,12 @@ class Movie(
self.directors = self.findItems(data, media.Director)
self.duration = utils.cast(int, data.attrib.get('duration'))
self.editionTitle = data.attrib.get('editionTitle')
self.enableCreditsMarkerGeneration = utils.cast(int, data.attrib.get('enableCreditsMarkerGeneration', '-1'))
self.genres = self.findItems(data, media.Genre)
self.guids = self.findItems(data, media.Guid)
self.labels = self.findItems(data, media.Label)
self.languageOverride = data.attrib.get('languageOverride')
self.markers = self.findItems(data, media.Marker)
self.media = self.findItems(data, media.Media)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
self.originalTitle = data.attrib.get('originalTitle')
@ -390,6 +412,11 @@ class Movie(
"""
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
def hasPreviewThumbnails(self):
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """
@ -432,6 +459,7 @@ class Show(
TYPE (str): 'show'
audienceRating (float): Audience rating (TMDB or TVDB).
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
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
@ -440,10 +468,11 @@ class Show(
after being watched for the show (0 = Never, 1 = After a day, 7 = After a week,
100 = On next refresh).
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.
contentRating (str) Content rating (PG-13; NR; TV-G).
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
(-1 = Library default, 0 = Oldest first, 1 = Newest first).
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).
ratings (List<:class:`~plexapi.media.Rating`>): List of rating 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
(None = Library default).
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
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.
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
@ -483,6 +516,7 @@ class Show(
Video._loadData(self, data)
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
self.audioLanguage = data.attrib.get('audioLanguage', '')
self.autoDeletionItemPolicyUnwatchedLibrary = utils.cast(
int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0'))
self.autoDeletionItemPolicyWatchedLibrary = utils.cast(
@ -492,6 +526,7 @@ class Show(
self.collections = self.findItems(data, media.Collection)
self.contentRating = data.attrib.get('contentRating')
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.flattenSeasons = utils.cast(int, data.attrib.get('flattenSeasons', '-1'))
self.genres = self.findItems(data, media.Genre)
@ -508,9 +543,12 @@ class Show(
self.rating = utils.cast(float, data.attrib.get('rating'))
self.ratings = self.findItems(data, media.Rating)
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.similar = self.findItems(data, media.Similar)
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.theme = data.attrib.get('theme')
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
@ -619,7 +657,7 @@ class Show(
@utils.registerPlexObject
class Season(
Video,
ExtrasMixin, RatingMixin,
AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin,
SummaryMixin, TitleMixin,
CollectionMixin, LabelMixin
@ -629,6 +667,7 @@ class Season(
Attributes:
TAG (str): 'Directory'
TYPE (str): 'season'
audioLanguage (str): Setting that indicates the preferred audio language.
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
index (int): Season number.
@ -644,6 +683,9 @@ class Season(
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.
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.
year (int): Year the season was released.
"""
@ -654,6 +696,7 @@ class Season(
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Video._loadData(self, data)
self.audioLanguage = data.attrib.get('audioLanguage', '')
self.collections = self.findItems(data, media.Collection)
self.guids = self.findItems(data, media.Guid)
self.index = utils.cast(int, data.attrib.get('index'))
@ -669,6 +712,8 @@ class Season(
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
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.year = utils.cast(int, data.attrib.get('year'))
@ -918,14 +963,19 @@ class Episode(
@property
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)
@property
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)
@property
def hasCreditsMarker(self):
""" Returns True if the episode has a credits marker. """
return any(marker.type == 'credits' for marker in self.markers)
@property
def hasPreviewThumbnails(self):
""" 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
packaging==23.0
paho-mqtt==1.6.1
plexapi==4.13.2
plexapi==4.13.4
portend==3.1.0
profilehooks==1.12.0
PyJWT==2.6.0