Skip to content

Commit

Permalink
Improve reload performance (#1451)
Browse files Browse the repository at this point in the history
* Change default includes to false

* Update isFullObject() check

* Fix checkFiles in tests
  • Loading branch information
JonnyWong16 authored Aug 17, 2024
1 parent d560467 commit bbe3e8e
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 44 deletions.
57 changes: 34 additions & 23 deletions plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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

Expand Down Expand Up @@ -391,31 +391,38 @@ def reload(self, key=None, **kwargs):
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:
.. code-block:: python
from plexapi.server import PlexServer
plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx')
# Search results are partial objects.
movie = plex.library.section('Movies').get('Cars')
movie.isPartialObject() # Returns True
# 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.
# Partial reload of the movie without a default include parameter.
# The movie object will remain as a partial object.
movie.reload(checkFiles=False)
movie.reload(includeMarkers=False)
movie.isPartialObject() # Returns True
# Full reload of the movie with all include parameters.
# 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)

Expand Down Expand Up @@ -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': (
Expand Down Expand Up @@ -592,7 +599,11 @@ def isFullObject(self):
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. """
Expand Down
4 changes: 4 additions & 0 deletions plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 6 additions & 8 deletions plexapi/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -240,8 +240,7 @@ def matches(self, agent=None, title=None, year=None, language=None):
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.
Expand Down Expand Up @@ -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:
Expand All @@ -289,8 +288,7 @@ def hubs(self):
""" 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:
Expand Down
12 changes: 6 additions & 6 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,8 @@ def _prettyfilename(self):

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
Expand Down Expand Up @@ -614,8 +614,8 @@ def onDeck(self):
""" 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.
Expand Down Expand Up @@ -796,8 +796,8 @@ def onDeck(self):
""" 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.
Expand Down
16 changes: 9 additions & 7 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit bbe3e8e

Please sign in to comment.