Improve reload performance (#1451)

* Change default includes to false

* Update isFullObject() check

* Fix checkFiles in tests
This commit is contained in:
JonnyWong16 2024-08-17 14:00:22 -07:00 committed by GitHub
parent d5604670d5
commit bbe3e8e49f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 61 additions and 46 deletions

View file

@ -3,7 +3,7 @@ import re
from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, TypeVar, Union
import weakref
from functools import cached_property
from urllib.parse import urlencode
from urllib.parse import parse_qsl, urlencode, urlparse
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
@ -391,10 +391,9 @@ class PlexObject:
Parameters:
key (string, optional): Override the key to reload.
**kwargs (dict): A dictionary of XML include parameters to exclude or override.
All parameters are included by default with the option to override each parameter
or disable each parameter individually by setting it to False or 0.
**kwargs (dict): A dictionary of XML include parameters to include/exclude or override.
See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters.
Set parameter to True to include and False to exclude.
Example:
@ -402,20 +401,28 @@ class PlexObject:
from plexapi.server import PlexServer
plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx')
movie = plex.library.section('Movies').get('Cars')
# Partial reload of the movie without the `checkFiles` parameter.
# Excluding `checkFiles` will prevent the Plex server from reading the
# file to check if the file still exists and is accessible.
# The movie object will remain as a partial object.
movie.reload(checkFiles=False)
# Search results are partial objects.
movie = plex.library.section('Movies').get('Cars')
movie.isPartialObject() # Returns True
# Full reload of the movie with all include parameters.
# Partial reload of the movie without a default include parameter.
# The movie object will remain as a partial object.
movie.reload(includeMarkers=False)
movie.isPartialObject() # Returns True
# Full reload of the movie with all default include parameters.
# The movie object will be a full object.
movie.reload()
movie.isFullObject() # Returns True
# Full reload of the movie with all default and extra include parameter.
# Including `checkFiles` will tell the Plex server to check if the file
# still exists and is accessible.
# The movie object will be a full object.
movie.reload(checkFiles=True)
movie.isFullObject() # Returns True
"""
return self._reload(key=key, **kwargs)
@ -505,25 +512,25 @@ class PlexPartialObject(PlexObject):
automatically and update itself.
"""
_INCLUDES = {
'checkFiles': 1,
'includeAllConcerts': 1,
'checkFiles': 0,
'includeAllConcerts': 0,
'includeBandwidths': 1,
'includeChapters': 1,
'includeChildren': 1,
'includeConcerts': 1,
'includeExternalMedia': 1,
'includeExtras': 1,
'includeChildren': 0,
'includeConcerts': 0,
'includeExternalMedia': 0,
'includeExtras': 0,
'includeFields': 'thumbBlurHash,artBlurHash',
'includeGeolocation': 1,
'includeLoudnessRamps': 1,
'includeMarkers': 1,
'includeOnDeck': 1,
'includePopularLeaves': 1,
'includePreferences': 1,
'includeRelated': 1,
'includeRelatedCount': 1,
'includeReviews': 1,
'includeStations': 1,
'includeOnDeck': 0,
'includePopularLeaves': 0,
'includePreferences': 0,
'includeRelated': 0,
'includeRelatedCount': 0,
'includeReviews': 0,
'includeStations': 0,
}
_EXCLUDES = {
'excludeElements': (
@ -592,7 +599,11 @@ class PlexPartialObject(PlexObject):
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie would contain.
"""
return not self.key or (self._details_key or self.key) == self._initpath
parsed_key = urlparse(self._details_key or self.key)
parsed_initpath = urlparse(self._initpath)
query_key = set(parse_qsl(parsed_key.query))
query_init = set(parse_qsl(parsed_initpath.query))
return not self.key or (parsed_key.path == parsed_initpath.path and query_key <= query_init)
def isPartialObject(self):
""" Returns True if this is not a full object. """

View file

@ -106,12 +106,16 @@ class MediaPart(PlexObject):
Attributes:
TAG (str): 'Part'
accessible (bool): True if the file is accessible.
Requires reloading the media with ``checkFiles=True``.
Refer to :func:`~plexapi.base.PlexObject.reload`.
audioProfile (str): The audio profile of the file.
container (str): The container type of the file (ex: avi).
decision (str): Unknown.
deepAnalysisVersion (int): The Plex deep analysis version for the file.
duration (int): The duration of the file in milliseconds.
exists (bool): True if the file exists.
Requires reloading the media with ``checkFiles=True``.
Refer to :func:`~plexapi.base.PlexObject.reload`.
file (str): The path to this file on disk (ex: /media/Movies/Cars (2006)/Cars (2006).mkv)
has64bitOffsets (bool): True if the file has 64 bit offsets.
hasThumbnail (bool): True if the file (track) has an embedded thumbnail.

View file

@ -14,8 +14,8 @@ class AdvancedSettingsMixin:
def preferences(self):
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, settings.Preferences, rtag='Preferences')
key = f'{self.key}?includePreferences=1'
return self.fetchItems(key, cls=settings.Preferences, rtag='Preferences')
def preference(self, pref):
""" Returns a :class:`~plexapi.settings.Preferences` object for the specified pref.
@ -240,8 +240,7 @@ class UnmatchMatchMixin:
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
key = key + '?' + urlencode(params)
data = self._server.query(key, method=self._server._session.get)
return self.findItems(data, initpath=key)
return self.fetchItems(key, cls=media.SearchResult)
def fixMatch(self, searchResult=None, auto=False, agent=None):
""" Use match result to update show metadata.
@ -278,8 +277,8 @@ class ExtrasMixin:
def extras(self):
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
from plexapi.video import Extra
data = self._server.query(self._details_key)
return self.findItems(data, Extra, rtag='Extras')
key = f'{self.key}/extras'
return self.fetchItems(key, cls=Extra)
class HubsMixin:
@ -289,8 +288,7 @@ class HubsMixin:
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
from plexapi.library import Hub
key = f'{self.key}/related'
data = self._server.query(key)
return self.findItems(data, Hub)
return self.fetchItems(key, cls=Hub)
class PlayedUnplayedMixin:

View file

@ -456,8 +456,8 @@ class Movie(
def reviews(self):
""" Returns a list of :class:`~plexapi.media.Review` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, media.Review, rtag='Video')
key = f'{self.key}?includeReviews=1'
return self.fetchItems(key, cls=media.Review, rtag='Video')
def editions(self):
""" Returns a list of :class:`~plexapi.video.Movie` objects
@ -614,8 +614,8 @@ class Show(
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
If show is unwatched, return will likely be the first episode.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)
key = f'{self.key}?includeOnDeck=1'
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)
def season(self, title=None, season=None):
""" Returns the season with the specified title or number.
@ -796,8 +796,8 @@ class Season(
""" Returns season's On Deck :class:`~plexapi.video.Video` object or `None`.
Will only return a match if the show's On Deck episode is in this season.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)
key = f'{self.key}?includeOnDeck=1'
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)
def episode(self, title=None, episode=None):
""" Returns the episode with the given title or number.

View file

@ -217,13 +217,13 @@ def test_video_Movie_attrs(movies):
assert utils.is_int(video.width, gte=400)
# Part
part = media.parts[0]
assert part.accessible
assert part.accessible is None
assert part.audioProfile == "lc"
assert part.container in utils.CONTAINERS
assert part.decision is None
assert part.deepAnalysisVersion is None or utils.is_int(part.deepAnalysisVersion)
assert utils.is_int(part.duration, gte=160000)
assert part.exists
assert part.exists is None
assert len(part.file) >= 10
assert part.has64bitOffsets is False
assert part.hasPreviewThumbnails is False
@ -323,10 +323,12 @@ def test_video_Movie_getStreamURL(movie, account):
def test_video_Movie_isFullObject_and_reload(plex):
movie = plex.library.section("Movies").get("Sita Sings the Blues")
assert movie.isFullObject() is False
movie.reload(checkFiles=False)
movie.reload(includeChapters=False)
assert movie.isFullObject() is False
movie.reload()
assert movie.isFullObject() is True
movie.reload(includeExtras=True)
assert movie.isFullObject() is True
movie_via_search = plex.library.search(movie.title)[0]
assert movie_via_search.isFullObject() is False
movie_via_search.reload()
@ -1285,8 +1287,8 @@ def test_video_Episode_attrs(episode):
assert len(part.key) >= 10
assert part._server._baseurl == utils.SERVER_BASEURL
assert utils.is_int(part.size, gte=18184197)
assert part.exists
assert part.accessible
assert part.exists is None
assert part.accessible is None
def test_video_Episode_watched(tvshows):
@ -1434,13 +1436,13 @@ def test_that_reload_return_the_same_object(plex):
def test_video_exists_accessible(movie, episode):
assert movie.media[0].parts[0].exists is None
assert movie.media[0].parts[0].accessible is None
movie.reload()
movie.reload(checkFiles=True)
assert movie.media[0].parts[0].exists is True
assert movie.media[0].parts[0].accessible is True
assert episode.media[0].parts[0].exists is None
assert episode.media[0].parts[0].accessible is None
episode.reload()
episode.reload(checkFiles=True)
assert episode.media[0].parts[0].exists is True
assert episode.media[0].parts[0].accessible is True