diff --git a/plexapi/base.py b/plexapi/base.py index 26b103b9..a7fa82ee 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -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. """ diff --git a/plexapi/media.py b/plexapi/media.py index 56126dcb..36020791 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -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. diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 8571ba63..bdf4607e 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -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: diff --git a/plexapi/video.py b/plexapi/video.py index 609f57f6..8622d4a7 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -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. diff --git a/tests/test_video.py b/tests/test_video.py index b25fe4b3..fe00a115 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -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