mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11: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")
|
||||
|
||||
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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
Expose version
|
||||
"""
|
||||
|
||||
__version__ = "3.0.1"
|
||||
__version__ = "3.1.0"
|
||||
VERSION = __version__.split(".")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue