mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 12:59:42 -07:00
Update plexapi==4.17.0
This commit is contained in:
parent
3cb71f94a3
commit
f6bffe1850
32 changed files with 1224 additions and 966 deletions
|
@ -8,7 +8,7 @@ from urllib.parse import quote_plus
|
|||
from typing import Any, Dict, List, Optional, TypeVar
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession, cached_data_property
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
|
||||
|
@ -59,14 +59,11 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||
self.distance = utils.cast(float, data.attrib.get('distance'))
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.images = self.findItems(data, media.Image)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
|
||||
|
@ -75,7 +72,6 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
|
|||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'audio'
|
||||
self.moods = self.findItems(data, media.Mood)
|
||||
self.musicAnalysisVersion = utils.cast(int, data.attrib.get('musicAnalysisVersion'))
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.summary = data.attrib.get('summary')
|
||||
|
@ -88,6 +84,18 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def images(self):
|
||||
return self.findItems(self._data, media.Image)
|
||||
|
||||
@cached_data_property
|
||||
def moods(self):
|
||||
return self.findItems(self._data, media.Mood)
|
||||
|
||||
def url(self, part):
|
||||
""" Returns the full URL for the audio item. Typically used for getting a specific track. """
|
||||
return self._server.url(part, includeToken=True) if part else None
|
||||
|
@ -205,18 +213,45 @@ class Artist(
|
|||
Audio._loadData(self, data)
|
||||
self.albumSort = utils.cast(int, data.attrib.get('albumSort', '-1'))
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.styles = self.findItems(data, media.Style)
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def countries(self):
|
||||
return self.findItems(self._data, media.Country)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def locations(self):
|
||||
return self.listAttrs(self._data, 'path', etag='Location')
|
||||
|
||||
@cached_data_property
|
||||
def similar(self):
|
||||
return self.findItems(self._data, media.Similar)
|
||||
|
||||
@cached_data_property
|
||||
def styles(self):
|
||||
return self.findItems(self._data, media.Style)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
def __iter__(self):
|
||||
for album in self.albums():
|
||||
|
@ -355,12 +390,7 @@ class Album(
|
|||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.formats = self.findItems(data, media.Format)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
|
@ -372,12 +402,41 @@ class Album(
|
|||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.styles = self.findItems(data, media.Style)
|
||||
self.subformats = self.findItems(data, media.Subformat)
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def formats(self):
|
||||
return self.findItems(self._data, media.Format)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def styles(self):
|
||||
return self.findItems(self._data, media.Style)
|
||||
|
||||
@cached_data_property
|
||||
def subformats(self):
|
||||
return self.findItems(self._data, media.Subformat)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
def __iter__(self):
|
||||
for track in self.tracks():
|
||||
yield track
|
||||
|
@ -495,11 +554,8 @@ class Track(
|
|||
Audio._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||
self.grandparentKey = data.attrib.get('grandparentKey')
|
||||
|
@ -507,9 +563,6 @@ class Track(
|
|||
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originalTitle = data.attrib.get('originalTitle')
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
|
@ -525,6 +578,30 @@ class Track(
|
|||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def chapters(self):
|
||||
return self.findItems(self._data, media.Chapter)
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def media(self):
|
||||
return self.findItems(self._data, media.Media)
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
""" This does not exist in plex xml response but is added to have a common
|
||||
|
|
|
@ -39,7 +39,42 @@ OPERATORS = {
|
|||
}
|
||||
|
||||
|
||||
class PlexObject:
|
||||
class cached_data_property(cached_property):
|
||||
"""Caching for PlexObject data properties.
|
||||
|
||||
This decorator creates properties that cache their values with
|
||||
automatic invalidation on data changes.
|
||||
"""
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""Register the annotated property in the parent class's _cached_data_properties set."""
|
||||
super().__set_name__(owner, name)
|
||||
if not hasattr(owner, '_cached_data_properties'):
|
||||
owner._cached_data_properties = set()
|
||||
owner._cached_data_properties.add(name)
|
||||
|
||||
|
||||
class PlexObjectMeta(type):
|
||||
"""Metaclass for PlexObject to handle cached_data_properties."""
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
cached_data_props = set()
|
||||
|
||||
# Merge all _cached_data_properties from parent classes
|
||||
for base in bases:
|
||||
if hasattr(base, '_cached_data_properties'):
|
||||
cached_data_props.update(base._cached_data_properties)
|
||||
|
||||
# Find all properties annotated with cached_data_property in the current class
|
||||
for attr_name, attr_value in attrs.items():
|
||||
if isinstance(attr_value, cached_data_property):
|
||||
cached_data_props.add(attr_name)
|
||||
|
||||
attrs['_cached_data_properties'] = cached_data_props
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class PlexObject(metaclass=PlexObjectMeta):
|
||||
""" Base class for all Plex objects.
|
||||
|
||||
Parameters:
|
||||
|
@ -387,7 +422,7 @@ class PlexObject:
|
|||
return results
|
||||
|
||||
def reload(self, key=None, **kwargs):
|
||||
""" Reload the data for this object from self.key.
|
||||
""" Reload the data for this object.
|
||||
|
||||
Parameters:
|
||||
key (string, optional): Override the key to reload.
|
||||
|
@ -435,7 +470,7 @@ class PlexObject:
|
|||
self._initpath = key
|
||||
data = self._server.query(key)
|
||||
self._overwriteNone = _overwriteNone
|
||||
self._loadData(data[0])
|
||||
self._invalidateCacheAndLoadData(data[0])
|
||||
self._overwriteNone = True
|
||||
return self
|
||||
|
||||
|
@ -497,9 +532,35 @@ class PlexObject:
|
|||
return float(value)
|
||||
return value
|
||||
|
||||
def _invalidateCacheAndLoadData(self, data):
|
||||
"""Load attribute values from Plex XML response and invalidate cached properties."""
|
||||
old_data_id = id(getattr(self, '_data', None))
|
||||
self._data = data
|
||||
|
||||
# If the data's object ID has changed, invalidate cached properties
|
||||
if id(data) != old_data_id:
|
||||
self._invalidateCachedProperties()
|
||||
|
||||
self._loadData(data)
|
||||
|
||||
def _invalidateCachedProperties(self):
|
||||
"""Invalidate all cached data property values."""
|
||||
cached_props = getattr(self.__class__, '_cached_data_properties', set())
|
||||
|
||||
for prop_name in cached_props:
|
||||
if prop_name in self.__dict__:
|
||||
del self.__dict__[prop_name]
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
raise NotImplementedError('Abstract method not implemented.')
|
||||
|
||||
def _findAndLoadElem(self, data, **kwargs):
|
||||
""" Find and load the first element in the data that matches the specified attributes. """
|
||||
for elem in data:
|
||||
if self._checkAttrs(elem, **kwargs):
|
||||
self._invalidateCacheAndLoadData(elem)
|
||||
|
||||
@property
|
||||
def _searchType(self):
|
||||
return self.TYPE
|
||||
|
@ -754,7 +815,7 @@ class PlexPartialObject(PlexObject):
|
|||
|
||||
|
||||
class Playable:
|
||||
""" This is a general place to store functions specific to media that is Playable.
|
||||
""" This is a mixin to store functions specific to media that is Playable.
|
||||
Things were getting mixed up a bit when dealing with Shows, Season, Artists,
|
||||
Albums which are all not playable.
|
||||
|
||||
|
@ -764,6 +825,7 @@ class Playable:
|
|||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
|
||||
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue
|
||||
|
||||
|
@ -931,8 +993,8 @@ class Playable:
|
|||
return self
|
||||
|
||||
|
||||
class PlexSession(object):
|
||||
""" This is a general place to store functions specific to media that is a Plex Session.
|
||||
class PlexSession:
|
||||
""" This is a mixin to store functions specific to media that is a Plex Session.
|
||||
|
||||
Attributes:
|
||||
live (bool): True if this is a live tv session.
|
||||
|
@ -945,23 +1007,44 @@ class PlexSession(object):
|
|||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.live = utils.cast(bool, data.attrib.get('live', '0'))
|
||||
self.player = self.findItem(data, etag='Player')
|
||||
self.session = self.findItem(data, etag='Session')
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.transcodeSession = self.findItem(data, etag='TranscodeSession')
|
||||
|
||||
user = data.find('User')
|
||||
self._username = user.attrib.get('title')
|
||||
self._userId = utils.cast(int, user.attrib.get('id'))
|
||||
|
||||
# For backwards compatibility
|
||||
self.players = [self.player] if self.player else []
|
||||
self.sessions = [self.session] if self.session else []
|
||||
self.transcodeSessions = [self.transcodeSession] if self.transcodeSession else []
|
||||
self.usernames = [self._username] if self._username else []
|
||||
# `players`, `sessions`, and `transcodeSessions` are returned with properties
|
||||
# to support lazy loading. See PR #1510
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def player(self):
|
||||
return self.findItem(self._data, etag='Player')
|
||||
|
||||
@cached_data_property
|
||||
def session(self):
|
||||
return self.findItem(self._data, etag='Session')
|
||||
|
||||
@cached_data_property
|
||||
def transcodeSession(self):
|
||||
return self.findItem(self._data, etag='TranscodeSession')
|
||||
|
||||
@property
|
||||
def players(self):
|
||||
return [self.player] if self.player else []
|
||||
|
||||
@property
|
||||
def sessions(self):
|
||||
return [self.session] if self.session else []
|
||||
|
||||
@property
|
||||
def transcodeSessions(self):
|
||||
return [self.transcodeSession] if self.transcodeSession else []
|
||||
|
||||
@cached_data_property
|
||||
def user(self):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
|
||||
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.
|
||||
|
@ -978,18 +1061,11 @@ class PlexSession(object):
|
|||
"""
|
||||
return self._reload()
|
||||
|
||||
def _reload(self, _autoReload=False, **kwargs):
|
||||
""" Perform the actual reload. """
|
||||
# Do not auto reload sessions
|
||||
if _autoReload:
|
||||
return self
|
||||
|
||||
def _reload(self, **kwargs):
|
||||
""" Reload the data for the session. """
|
||||
key = self._initpath
|
||||
data = self._server.query(key)
|
||||
for elem in data:
|
||||
if elem.attrib.get('sessionKey') == str(self.sessionKey):
|
||||
self._loadData(elem)
|
||||
break
|
||||
self._findAndLoadElem(data, sessionKey=str(self.sessionKey))
|
||||
return self
|
||||
|
||||
def source(self):
|
||||
|
@ -1010,8 +1086,8 @@ class PlexSession(object):
|
|||
return self._server.query(key, params=params)
|
||||
|
||||
|
||||
class PlexHistory(object):
|
||||
""" This is a general place to store functions specific to media that is a Plex history item.
|
||||
class PlexHistory:
|
||||
""" This is a mixin to store functions specific to media that is a Plex history item.
|
||||
|
||||
Attributes:
|
||||
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
|
||||
|
@ -1021,6 +1097,7 @@ class PlexHistory(object):
|
|||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.accountID = utils.cast(int, data.attrib.get('accountID'))
|
||||
self.deviceID = utils.cast(int, data.attrib.get('deviceID'))
|
||||
self.historyKey = data.attrib.get('historyKey')
|
||||
|
@ -1124,7 +1201,7 @@ class MediaContainer(
|
|||
setattr(self, key, getattr(__iterable, key))
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.allowSync = utils.cast(int, data.attrib.get('allowSync'))
|
||||
self.augmentationKey = data.attrib.get('augmentationKey')
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
|
|
|
@ -115,7 +115,7 @@ class PlexClient(PlexObject):
|
|||
)
|
||||
else:
|
||||
client = data[0]
|
||||
self._loadData(client)
|
||||
self._invalidateCacheAndLoadData(client)
|
||||
return self
|
||||
|
||||
def reload(self):
|
||||
|
@ -124,7 +124,6 @@ class PlexClient(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.deviceClass = data.attrib.get('deviceClass')
|
||||
self.machineIdentifier = data.attrib.get('machineIdentifier')
|
||||
self.product = data.attrib.get('product')
|
||||
|
@ -197,8 +196,7 @@ class PlexClient(PlexObject):
|
|||
raise NotFound(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
data = utils.cleanXMLString(response.text).encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
return utils.parseXMLString(response.text)
|
||||
|
||||
def sendCommand(self, command, proxy=None, **params):
|
||||
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query` to more easily
|
||||
|
@ -222,7 +220,7 @@ class PlexClient(PlexObject):
|
|||
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||
query = self._server.query if proxy else self.query
|
||||
|
||||
# Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244
|
||||
# Workaround for ptp. See https://github.com/pushingkarmaorg/python-plexapi/issues/244
|
||||
t = time.time()
|
||||
if command == 'timeline/poll':
|
||||
self._last_call = t
|
||||
|
@ -606,7 +604,7 @@ class ClientTimeline(PlexObject):
|
|||
key = 'timeline/poll'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.address = data.attrib.get('address')
|
||||
self.audioStreamId = utils.cast(int, data.attrib.get('audioStreamId'))
|
||||
self.autoPlay = utils.cast(bool, data.attrib.get('autoPlay'))
|
||||
|
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
|||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import PlexPartialObject
|
||||
from plexapi.base import PlexPartialObject, cached_data_property
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
from plexapi.library import LibrarySection, ManagedHub
|
||||
from plexapi.mixins import (
|
||||
|
@ -69,7 +69,7 @@ class Collection(
|
|||
TYPE = 'collection'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||
|
@ -81,12 +81,9 @@ class Collection(
|
|||
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0'))
|
||||
self.content = data.attrib.get('content')
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.images = self.findItems(data, media.Image)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
|
@ -105,12 +102,24 @@ class Collection(
|
|||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
self.type = data.attrib.get('type')
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self._items = None # cache for self.items
|
||||
self._section = None # cache for self.section
|
||||
self._filters = None # cache for self.filters
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def images(self):
|
||||
return self.findItems(self._data, media.Image)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
def __len__(self): # pragma: no cover
|
||||
return len(self.items())
|
||||
|
@ -162,20 +171,26 @@ class Collection(
|
|||
def children(self):
|
||||
return self.items()
|
||||
|
||||
@cached_data_property
|
||||
def _filters(self):
|
||||
""" Cache for filters. """
|
||||
return self._parseFilters(self.content)
|
||||
|
||||
def filters(self):
|
||||
""" Returns the search filter dict for smart collection.
|
||||
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
|
||||
to get the list of items.
|
||||
"""
|
||||
if self.smart and self._filters is None:
|
||||
self._filters = self._parseFilters(self.content)
|
||||
return self._filters
|
||||
|
||||
@cached_data_property
|
||||
def _section(self):
|
||||
""" Cache for section. """
|
||||
return super(Collection, self).section()
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this collection belongs to.
|
||||
"""
|
||||
if self._section is None:
|
||||
self._section = super(Collection, self).section()
|
||||
return self._section
|
||||
|
||||
def item(self, title):
|
||||
|
@ -192,12 +207,14 @@ class Collection(
|
|||
return item
|
||||
raise NotFound(f'Item with title "{title}" not found in the collection')
|
||||
|
||||
@cached_data_property
|
||||
def _items(self):
|
||||
""" Cache for the items. """
|
||||
key = f'{self.key}/children'
|
||||
return self.fetchItems(key)
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of all items in the collection. """
|
||||
if self._items is None:
|
||||
key = f'{self.key}/children'
|
||||
items = self.fetchItems(key)
|
||||
self._items = items
|
||||
return self._items
|
||||
|
||||
def visibility(self):
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
# Library version
|
||||
MAJOR_VERSION = 4
|
||||
MINOR_VERSION = 16
|
||||
PATCH_VERSION = 1
|
||||
MINOR_VERSION = 17
|
||||
PATCH_VERSION = 0
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
|
|
|
@ -6,11 +6,10 @@ from typing import Any, TYPE_CHECKING
|
|||
import warnings
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse
|
||||
|
||||
from plexapi import log, media, utils
|
||||
from plexapi.base import OPERATORS, PlexObject
|
||||
from plexapi.base import OPERATORS, PlexObject, cached_data_property
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.mixins import (
|
||||
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
|
||||
|
@ -39,14 +38,13 @@ class Library(PlexObject):
|
|||
key = '/library'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
|
||||
self.title1 = data.attrib.get('title1')
|
||||
self.title2 = data.attrib.get('title2')
|
||||
self._sectionsByID = {} # cached sections by key
|
||||
self._sectionsByTitle = {} # cached sections by title
|
||||
|
||||
@cached_data_property
|
||||
def _loadSections(self):
|
||||
""" Loads and caches all the library sections. """
|
||||
key = '/library/sections'
|
||||
|
@ -64,15 +62,23 @@ class Library(PlexObject):
|
|||
sectionsByID[section.key] = section
|
||||
sectionsByTitle[section.title.lower().strip()].append(section)
|
||||
|
||||
self._sectionsByID = sectionsByID
|
||||
self._sectionsByTitle = dict(sectionsByTitle)
|
||||
return sectionsByID, dict(sectionsByTitle)
|
||||
|
||||
@property
|
||||
def _sectionsByID(self):
|
||||
""" Returns a dictionary of all library sections by ID. """
|
||||
return self._loadSections[0]
|
||||
|
||||
@property
|
||||
def _sectionsByTitle(self):
|
||||
""" Returns a dictionary of all library sections by title. """
|
||||
return self._loadSections[1]
|
||||
|
||||
def sections(self):
|
||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
|
||||
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
|
||||
"""
|
||||
self._loadSections()
|
||||
return list(self._sectionsByID.values())
|
||||
|
||||
def section(self, title):
|
||||
|
@ -87,8 +93,6 @@ class Library(PlexObject):
|
|||
:exc:`~plexapi.exceptions.NotFound`: The library section title is not found on the server.
|
||||
"""
|
||||
normalized_title = title.lower().strip()
|
||||
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
|
||||
self._loadSections()
|
||||
try:
|
||||
sections = self._sectionsByTitle[normalized_title]
|
||||
except KeyError:
|
||||
|
@ -110,8 +114,6 @@ class Library(PlexObject):
|
|||
Raises:
|
||||
:exc:`~plexapi.exceptions.NotFound`: The library section ID is not found on the server.
|
||||
"""
|
||||
if not self._sectionsByID or sectionID not in self._sectionsByID:
|
||||
self._loadSections()
|
||||
try:
|
||||
return self._sectionsByID[sectionID]
|
||||
except KeyError:
|
||||
|
@ -385,7 +387,9 @@ class Library(PlexObject):
|
|||
if kwargs:
|
||||
prefs_params = {f'prefs[{k}]': v for k, v in kwargs.items()}
|
||||
part += f'&{urlencode(prefs_params)}'
|
||||
return self._server.query(part, method=self._server._session.post)
|
||||
data = self._server.query(part, method=self._server._session.post)
|
||||
self._invalidateCachedProperties()
|
||||
return data
|
||||
|
||||
def history(self, maxresults=None, mindate=None):
|
||||
""" Get Play History for all library Sections for the owner.
|
||||
|
@ -432,7 +436,7 @@ class LibrarySection(PlexObject):
|
|||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.agent = data.attrib.get('agent')
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.art = data.attrib.get('art')
|
||||
|
@ -441,7 +445,6 @@ class LibrarySection(PlexObject):
|
|||
self.filters = utils.cast(bool, data.attrib.get('filters'))
|
||||
self.key = utils.cast(int, data.attrib.get('key'))
|
||||
self.language = data.attrib.get('language')
|
||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
|
||||
self.scanner = data.attrib.get('scanner')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
@ -449,14 +452,12 @@ class LibrarySection(PlexObject):
|
|||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.uuid = data.attrib.get('uuid')
|
||||
# Private attrs as we don't want a reload.
|
||||
self._filterTypes = None
|
||||
self._fieldTypes = None
|
||||
self._totalViewSize = None
|
||||
self._totalDuration = None
|
||||
self._totalStorage = None
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def locations(self):
|
||||
return self.listAttrs(self._data, 'path', etag='Location')
|
||||
|
||||
@cached_data_property
|
||||
def totalSize(self):
|
||||
""" Returns the total number of items in the library for the default library type. """
|
||||
return self.totalViewSize(includeCollections=False)
|
||||
|
@ -464,16 +465,12 @@ class LibrarySection(PlexObject):
|
|||
@property
|
||||
def totalDuration(self):
|
||||
""" Returns the total duration (in milliseconds) of items in the library. """
|
||||
if self._totalDuration is None:
|
||||
self._getTotalDurationStorage()
|
||||
return self._totalDuration
|
||||
return self._getTotalDurationStorage[0]
|
||||
|
||||
@property
|
||||
def totalStorage(self):
|
||||
""" Returns the total storage (in bytes) of items in the library. """
|
||||
if self._totalStorage is None:
|
||||
self._getTotalDurationStorage()
|
||||
return self._totalStorage
|
||||
return self._getTotalDurationStorage[1]
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
# Intercept to call EditFieldMixin and EditTagMixin methods
|
||||
|
@ -489,6 +486,7 @@ class LibrarySection(PlexObject):
|
|||
)
|
||||
return value
|
||||
|
||||
@cached_data_property
|
||||
def _getTotalDurationStorage(self):
|
||||
""" Queries the Plex server for the total library duration and storage and caches the values. """
|
||||
data = self._server.query('/media/providers?includeStorage=1')
|
||||
|
@ -499,8 +497,10 @@ class LibrarySection(PlexObject):
|
|||
)
|
||||
directory = next(iter(data.findall(xpath)), None)
|
||||
if directory:
|
||||
self._totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
|
||||
self._totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
|
||||
totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
|
||||
totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
|
||||
return totalDuration, totalStorage
|
||||
return None, None
|
||||
|
||||
def totalViewSize(self, libtype=None, includeCollections=True):
|
||||
""" Returns the total number of items in the library for a specified libtype.
|
||||
|
@ -531,18 +531,20 @@ class LibrarySection(PlexObject):
|
|||
def delete(self):
|
||||
""" Delete a library section. """
|
||||
try:
|
||||
return self._server.query(f'/library/sections/{self.key}', method=self._server._session.delete)
|
||||
data = self._server.query(f'/library/sections/{self.key}', method=self._server._session.delete)
|
||||
self._server.library._invalidateCachedProperties()
|
||||
return data
|
||||
except BadRequest: # pragma: no cover
|
||||
msg = f'Failed to delete library {self.key}'
|
||||
msg += 'You may need to allow this permission in your Plex settings.'
|
||||
log.error(msg)
|
||||
raise
|
||||
|
||||
def reload(self):
|
||||
def _reload(self, **kwargs):
|
||||
""" Reload the data for the library section. """
|
||||
self._server.library._loadSections()
|
||||
newLibrary = self._server.library.sectionByID(self.key)
|
||||
self.__dict__.update(newLibrary.__dict__)
|
||||
key = self._initpath
|
||||
data = self._server.query(key)
|
||||
self._findAndLoadElem(data, key=str(self.key))
|
||||
return self
|
||||
|
||||
def edit(self, agent=None, **kwargs):
|
||||
|
@ -871,6 +873,7 @@ class LibrarySection(PlexObject):
|
|||
self._server.query(key, method=self._server._session.delete)
|
||||
return self
|
||||
|
||||
@cached_data_property
|
||||
def _loadFilters(self):
|
||||
""" Retrieves and caches the list of :class:`~plexapi.library.FilteringType` and
|
||||
list of :class:`~plexapi.library.FilteringFieldType` for this library section.
|
||||
|
@ -880,23 +883,23 @@ class LibrarySection(PlexObject):
|
|||
|
||||
key = _key.format(key=self.key, filter='all')
|
||||
data = self._server.query(key)
|
||||
self._filterTypes = self.findItems(data, FilteringType, rtag='Meta')
|
||||
self._fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
|
||||
filterTypes = self.findItems(data, FilteringType, rtag='Meta')
|
||||
fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
|
||||
|
||||
if self.TYPE != 'photo': # No collections for photo library
|
||||
key = _key.format(key=self.key, filter='collections')
|
||||
data = self._server.query(key)
|
||||
self._filterTypes.extend(self.findItems(data, FilteringType, rtag='Meta'))
|
||||
filterTypes.extend(self.findItems(data, FilteringType, rtag='Meta'))
|
||||
|
||||
# Manually add guid field type, only allowing "is" operator
|
||||
guidFieldType = '<FieldType type="guid"><Operator key="=" title="is"/></FieldType>'
|
||||
self._fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
|
||||
fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
|
||||
|
||||
return filterTypes, fieldTypes
|
||||
|
||||
def filterTypes(self):
|
||||
""" Returns a list of available :class:`~plexapi.library.FilteringType` for this library section. """
|
||||
if self._filterTypes is None:
|
||||
self._loadFilters()
|
||||
return self._filterTypes
|
||||
return self._loadFilters[0]
|
||||
|
||||
def getFilterType(self, libtype=None):
|
||||
""" Returns a :class:`~plexapi.library.FilteringType` for a specified libtype.
|
||||
|
@ -918,9 +921,7 @@ class LibrarySection(PlexObject):
|
|||
|
||||
def fieldTypes(self):
|
||||
""" Returns a list of available :class:`~plexapi.library.FilteringFieldType` for this library section. """
|
||||
if self._fieldTypes is None:
|
||||
self._loadFilters()
|
||||
return self._fieldTypes
|
||||
return self._loadFilters[1]
|
||||
|
||||
def getFieldType(self, fieldType):
|
||||
""" Returns a :class:`~plexapi.library.FilteringFieldType` for a specified fieldType.
|
||||
|
@ -1969,7 +1970,7 @@ class MusicSection(LibrarySection, ArtistEditMixins, AlbumEditMixins, TrackEditM
|
|||
|
||||
def stations(self):
|
||||
""" Returns a list of :class:`~plexapi.playlist.Playlist` stations in this section. """
|
||||
return next((hub.items for hub in self.hubs() if hub.context == 'hub.music.stations'), None)
|
||||
return next((hub._partialItems for hub in self.hubs() if hub.context == 'hub.music.stations'), None)
|
||||
|
||||
def searchArtists(self, **kwargs):
|
||||
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||
|
@ -2165,7 +2166,6 @@ class LibraryTimeline(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.art = data.attrib.get('art')
|
||||
|
@ -2194,7 +2194,6 @@ class Location(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.path = data.attrib.get('path')
|
||||
|
||||
|
@ -2208,9 +2207,10 @@ class Hub(PlexObject):
|
|||
context (str): The context of the hub.
|
||||
hubKey (str): API URL for these specific hub items.
|
||||
hubIdentifier (str): The identifier of the hub.
|
||||
items (list): List of items in the hub.
|
||||
items (list): List of items in the hub (automatically loads all items if more is True).
|
||||
key (str): API URL for the hub.
|
||||
more (bool): True if there are more items to load (call reload() to fetch all items).
|
||||
random (bool): True if the items in the hub are randomized.
|
||||
more (bool): True if there are more items to load (call items to fetch all items).
|
||||
size (int): The number of items in the hub.
|
||||
style (str): The style of the hub.
|
||||
title (str): The title of the hub.
|
||||
|
@ -2220,36 +2220,57 @@ class Hub(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.context = data.attrib.get('context')
|
||||
self.hubKey = data.attrib.get('hubKey')
|
||||
self.hubIdentifier = data.attrib.get('hubIdentifier')
|
||||
self.items = self.findItems(data)
|
||||
self.key = data.attrib.get('key')
|
||||
self.more = utils.cast(bool, data.attrib.get('more'))
|
||||
self.random = utils.cast(bool, data.attrib.get('random', '0'))
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.style = data.attrib.get('style')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self._section = None # cache for self.section
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def reload(self):
|
||||
""" Reloads the hub to fetch all items in the hub. """
|
||||
if self.more and self.key:
|
||||
self.items = self.fetchItems(self.key)
|
||||
@cached_data_property
|
||||
def _partialItems(self):
|
||||
""" Cache for partial items. """
|
||||
return self.findItems(self._data)
|
||||
|
||||
@cached_data_property
|
||||
def _items(self):
|
||||
""" Cache for items. """
|
||||
if self.more and self.key: # If there are more items to load, fetch them
|
||||
items = self.fetchItems(self.key)
|
||||
self.more = False
|
||||
self.size = len(self.items)
|
||||
self.size = len(items)
|
||||
return items
|
||||
# Otherwise, all the data is in the initial _data XML response
|
||||
return self._partialItems
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of all items in the hub. """
|
||||
return self._items
|
||||
|
||||
@cached_data_property
|
||||
def _section(self):
|
||||
""" Cache for section. """
|
||||
return self._server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this hub belongs to.
|
||||
"""
|
||||
if self._section is None:
|
||||
self._section = self._server.library.sectionByID(self.librarySectionID)
|
||||
return self._section
|
||||
|
||||
def _reload(self, **kwargs):
|
||||
""" Reload the data for the hub. """
|
||||
key = self._initpath
|
||||
data = self._server.query(key)
|
||||
self._findAndLoadElem(data, hubIdentifier=self.hubIdentifier)
|
||||
return self
|
||||
|
||||
|
||||
class LibraryMediaTag(PlexObject):
|
||||
""" Base class of library media tags.
|
||||
|
@ -2279,7 +2300,6 @@ class LibraryMediaTag(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.count = utils.cast(int, data.attrib.get('count'))
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
|
@ -2668,22 +2688,25 @@ class FilteringType(PlexObject):
|
|||
return f"<{':'.join([p for p in [self.__class__.__name__, _type] if p])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.active = utils.cast(bool, data.attrib.get('active', '0'))
|
||||
self.fields = self.findItems(data, FilteringField)
|
||||
self.filters = self.findItems(data, FilteringFilter)
|
||||
self.key = data.attrib.get('key')
|
||||
self.sorts = self.findItems(data, FilteringSort)
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
||||
self._librarySectionID = self._parent().key
|
||||
|
||||
# Add additional manual filters, sorts, and fields which are available
|
||||
# but not exposed on the Plex server
|
||||
self.filters += self._manualFilters()
|
||||
self.sorts += self._manualSorts()
|
||||
self.fields += self._manualFields()
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, FilteringField) + self._manualFields()
|
||||
|
||||
@cached_data_property
|
||||
def filters(self):
|
||||
return self.findItems(self._data, FilteringFilter) + self._manualFilters()
|
||||
|
||||
@cached_data_property
|
||||
def sorts(self):
|
||||
return self.findItems(self._data, FilteringSort) + self._manualSorts()
|
||||
|
||||
def _manualFilters(self):
|
||||
""" Manually add additional filters which are available
|
||||
|
@ -2863,7 +2886,7 @@ class FilteringFilter(PlexObject):
|
|||
TAG = 'Filter'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.filterType = data.attrib.get('filterType')
|
||||
self.key = data.attrib.get('key')
|
||||
|
@ -2889,7 +2912,6 @@ class FilteringSort(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.active = utils.cast(bool, data.attrib.get('active', '0'))
|
||||
self.activeDirection = data.attrib.get('activeDirection')
|
||||
self.default = data.attrib.get('default')
|
||||
|
@ -2914,7 +2936,6 @@ class FilteringField(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
@ -2937,9 +2958,11 @@ class FilteringFieldType(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.type = data.attrib.get('type')
|
||||
self.operators = self.findItems(data, FilteringOperator)
|
||||
|
||||
@cached_data_property
|
||||
def operators(self):
|
||||
return self.findItems(self._data, FilteringOperator)
|
||||
|
||||
|
||||
class FilteringOperator(PlexObject):
|
||||
|
@ -2976,7 +2999,6 @@ class FilterChoice(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.fastKey = data.attrib.get('fastKey')
|
||||
self.key = data.attrib.get('key')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
@ -3006,7 +3028,6 @@ class ManagedHub(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.deletable = utils.cast(bool, data.attrib.get('deletable', True))
|
||||
self.homeVisibility = data.attrib.get('homeVisibility', 'none')
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
|
@ -3020,11 +3041,11 @@ class ManagedHub(PlexObject):
|
|||
parent = self._parent()
|
||||
self.librarySectionID = parent.key if isinstance(parent, LibrarySection) else parent.librarySectionID
|
||||
|
||||
def reload(self):
|
||||
def _reload(self, **kwargs):
|
||||
""" Reload the data for this managed hub. """
|
||||
key = f'/hubs/sections/{self.librarySectionID}/manage'
|
||||
hub = self.fetchItem(key, self.__class__, identifier=self.identifier)
|
||||
self.__dict__.update(hub.__dict__)
|
||||
data = self._server.query(key)
|
||||
self._findAndLoadElem(data, identifier=self.identifier)
|
||||
return self
|
||||
|
||||
def move(self, after=None):
|
||||
|
@ -3170,7 +3191,6 @@ class FirstCharacter(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.key = data.attrib.get('key')
|
||||
self.size = data.attrib.get('size')
|
||||
self.title = data.attrib.get('title')
|
||||
|
@ -3191,6 +3211,7 @@ class Path(PlexObject):
|
|||
TAG = 'Path'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.network = utils.cast(bool, data.attrib.get('network'))
|
||||
|
@ -3220,6 +3241,7 @@ class File(PlexObject):
|
|||
TAG = 'File'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.key = data.attrib.get('key')
|
||||
self.path = data.attrib.get('path')
|
||||
self.title = data.attrib.get('title')
|
||||
|
@ -3268,41 +3290,83 @@ class Common(PlexObject):
|
|||
TAG = 'Common'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.editionTitle = data.attrib.get('editionTitle')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.mixedFields = data.attrib.get('mixedFields').split(',')
|
||||
self.moods = self.findItems(data, media.Mood)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'))
|
||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.styles = self.findItems(data, media.Style)
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.tags = self.findItems(data, media.Tag)
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort')
|
||||
self.type = data.attrib.get('type')
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def countries(self):
|
||||
return self.findItems(self._data, media.Country)
|
||||
|
||||
@cached_data_property
|
||||
def directors(self):
|
||||
return self.findItems(self._data, media.Director)
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def moods(self):
|
||||
return self.findItems(self._data, media.Mood)
|
||||
|
||||
@cached_data_property
|
||||
def producers(self):
|
||||
return self.findItems(self._data, media.Producer)
|
||||
|
||||
@cached_data_property
|
||||
def ratings(self):
|
||||
return self.findItems(self._data, media.Rating)
|
||||
|
||||
@cached_data_property
|
||||
def roles(self):
|
||||
return self.findItems(self._data, media.Role)
|
||||
|
||||
@cached_data_property
|
||||
def styles(self):
|
||||
return self.findItems(self._data, media.Style)
|
||||
|
||||
@cached_data_property
|
||||
def tags(self):
|
||||
return self.findItems(self._data, media.Tag)
|
||||
|
||||
@cached_data_property
|
||||
def writers(self):
|
||||
return self.findItems(self._data, media.Writer)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (
|
||||
self.__class__.__name__,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import xml
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from plexapi import log, settings, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.base import PlexObject, cached_data_property
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
@ -51,7 +51,6 @@ class Media(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.aspectRatio = utils.cast(float, data.attrib.get('aspectRatio'))
|
||||
self.audioChannels = utils.cast(int, data.attrib.get('audioChannels'))
|
||||
self.audioCodec = data.attrib.get('audioCodec')
|
||||
|
@ -64,7 +63,6 @@ class Media(PlexObject):
|
|||
self.has64bitOffsets = utils.cast(bool, data.attrib.get('has64bitOffsets'))
|
||||
self.hasVoiceActivity = utils.cast(bool, data.attrib.get('hasVoiceActivity', '0'))
|
||||
self.optimizedForStreaming = utils.cast(bool, data.attrib.get('optimizedForStreaming'))
|
||||
self.parts = self.findItems(data, MediaPart)
|
||||
self.proxyType = utils.cast(int, data.attrib.get('proxyType'))
|
||||
self.selected = utils.cast(bool, data.attrib.get('selected'))
|
||||
self.target = data.attrib.get('target')
|
||||
|
@ -87,6 +85,10 @@ class Media(PlexObject):
|
|||
parent = self._parent()
|
||||
self._parentKey = parent.key
|
||||
|
||||
@cached_data_property
|
||||
def parts(self):
|
||||
return self.findItems(self._data, MediaPart)
|
||||
|
||||
@property
|
||||
def isOptimizedVersion(self):
|
||||
""" Returns True if the media is a Plex optimized version. """
|
||||
|
@ -138,7 +140,6 @@ class MediaPart(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.accessible = utils.cast(bool, data.attrib.get('accessible'))
|
||||
self.audioProfile = data.attrib.get('audioProfile')
|
||||
self.container = data.attrib.get('container')
|
||||
|
@ -268,7 +269,6 @@ class MediaPartStream(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.bitrate = utils.cast(int, data.attrib.get('bitrate'))
|
||||
self.codec = data.attrib.get('codec')
|
||||
self.decision = data.attrib.get('decision')
|
||||
|
@ -386,6 +386,7 @@ class AudioStream(MediaPartStream):
|
|||
profile (str): The profile of the audio stream.
|
||||
samplingRate (int): The sampling rate of the audio stream (ex: xxx)
|
||||
streamIdentifier (int): The stream identifier of the audio stream.
|
||||
visualImpaired (bool): True if this is a visually impaired (AD) audio stream.
|
||||
|
||||
Track_only_attributes: The following attributes are only available for tracks.
|
||||
|
||||
|
@ -413,6 +414,7 @@ class AudioStream(MediaPartStream):
|
|||
self.profile = data.attrib.get('profile')
|
||||
self.samplingRate = utils.cast(int, data.attrib.get('samplingRate'))
|
||||
self.streamIdentifier = utils.cast(int, data.attrib.get('streamIdentifier'))
|
||||
self.visualImpaired = utils.cast(bool, data.attrib.get('visualImpaired', '0'))
|
||||
|
||||
# Track only attributes
|
||||
self.albumGain = utils.cast(float, data.attrib.get('albumGain'))
|
||||
|
@ -523,6 +525,7 @@ class Session(PlexObject):
|
|||
TAG = 'Session'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.id = data.attrib.get('id')
|
||||
self.bandwidth = utils.cast(int, data.attrib.get('bandwidth'))
|
||||
self.location = data.attrib.get('location')
|
||||
|
@ -569,7 +572,6 @@ class TranscodeSession(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.audioChannels = utils.cast(int, data.attrib.get('audioChannels'))
|
||||
self.audioCodec = data.attrib.get('audioCodec')
|
||||
self.audioDecision = data.attrib.get('audioDecision')
|
||||
|
@ -610,7 +612,7 @@ class TranscodeJob(PlexObject):
|
|||
TAG = 'TranscodeJob'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.generatorID = data.attrib.get('generatorID')
|
||||
self.key = data.attrib.get('key')
|
||||
self.progress = data.attrib.get('progress')
|
||||
|
@ -629,7 +631,7 @@ class Optimized(PlexObject):
|
|||
TAG = 'Item'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.id = data.attrib.get('id')
|
||||
self.composite = data.attrib.get('composite')
|
||||
self.title = data.attrib.get('title')
|
||||
|
@ -667,7 +669,7 @@ class Conversion(PlexObject):
|
|||
TAG = 'Video'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.addedAt = data.attrib.get('addedAt')
|
||||
self.art = data.attrib.get('art')
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
|
@ -743,7 +745,6 @@ class MediaTag(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.key = data.attrib.get('key')
|
||||
|
@ -954,7 +955,6 @@ class Guid(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = data.attrib.get('id')
|
||||
|
||||
|
||||
|
@ -972,7 +972,6 @@ class Image(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.alt = data.attrib.get('alt')
|
||||
self.type = data.attrib.get('type')
|
||||
self.url = data.attrib.get('url')
|
||||
|
@ -994,7 +993,6 @@ class Rating(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.image = data.attrib.get('image')
|
||||
self.type = data.attrib.get('type')
|
||||
self.value = utils.cast(float, data.attrib.get('value'))
|
||||
|
@ -1017,7 +1015,7 @@ class Review(PlexObject):
|
|||
TAG = 'Review'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = utils.cast(int, data.attrib.get('id', 0))
|
||||
self.image = data.attrib.get('image')
|
||||
|
@ -1042,7 +1040,6 @@ class UltraBlurColors(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.bottomLeft = data.attrib.get('bottomLeft')
|
||||
self.bottomRight = data.attrib.get('bottomRight')
|
||||
self.topLeft = data.attrib.get('topLeft')
|
||||
|
@ -1063,7 +1060,7 @@ class BaseResource(PlexObject):
|
|||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.key = data.attrib.get('key')
|
||||
self.provider = data.attrib.get('provider')
|
||||
self.ratingKey = data.attrib.get('ratingKey')
|
||||
|
@ -1075,7 +1072,7 @@ class BaseResource(PlexObject):
|
|||
data = f'{key}?url={quote_plus(self.ratingKey)}'
|
||||
try:
|
||||
self._server.query(data, method=self._server._session.put)
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
except ElementTree.ParseError:
|
||||
pass
|
||||
|
||||
@property
|
||||
|
@ -1138,7 +1135,7 @@ class Chapter(PlexObject):
|
|||
return f"<{':'.join([self.__class__.__name__, name, offsets])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = utils.cast(int, data.attrib.get('id', 0))
|
||||
|
@ -1172,7 +1169,7 @@ class Marker(PlexObject):
|
|||
return f"<{':'.join([self.__class__.__name__, name, offsets])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
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'))
|
||||
|
@ -1206,7 +1203,7 @@ class Field(PlexObject):
|
|||
TAG = 'Field'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.locked = utils.cast(bool, data.attrib.get('locked'))
|
||||
self.name = data.attrib.get('name')
|
||||
|
||||
|
@ -1226,7 +1223,7 @@ class SearchResult(PlexObject):
|
|||
return f"<{':'.join([p for p in [self.__class__.__name__, name, score] if p])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.lifespanEnded = data.attrib.get('lifespanEnded')
|
||||
self.name = data.attrib.get('name')
|
||||
|
@ -1248,7 +1245,7 @@ class Agent(PlexObject):
|
|||
return f"<{':'.join([p for p in [self.__class__.__name__, uid] if p])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.hasAttribution = data.attrib.get('hasAttribution')
|
||||
self.hasPrefs = data.attrib.get('hasPrefs')
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
|
@ -1256,12 +1253,17 @@ class Agent(PlexObject):
|
|||
self.primary = data.attrib.get('primary')
|
||||
self.shortIdentifier = self.identifier.rsplit('.', 1)[1]
|
||||
|
||||
@cached_data_property
|
||||
def languageCodes(self):
|
||||
if 'mediaType' in self._initpath:
|
||||
self.languageCodes = self.listAttrs(data, 'code', etag='Language')
|
||||
self.mediaTypes = []
|
||||
else:
|
||||
self.languageCodes = []
|
||||
self.mediaTypes = self.findItems(data, cls=AgentMediaType)
|
||||
return self.listAttrs(self._data, 'code', etag='Language')
|
||||
return []
|
||||
|
||||
@cached_data_property
|
||||
def mediaTypes(self):
|
||||
if 'mediaType' not in self._initpath:
|
||||
return self.findItems(self._data, cls=AgentMediaType)
|
||||
return []
|
||||
|
||||
@property
|
||||
@deprecated('use "languageCodes" instead')
|
||||
|
@ -1291,10 +1293,14 @@ class AgentMediaType(Agent):
|
|||
return f"<{':'.join([p for p in [self.__class__.__name__, uid] if p])}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self.languageCodes = self.listAttrs(data, 'code', etag='Language')
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.mediaType = utils.cast(int, data.attrib.get('mediaType'))
|
||||
self.name = data.attrib.get('name')
|
||||
|
||||
@cached_data_property
|
||||
def languageCodes(self):
|
||||
return self.listAttrs(self._data, 'code', etag='Language')
|
||||
|
||||
@property
|
||||
@deprecated('use "languageCodes" instead')
|
||||
def languageCode(self):
|
||||
|
@ -1325,7 +1331,7 @@ class Availability(PlexObject):
|
|||
return f'<{self.__class__.__name__}:{self.platform}:{self.offerType}>'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.country = data.attrib.get('country')
|
||||
self.offerType = data.attrib.get('offerType')
|
||||
self.platform = data.attrib.get('platform')
|
||||
|
|
|
@ -4,13 +4,12 @@ import html
|
|||
import threading
|
||||
import time
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
|
||||
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_ENABLE_FAST_CONNECT, X_PLEX_IDENTIFIER,
|
||||
log, logfilter, utils)
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.base import PlexObject, cached_data_property
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized, TwoFactorRequired
|
||||
from plexapi.library import LibrarySection
|
||||
|
@ -144,7 +143,6 @@ class MyPlexAccount(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self._token = logfilter.add_secret(data.attrib.get('authToken'))
|
||||
self._webhooks = []
|
||||
|
||||
|
@ -185,7 +183,6 @@ class MyPlexAccount(PlexObject):
|
|||
subscription = data.find('subscription')
|
||||
self.subscriptionActive = utils.cast(bool, subscription.attrib.get('active'))
|
||||
self.subscriptionDescription = data.attrib.get('subscriptionDescription')
|
||||
self.subscriptionFeatures = self.listAttrs(subscription, 'id', rtag='features', etag='feature')
|
||||
self.subscriptionPaymentService = subscription.attrib.get('paymentService')
|
||||
self.subscriptionPlan = subscription.attrib.get('plan')
|
||||
self.subscriptionStatus = subscription.attrib.get('status')
|
||||
|
@ -201,21 +198,31 @@ class MyPlexAccount(PlexObject):
|
|||
self.profileDefaultSubtitleAccessibility = utils.cast(int, profile.attrib.get('defaultSubtitleAccessibility'))
|
||||
self.profileDefaultSubtitleForces = utils.cast(int, profile.attrib.get('defaultSubtitleForces'))
|
||||
|
||||
self.entitlements = self.listAttrs(data, 'id', rtag='entitlements', etag='entitlement')
|
||||
self.roles = self.listAttrs(data, 'id', rtag='roles', etag='role')
|
||||
|
||||
# TODO: Fetch missing MyPlexAccount services
|
||||
self.services = None
|
||||
|
||||
@cached_data_property
|
||||
def subscriptionFeatures(self):
|
||||
subscription = self._data.find('subscription')
|
||||
return self.listAttrs(subscription, 'id', rtag='features', etag='feature')
|
||||
|
||||
@cached_data_property
|
||||
def entitlements(self):
|
||||
return self.listAttrs(self._data, 'id', rtag='entitlements', etag='entitlement')
|
||||
|
||||
@cached_data_property
|
||||
def roles(self):
|
||||
return self.listAttrs(self._data, 'id', rtag='roles', etag='role')
|
||||
|
||||
@property
|
||||
def authenticationToken(self):
|
||||
""" Returns the authentication token for the account. Alias for ``authToken``. """
|
||||
return self.authToken
|
||||
|
||||
def _reload(self, key=None, **kwargs):
|
||||
def _reload(self, **kwargs):
|
||||
""" Perform the actual reload. """
|
||||
data = self.query(self.key)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
||||
def _headers(self, **kwargs):
|
||||
|
@ -250,8 +257,7 @@ class MyPlexAccount(PlexObject):
|
|||
return response.json()
|
||||
elif 'text/plain' in response.headers.get('Content-Type', ''):
|
||||
return response.text.strip()
|
||||
data = utils.cleanXMLString(response.text).encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
return utils.parseXMLString(response.text)
|
||||
|
||||
def ping(self):
|
||||
""" Ping the Plex.tv API.
|
||||
|
@ -1206,7 +1212,6 @@ class MyPlexUser(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.friend = self._initpath == self.key
|
||||
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
|
||||
self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels'))
|
||||
|
@ -1225,10 +1230,13 @@ class MyPlexUser(PlexObject):
|
|||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title', '')
|
||||
self.username = data.attrib.get('username', '')
|
||||
self.servers = self.findItems(data, MyPlexServerShare)
|
||||
for server in self.servers:
|
||||
server.accountID = self.id
|
||||
|
||||
@cached_data_property
|
||||
def servers(self):
|
||||
return self.findItems(self._data, MyPlexServerShare)
|
||||
|
||||
def get_token(self, machineIdentifier):
|
||||
try:
|
||||
for item in self._server.query(self._server.FRIENDINVITE.format(machineId=machineIdentifier)):
|
||||
|
@ -1283,7 +1291,6 @@ class MyPlexInvite(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.email = data.attrib.get('email')
|
||||
self.friend = utils.cast(bool, data.attrib.get('friend'))
|
||||
|
@ -1291,12 +1298,15 @@ class MyPlexInvite(PlexObject):
|
|||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.server = utils.cast(bool, data.attrib.get('server'))
|
||||
self.servers = self.findItems(data, MyPlexServerShare)
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.username = data.attrib.get('username', '')
|
||||
for server in self.servers:
|
||||
server.accountID = self.id
|
||||
|
||||
@cached_data_property
|
||||
def servers(self):
|
||||
return self.findItems(self._data, MyPlexServerShare)
|
||||
|
||||
|
||||
class Section(PlexObject):
|
||||
""" This refers to a shared section. The raw xml for the data presented here
|
||||
|
@ -1314,7 +1324,7 @@ class Section(PlexObject):
|
|||
TAG = 'Section'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.key = utils.cast(int, data.attrib.get('key'))
|
||||
self.shared = utils.cast(bool, data.attrib.get('shared', '0'))
|
||||
|
@ -1353,7 +1363,6 @@ class MyPlexServerShare(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.accountID = utils.cast(int, data.attrib.get('accountID'))
|
||||
self.serverId = utils.cast(int, data.attrib.get('serverId'))
|
||||
|
@ -1437,10 +1446,9 @@ class MyPlexResource(PlexObject):
|
|||
DEFAULT_SCHEME_ORDER = ['https', 'http']
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.accessToken = logfilter.add_secret(data.attrib.get('accessToken'))
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.connections = self.findItems(data, ResourceConnection, rtag='connections')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'), "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.device = data.attrib.get('device')
|
||||
self.dnsRebindingProtection = utils.cast(bool, data.attrib.get('dnsRebindingProtection'))
|
||||
|
@ -1462,6 +1470,10 @@ class MyPlexResource(PlexObject):
|
|||
self.sourceTitle = data.attrib.get('sourceTitle')
|
||||
self.synced = utils.cast(bool, data.attrib.get('synced'))
|
||||
|
||||
@cached_data_property
|
||||
def connections(self):
|
||||
return self.findItems(self._data, ResourceConnection, rtag='connections')
|
||||
|
||||
def preferred_connections(
|
||||
self,
|
||||
ssl=None,
|
||||
|
@ -1555,7 +1567,7 @@ class ResourceConnection(PlexObject):
|
|||
TAG = 'connection'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.address = data.attrib.get('address')
|
||||
self.ipv6 = utils.cast(bool, data.attrib.get('IPv6'))
|
||||
self.local = utils.cast(bool, data.attrib.get('local'))
|
||||
|
@ -1598,7 +1610,7 @@ class MyPlexDevice(PlexObject):
|
|||
key = 'https://plex.tv/devices.xml'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.name = data.attrib.get('name')
|
||||
self.publicAddress = data.attrib.get('publicAddress')
|
||||
self.product = data.attrib.get('product')
|
||||
|
@ -1617,7 +1629,10 @@ class MyPlexDevice(PlexObject):
|
|||
self.screenDensity = data.attrib.get('screenDensity')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.lastSeenAt = utils.toDatetime(data.attrib.get('lastSeenAt'))
|
||||
self.connections = self.listAttrs(data, 'uri', etag='Connection')
|
||||
|
||||
@cached_data_property
|
||||
def connections(self):
|
||||
return self.listAttrs(self._data, 'uri', etag='Connection')
|
||||
|
||||
def connect(self, timeout=None):
|
||||
""" Returns a new :class:`~plexapi.client.PlexClient` or :class:`~plexapi.server.PlexServer`
|
||||
|
@ -1879,8 +1894,7 @@ class MyPlexPinLogin:
|
|||
codename = codes.get(response.status_code)[0]
|
||||
errtext = response.text.replace('\n', ' ')
|
||||
raise BadRequest(f'({response.status_code}) {codename} {response.url}; {errtext}')
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
return utils.parseXMLString(response.text)
|
||||
|
||||
|
||||
def _connect(cls, url, token, session, timeout, results, i, job_is_done_event=None):
|
||||
|
@ -1939,6 +1953,7 @@ class AccountOptOut(PlexObject):
|
|||
CHOICES = {'opt_in', 'opt_out', 'opt_out_managed'}
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.key = data.attrib.get('key')
|
||||
self.value = data.attrib.get('value')
|
||||
|
||||
|
@ -1997,6 +2012,7 @@ class UserState(PlexObject):
|
|||
return f'<{self.__class__.__name__}:{self.ratingKey}>'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.ratingKey = data.attrib.get('ratingKey')
|
||||
self.type = data.attrib.get('type')
|
||||
|
@ -2026,7 +2042,7 @@ class GeoLocation(PlexObject):
|
|||
TAG = 'location'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.city = data.attrib.get('city')
|
||||
self.code = data.attrib.get('code')
|
||||
self.continentCode = data.attrib.get('continent_code')
|
||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils, video
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexSession
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexSession, cached_data_property
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import (
|
||||
RatingMixin,
|
||||
|
@ -56,9 +56,7 @@ class Photoalbum(
|
|||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.composite = data.attrib.get('composite')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.images = self.findItems(data, media.Image)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
|
||||
|
@ -75,6 +73,14 @@ class Photoalbum(
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def images(self):
|
||||
return self.findItems(self._data, media.Image)
|
||||
|
||||
def album(self, title):
|
||||
""" Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title.
|
||||
|
||||
|
@ -205,9 +211,7 @@ class Photo(
|
|||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.createdAtAccuracy = data.attrib.get('createdAtAccuracy')
|
||||
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.images = self.findItems(data, media.Image)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
|
||||
|
@ -215,7 +219,6 @@ class Photo(
|
|||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'photo'
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
|
@ -226,7 +229,6 @@ class Photo(
|
|||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.sourceURI = data.attrib.get('source') # remote playlist item
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.tags = self.findItems(data, media.Tag)
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
|
@ -235,6 +237,22 @@ class Photo(
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def images(self):
|
||||
return self.findItems(self._data, media.Image)
|
||||
|
||||
@cached_data_property
|
||||
def media(self):
|
||||
return self.findItems(self._data, media.Media)
|
||||
|
||||
@cached_data_property
|
||||
def tags(self):
|
||||
return self.findItems(self._data, media.Tag)
|
||||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
if self.parentTitle:
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
from urllib.parse import quote_plus, unquote
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.base import Playable, PlexPartialObject, cached_data_property
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
from plexapi.library import LibrarySection, MusicSection
|
||||
from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin, PlaylistEditMixins
|
||||
|
@ -60,7 +60,6 @@ class Playlist(
|
|||
self.content = data.attrib.get('content')
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.durationInSeconds = utils.cast(int, data.attrib.get('durationInSeconds'))
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.icon = data.attrib.get('icon')
|
||||
self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50
|
||||
|
@ -77,9 +76,10 @@ class Playlist(
|
|||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self._items = None # cache for self.items
|
||||
self._section = None # cache for self.section
|
||||
self._filters = None # cache for self.filters
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
def __len__(self): # pragma: no cover
|
||||
return len(self.items())
|
||||
|
@ -133,15 +133,36 @@ class Playlist(
|
|||
return _item.playlistItemID
|
||||
raise NotFound(f'Item with title "{item.title}" not found in the playlist')
|
||||
|
||||
@cached_data_property
|
||||
def _filters(self):
|
||||
""" Cache for filters. """
|
||||
return self._parseFilters(self.content)
|
||||
|
||||
def filters(self):
|
||||
""" Returns the search filter dict for smart playlist.
|
||||
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
|
||||
to get the list of items.
|
||||
"""
|
||||
if self.smart and self._filters is None:
|
||||
self._filters = self._parseFilters(self.content)
|
||||
return self._filters
|
||||
|
||||
@cached_data_property
|
||||
def _section(self):
|
||||
""" Cache for section. """
|
||||
if not self.smart:
|
||||
raise BadRequest('Regular playlists are not associated with a library.')
|
||||
|
||||
# Try to parse the library section from the content URI string
|
||||
match = re.search(r'/library/sections/(\d+)/all', unquote(self.content or ''))
|
||||
if match:
|
||||
sectionKey = int(match.group(1))
|
||||
return self._server.library.sectionByID(sectionKey)
|
||||
|
||||
# Try to get the library section from the first item in the playlist
|
||||
if self.items():
|
||||
return self.items()[0].section()
|
||||
|
||||
raise Unsupported('Unable to determine the library section')
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this smart playlist belongs to.
|
||||
|
||||
|
@ -149,24 +170,6 @@ class Playlist(
|
|||
:class:`plexapi.exceptions.BadRequest`: When trying to get the section for a regular playlist.
|
||||
:class:`plexapi.exceptions.Unsupported`: When unable to determine the library section.
|
||||
"""
|
||||
if not self.smart:
|
||||
raise BadRequest('Regular playlists are not associated with a library.')
|
||||
|
||||
if self._section is None:
|
||||
# Try to parse the library section from the content URI string
|
||||
match = re.search(r'/library/sections/(\d+)/all', unquote(self.content or ''))
|
||||
if match:
|
||||
sectionKey = int(match.group(1))
|
||||
self._section = self._server.library.sectionByID(sectionKey)
|
||||
return self._section
|
||||
|
||||
# Try to get the library section from the first item in the playlist
|
||||
if self.items():
|
||||
self._section = self.items()[0].section()
|
||||
return self._section
|
||||
|
||||
raise Unsupported('Unable to determine the library section')
|
||||
|
||||
return self._section
|
||||
|
||||
def item(self, title):
|
||||
|
@ -183,28 +186,32 @@ class Playlist(
|
|||
return item
|
||||
raise NotFound(f'Item with title "{title}" not found in the playlist')
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of all items in the playlist. """
|
||||
@cached_data_property
|
||||
def _items(self):
|
||||
""" Cache for items. """
|
||||
if self.radio:
|
||||
return []
|
||||
if self._items is None:
|
||||
key = f'{self.key}/items'
|
||||
items = self.fetchItems(key)
|
||||
|
||||
# Cache server connections to avoid reconnecting for each item
|
||||
_servers = {}
|
||||
for item in items:
|
||||
if item.sourceURI:
|
||||
serverID = item.sourceURI.split('/')[2]
|
||||
if serverID not in _servers:
|
||||
try:
|
||||
_servers[serverID] = self._server.myPlexAccount().resource(serverID).connect()
|
||||
except NotFound:
|
||||
# Override the server connection with None if the server is not found
|
||||
_servers[serverID] = None
|
||||
item._server = _servers[serverID]
|
||||
key = f'{self.key}/items'
|
||||
items = self.fetchItems(key)
|
||||
|
||||
self._items = items
|
||||
# Cache server connections to avoid reconnecting for each item
|
||||
_servers = {}
|
||||
for item in items:
|
||||
if item.sourceURI:
|
||||
serverID = item.sourceURI.split('/')[2]
|
||||
if serverID not in _servers:
|
||||
try:
|
||||
_servers[serverID] = self._server.myPlexAccount().resource(serverID).connect()
|
||||
except NotFound:
|
||||
# Override the server connection with None if the server is not found
|
||||
_servers[serverID] = None
|
||||
item._server = _servers[serverID]
|
||||
|
||||
return items
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of all items in the playlist. """
|
||||
return self._items
|
||||
|
||||
def get(self, title):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.base import PlexObject, cached_data_property
|
||||
from plexapi.exceptions import BadRequest
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ class PlayQueue(PlexObject):
|
|||
TYPE = "playqueue"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.identifier = data.attrib.get("identifier")
|
||||
self.mediaTagPrefix = data.attrib.get("mediaTagPrefix")
|
||||
self.mediaTagVersion = utils.cast(int, data.attrib.get("mediaTagVersion"))
|
||||
|
@ -62,9 +62,12 @@ class PlayQueue(PlexObject):
|
|||
)
|
||||
self.playQueueVersion = utils.cast(int, data.attrib.get("playQueueVersion"))
|
||||
self.size = utils.cast(int, data.attrib.get("size", 0))
|
||||
self.items = self.findItems(data)
|
||||
self.selectedItem = self[self.playQueueSelectedItemOffset]
|
||||
|
||||
@cached_data_property
|
||||
def items(self):
|
||||
return self.findItems(self._data)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not self.items:
|
||||
return None
|
||||
|
@ -254,7 +257,7 @@ class PlayQueue(PlexObject):
|
|||
|
||||
path = f"/playQueues/{self.playQueueID}{utils.joinArgs(args)}"
|
||||
data = self._server.query(path, method=self._server._session.put)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
||||
def moveItem(self, item, after=None, refresh=True):
|
||||
|
@ -283,7 +286,7 @@ class PlayQueue(PlexObject):
|
|||
|
||||
path = f"/playQueues/{self.playQueueID}/items/{item.playQueueItemID}/move{utils.joinArgs(args)}"
|
||||
data = self._server.query(path, method=self._server._session.put)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
||||
def removeItem(self, item, refresh=True):
|
||||
|
@ -301,19 +304,19 @@ class PlayQueue(PlexObject):
|
|||
|
||||
path = f"/playQueues/{self.playQueueID}/items/{item.playQueueItemID}"
|
||||
data = self._server.query(path, method=self._server._session.delete)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items from the PlayQueue."""
|
||||
path = f"/playQueues/{self.playQueueID}/items"
|
||||
data = self._server.query(path, method=self._server._session.delete)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the PlayQueue from the Plex server."""
|
||||
path = f"/playQueues/{self.playQueueID}"
|
||||
data = self._server.query(path, method=self._server._session.get)
|
||||
self._loadData(data)
|
||||
self._invalidateCacheAndLoadData(data)
|
||||
return self
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from functools import cached_property
|
||||
from urllib.parse import urlencode
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, log, logfilter
|
||||
from plexapi import utils
|
||||
from plexapi.alert import AlertListener
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.base import PlexObject, cached_data_property
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.collection import Collection
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
|
@ -110,15 +108,11 @@ class PlexServer(PlexObject):
|
|||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||
self._session = session or requests.Session()
|
||||
self._timeout = timeout or TIMEOUT
|
||||
self._myPlexAccount = None # cached myPlexAccount
|
||||
self._systemAccounts = None # cached list of SystemAccount
|
||||
self._systemDevices = None # cached list of SystemDevice
|
||||
data = self.query(self.key, timeout=self._timeout)
|
||||
super(PlexServer, self).__init__(self, data, self.key)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
|
||||
self.allowChannelAccess = utils.cast(bool, data.attrib.get('allowChannelAccess'))
|
||||
self.allowMediaDeletion = utils.cast(bool, data.attrib.get('allowMediaDeletion'))
|
||||
|
@ -172,7 +166,7 @@ class PlexServer(PlexObject):
|
|||
def _uriRoot(self):
|
||||
return f'server://{self.machineIdentifier}/com.plexapp.plugins.library'
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def library(self):
|
||||
""" Library to browse or search your media. """
|
||||
try:
|
||||
|
@ -183,7 +177,7 @@ class PlexServer(PlexObject):
|
|||
data = self.query('/library/sections/')
|
||||
return Library(self, data)
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def settings(self):
|
||||
""" Returns a list of all server settings. """
|
||||
data = self.query(Settings.key)
|
||||
|
@ -276,11 +270,14 @@ class PlexServer(PlexObject):
|
|||
timeout = self._timeout
|
||||
return PlexServer(self._baseurl, token=userToken, session=session, timeout=timeout)
|
||||
|
||||
@cached_data_property
|
||||
def _systemAccounts(self):
|
||||
""" Cache for systemAccounts. """
|
||||
key = '/accounts'
|
||||
return self.fetchItems(key, SystemAccount)
|
||||
|
||||
def systemAccounts(self):
|
||||
""" Returns a list of :class:`~plexapi.server.SystemAccount` objects this server contains. """
|
||||
if self._systemAccounts is None:
|
||||
key = '/accounts'
|
||||
self._systemAccounts = self.fetchItems(key, SystemAccount)
|
||||
return self._systemAccounts
|
||||
|
||||
def systemAccount(self, accountID):
|
||||
|
@ -294,11 +291,14 @@ class PlexServer(PlexObject):
|
|||
except StopIteration:
|
||||
raise NotFound(f'Unknown account with accountID={accountID}') from None
|
||||
|
||||
@cached_data_property
|
||||
def _systemDevices(self):
|
||||
""" Cache for systemDevices. """
|
||||
key = '/devices'
|
||||
return self.fetchItems(key, SystemDevice)
|
||||
|
||||
def systemDevices(self):
|
||||
""" Returns a list of :class:`~plexapi.server.SystemDevice` objects this server contains. """
|
||||
if self._systemDevices is None:
|
||||
key = '/devices'
|
||||
self._systemDevices = self.fetchItems(key, SystemDevice)
|
||||
return self._systemDevices
|
||||
|
||||
def systemDevice(self, deviceID):
|
||||
|
@ -312,21 +312,24 @@ class PlexServer(PlexObject):
|
|||
except StopIteration:
|
||||
raise NotFound(f'Unknown device with deviceID={deviceID}') from None
|
||||
|
||||
@cached_data_property
|
||||
def _myPlexAccount(self):
|
||||
""" Cache for myPlexAccount. """
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
return MyPlexAccount(token=self._token, session=self._session)
|
||||
|
||||
def myPlexAccount(self):
|
||||
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
||||
token to access this server. If you are not the owner of this PlexServer
|
||||
you're likely to receive an authentication error calling this.
|
||||
"""
|
||||
if self._myPlexAccount is None:
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
self._myPlexAccount = MyPlexAccount(token=self._token, session=self._session)
|
||||
return self._myPlexAccount
|
||||
|
||||
def _myPlexClientPorts(self):
|
||||
""" Sometimes the PlexServer does not properly advertise port numbers required
|
||||
to connect. This attempts to look up device port number from plex.tv.
|
||||
See issue #126: Make PlexServer.clients() more user friendly.
|
||||
https://github.com/pkkid/python-plexapi/issues/126
|
||||
https://github.com/pushingkarmaorg/python-plexapi/issues/126
|
||||
"""
|
||||
try:
|
||||
ports = {}
|
||||
|
@ -768,8 +771,7 @@ class PlexServer(PlexObject):
|
|||
raise NotFound(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
data = utils.cleanXMLString(response.text).encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
return utils.parseXMLString(response.text)
|
||||
|
||||
def search(self, query, mediatype=None, limit=None, sectionId=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
|
@ -804,9 +806,9 @@ class PlexServer(PlexObject):
|
|||
for hub in self.fetchItems(key, Hub):
|
||||
if mediatype:
|
||||
if hub.type == mediatype:
|
||||
return hub.items
|
||||
return hub._partialItems
|
||||
else:
|
||||
results += hub.items
|
||||
results += hub._partialItems
|
||||
return results
|
||||
|
||||
def continueWatching(self):
|
||||
|
@ -1093,7 +1095,7 @@ class Account(PlexObject):
|
|||
key = '/myplex/account'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.authToken = data.attrib.get('authToken')
|
||||
self.username = data.attrib.get('username')
|
||||
self.mappingState = data.attrib.get('mappingState')
|
||||
|
@ -1114,7 +1116,7 @@ class Activity(PlexObject):
|
|||
key = '/activities'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.cancellable = utils.cast(bool, data.attrib.get('cancellable'))
|
||||
self.progress = utils.cast(int, data.attrib.get('progress'))
|
||||
self.title = data.attrib.get('title')
|
||||
|
@ -1129,6 +1131,7 @@ class Release(PlexObject):
|
|||
key = '/updater/status'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.download_key = data.attrib.get('key')
|
||||
self.version = data.attrib.get('version')
|
||||
self.added = data.attrib.get('added')
|
||||
|
@ -1154,7 +1157,7 @@ class SystemAccount(PlexObject):
|
|||
TAG = 'Account'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.autoSelectAudio = utils.cast(bool, data.attrib.get('autoSelectAudio'))
|
||||
self.defaultAudioLanguage = data.attrib.get('defaultAudioLanguage')
|
||||
self.defaultSubtitleLanguage = data.attrib.get('defaultSubtitleLanguage')
|
||||
|
@ -1183,7 +1186,7 @@ class SystemDevice(PlexObject):
|
|||
TAG = 'Device'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
|
@ -1209,7 +1212,7 @@ class StatisticsBandwidth(PlexObject):
|
|||
TAG = 'StatisticsBandwidth'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.accountID = utils.cast(int, data.attrib.get('accountID'))
|
||||
self.at = utils.toDatetime(data.attrib.get('at'))
|
||||
self.bytes = utils.cast(int, data.attrib.get('bytes'))
|
||||
|
@ -1251,7 +1254,7 @@ class StatisticsResources(PlexObject):
|
|||
TAG = 'StatisticsResources'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.at = utils.toDatetime(data.attrib.get('at'))
|
||||
self.hostCpuUtilization = utils.cast(float, data.attrib.get('hostCpuUtilization'))
|
||||
self.hostMemoryUtilization = utils.cast(float, data.attrib.get('hostMemoryUtilization'))
|
||||
|
@ -1279,7 +1282,7 @@ class ButlerTask(PlexObject):
|
|||
TAG = 'ButlerTask'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.description = data.attrib.get('description')
|
||||
self.enabled = utils.cast(bool, data.attrib.get('enabled'))
|
||||
self.interval = utils.cast(int, data.attrib.get('interval'))
|
||||
|
@ -1301,7 +1304,7 @@ class Identity(PlexObject):
|
|||
return f"<{self.__class__.__name__}:{self.machineIdentifier}>"
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.claimed = utils.cast(bool, data.attrib.get('claimed'))
|
||||
self.machineIdentifier = data.attrib.get('machineIdentifier')
|
||||
self.version = data.attrib.get('version')
|
||||
|
|
|
@ -34,11 +34,10 @@ class Settings(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
for elem in data:
|
||||
id = utils.lowerFirst(elem.attrib['id'])
|
||||
if id in self._settings:
|
||||
self._settings[id]._loadData(elem)
|
||||
self._settings[id]._invalidateCacheAndLoadData(elem)
|
||||
continue
|
||||
self._settings[id] = Setting(self._server, elem, self._initpath)
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ class PlexSonosClient(PlexClient):
|
|||
"""
|
||||
|
||||
def __init__(self, account, data, timeout=None):
|
||||
self._data = data
|
||||
self.deviceClass = data.attrib.get("deviceClass")
|
||||
self.machineIdentifier = data.attrib.get("machineIdentifier")
|
||||
self.product = data.attrib.get("product")
|
||||
|
|
|
@ -63,7 +63,7 @@ class SyncItem(PlexObject):
|
|||
self.clientIdentifier = clientIdentifier
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.id = plexapi.utils.cast(int, data.attrib.get('id'))
|
||||
self.version = plexapi.utils.cast(int, data.attrib.get('version'))
|
||||
self.rootTitle = data.attrib.get('rootTitle')
|
||||
|
@ -118,7 +118,7 @@ class SyncList(PlexObject):
|
|||
TAG = 'SyncList'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.clientId = data.attrib.get('clientIdentifier')
|
||||
self.items = []
|
||||
|
||||
|
|
|
@ -17,12 +17,12 @@ from getpass import getpass
|
|||
from hashlib import sha1
|
||||
from threading import Event, Thread
|
||||
from urllib.parse import quote
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
|
@ -718,3 +718,14 @@ _illegal_XML_re = re.compile(fr'[{"".join(_illegal_XML_ranges)}]')
|
|||
|
||||
def cleanXMLString(s):
|
||||
return _illegal_XML_re.sub('', s)
|
||||
|
||||
|
||||
def parseXMLString(s: str):
|
||||
""" Parse an XML string and return an ElementTree object. """
|
||||
if not s.strip():
|
||||
return None
|
||||
try: # Attempt to parse the string as-is without cleaning (which is expensive)
|
||||
return ElementTree.fromstring(s.encode('utf-8'))
|
||||
except ElementTree.ParseError: # If it fails, clean the string and try again
|
||||
cleaned_s = cleanXMLString(s).encode('utf-8')
|
||||
return ElementTree.fromstring(cleaned_s) if cleaned_s.strip() else None
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession, cached_data_property
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
|
||||
|
@ -48,13 +47,10 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
|
|||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.images = self.findItems(data, media.Image)
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
|
@ -73,6 +69,14 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
|
||||
@cached_data_property
|
||||
def fields(self):
|
||||
return self.findItems(self._data, media.Field)
|
||||
|
||||
@cached_data_property
|
||||
def images(self):
|
||||
return self.findItems(self._data, media.Image)
|
||||
|
||||
def url(self, part):
|
||||
""" Returns the full url for something. Typically used for getting a specific image. """
|
||||
return self._server.url(part, includeToken=True) if part else None
|
||||
|
@ -394,41 +398,86 @@ class Movie(
|
|||
Playable._loadData(self, data)
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
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')
|
||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratingImage = data.attrib.get('ratingImage')
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.slug = data.attrib.get('slug')
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.sourceURI = data.attrib.get('source') # remote playlist item
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def chapters(self):
|
||||
return self.findItems(self._data, media.Chapter)
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def countries(self):
|
||||
return self.findItems(self._data, media.Country)
|
||||
|
||||
@cached_data_property
|
||||
def directors(self):
|
||||
return self.findItems(self._data, media.Director)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def markers(self):
|
||||
return self.findItems(self._data, media.Marker)
|
||||
|
||||
@cached_data_property
|
||||
def media(self):
|
||||
return self.findItems(self._data, media.Media)
|
||||
|
||||
@cached_data_property
|
||||
def producers(self):
|
||||
return self.findItems(self._data, media.Producer)
|
||||
|
||||
@cached_data_property
|
||||
def ratings(self):
|
||||
return self.findItems(self._data, media.Rating)
|
||||
|
||||
@cached_data_property
|
||||
def roles(self):
|
||||
return self.findItems(self._data, media.Role)
|
||||
|
||||
@cached_data_property
|
||||
def similar(self):
|
||||
return self.findItems(self._data, media.Similar)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
@cached_data_property
|
||||
def writers(self):
|
||||
return self.findItems(self._data, media.Writer)
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
""" Alias to self.roles. """
|
||||
|
@ -573,40 +622,67 @@ class Show(
|
|||
self.autoDeletionItemPolicyWatchedLibrary = utils.cast(
|
||||
int, data.attrib.get('autoDeletionItemPolicyWatchedLibrary', '0'))
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
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)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.languageOverride = data.attrib.get('languageOverride')
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||
self.network = data.attrib.get('network')
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.originalTitle = data.attrib.get('originalTitle')
|
||||
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.slug = data.attrib.get('slug')
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.subtitleLanguage = data.attrib.get('subtitleLanguage', '')
|
||||
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def genres(self):
|
||||
return self.findItems(self._data, media.Genre)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def locations(self):
|
||||
return self.listAttrs(self._data, 'path', etag='Location')
|
||||
|
||||
@cached_data_property
|
||||
def ratings(self):
|
||||
return self.findItems(self._data, media.Rating)
|
||||
|
||||
@cached_data_property
|
||||
def roles(self):
|
||||
return self.findItems(self._data, media.Role)
|
||||
|
||||
@cached_data_property
|
||||
def similar(self):
|
||||
return self.findItems(self._data, media.Similar)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
def __iter__(self):
|
||||
for season in self.seasons():
|
||||
yield season
|
||||
|
@ -759,11 +835,8 @@ class Season(
|
|||
Video._loadData(self, data)
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
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'))
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
|
@ -775,13 +848,31 @@ class Season(
|
|||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.subtitleLanguage = data.attrib.get('subtitleLanguage', '')
|
||||
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def ratings(self):
|
||||
return self.findItems(self._data, media.Rating)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
def __iter__(self):
|
||||
for episode in self.episodes():
|
||||
yield episode
|
||||
|
@ -942,11 +1033,8 @@ class Episode(
|
|||
Playable._loadData(self, data)
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||
|
@ -956,25 +1044,16 @@ class Episode(
|
|||
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
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.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.parentYear = utils.cast(int, data.attrib.get('parentYear'))
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
||||
self.sourceURI = data.attrib.get('source') # remote playlist item
|
||||
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
# If seasons are hidden, parentKey and parentRatingKey are missing from the XML response.
|
||||
|
@ -984,7 +1063,55 @@ class Episode(
|
|||
self._parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self._parentThumb = data.attrib.get('parentThumb')
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def chapters(self):
|
||||
return self.findItems(self._data, media.Chapter)
|
||||
|
||||
@cached_data_property
|
||||
def collections(self):
|
||||
return self.findItems(self._data, media.Collection)
|
||||
|
||||
@cached_data_property
|
||||
def directors(self):
|
||||
return self.findItems(self._data, media.Director)
|
||||
|
||||
@cached_data_property
|
||||
def guids(self):
|
||||
return self.findItems(self._data, media.Guid)
|
||||
|
||||
@cached_data_property
|
||||
def labels(self):
|
||||
return self.findItems(self._data, media.Label)
|
||||
|
||||
@cached_data_property
|
||||
def markers(self):
|
||||
return self.findItems(self._data, media.Marker)
|
||||
|
||||
@cached_data_property
|
||||
def media(self):
|
||||
return self.findItems(self._data, media.Media)
|
||||
|
||||
@cached_data_property
|
||||
def producers(self):
|
||||
return self.findItems(self._data, media.Producer)
|
||||
|
||||
@cached_data_property
|
||||
def ratings(self):
|
||||
return self.findItems(self._data, media.Rating)
|
||||
|
||||
@cached_data_property
|
||||
def roles(self):
|
||||
return self.findItems(self._data, media.Role)
|
||||
|
||||
@cached_data_property
|
||||
def writers(self):
|
||||
return self.findItems(self._data, media.Writer)
|
||||
|
||||
@cached_data_property
|
||||
def ultraBlurColors(self):
|
||||
return self.findItem(self._data, media.UltraBlurColors)
|
||||
|
||||
@cached_data_property
|
||||
def parentKey(self):
|
||||
""" Returns the parentKey. Refer to the Episode attributes. """
|
||||
if self._parentKey:
|
||||
|
@ -993,7 +1120,7 @@ class Episode(
|
|||
return f'/library/metadata/{self.parentRatingKey}'
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def parentRatingKey(self):
|
||||
""" Returns the parentRatingKey. Refer to the Episode attributes. """
|
||||
if self._parentRatingKey is not None:
|
||||
|
@ -1006,7 +1133,7 @@ class Episode(
|
|||
return self._season.ratingKey
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def parentThumb(self):
|
||||
""" Returns the parentThumb. Refer to the Episode attributes. """
|
||||
if self._parentThumb:
|
||||
|
@ -1015,7 +1142,7 @@ class Episode(
|
|||
return self._season.thumb
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def _season(self):
|
||||
""" Returns the :class:`~plexapi.video.Season` object by querying for the show's children. """
|
||||
if self.grandparentKey and self.parentIndex is not None:
|
||||
|
@ -1055,7 +1182,7 @@ class Episode(
|
|||
""" Returns the episode number. """
|
||||
return self.index
|
||||
|
||||
@cached_property
|
||||
@cached_data_property
|
||||
def seasonNumber(self):
|
||||
""" Returns the episode's season number. """
|
||||
if isinstance(self.parentIndex, int):
|
||||
|
@ -1149,12 +1276,10 @@ class Clip(
|
|||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._data = data
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.extraType = utils.cast(int, data.attrib.get('extraType'))
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = utils.toDatetime(
|
||||
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
|
||||
|
@ -1163,6 +1288,10 @@ class Clip(
|
|||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@cached_data_property
|
||||
def media(self):
|
||||
return self.findItems(self._data, media.Media)
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
""" This does not exist in plex xml response but is added to have a common
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue