mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 21:21:15 -07:00
Update plexapi==4.13.4
This commit is contained in:
parent
b7da2dedf3
commit
eb7495e930
18 changed files with 187 additions and 75 deletions
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
Expose version
|
Expose version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "3.0.1"
|
__version__ = "3.1.0"
|
||||||
VERSION = __version__.split(".")
|
VERSION = __version__.split(".")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
return REVERSESEARCHTYPES[int(libtype)]
|
||||||
|
except (KeyError, ValueError):
|
||||||
if libtype in SEARCHTYPES:
|
if libtype in SEARCHTYPES:
|
||||||
return libtype
|
return libtype
|
||||||
libtype = int(libtype)
|
raise NotFound(f'Unknown libtype: {libtype}') from None
|
||||||
for k, v in SEARCHTYPES.items():
|
|
||||||
if libtype == v:
|
|
||||||
return k
|
|
||||||
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
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
return REVERSETAGTYPES[int(tag)]
|
||||||
|
except (KeyError, ValueError):
|
||||||
if tag in TAGTYPES:
|
if tag in TAGTYPES:
|
||||||
return tag
|
return tag
|
||||||
tag = int(tag)
|
raise NotFound(f'Unknown tag: {tag}') from None
|
||||||
for k, v in TAGTYPES.items():
|
|
||||||
if tag == v:
|
|
||||||
return k
|
|
||||||
raise NotFound(f'Unknown tag: {tag}')
|
|
||||||
|
|
||||||
|
|
||||||
def threaded(callback, listargs):
|
def threaded(callback, listargs):
|
||||||
|
|
|
@ -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. """
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue