Skip to content

Lazy loading and caching for attributes set in _loadData(..) #1510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0806788
feat: Implemented a caching mecahnism for PlexObject classes
eliasbenb Apr 4, 2025
9fe844b
perf: Cache all data attributes that are computation heavy
eliasbenb Apr 4, 2025
d8860c9
fix: Don't invalidate property cache on object initialization
eliasbenb Apr 4, 2025
c31f7c6
refactor: For all Plex objects, call the base class's loadData functi…
eliasbenb Apr 4, 2025
9d5abc9
perf: Convert attributes that call `findItem` to cached data properties
eliasbenb Apr 4, 2025
e8348df
perf: Attempt to parse XML strings without cleaning (which is expensi…
eliasbenb Apr 4, 2025
aed1fd9
Revert "perf: Attempt to parse XML strings without cleaning (which is…
eliasbenb Apr 4, 2025
da10d35
fix: Use the correct attribute name when deleting invalidated cached …
eliasbenb Apr 4, 2025
f976cf0
fix: Follow the same behavior as before the introduction of cached pr…
eliasbenb Apr 4, 2025
526e47b
fix: Typo in declaring cached data property attributes
eliasbenb Apr 4, 2025
c3cd373
test: Don't use ` __dict__` to access attributes
eliasbenb Apr 4, 2025
83e6286
test: Don't reload objects for the test_video_Movie_reload_kwargs test
eliasbenb Apr 5, 2025
0ef26ed
fix: Ensure `PlexObject._loadData` is called in child classes that ov…
eliasbenb Apr 5, 2025
4f66a0a
fix: Handle special cache invalidation for LibrarySection objects
eliasbenb Apr 5, 2025
947393d
test: Tests for cache invalidation in library and video objects
eliasbenb Apr 5, 2025
2212c2d
test: Removed unexpected exception from test_library_section_cache_in…
eliasbenb Apr 5, 2025
aea4282
test: Replaced incorrect object ID comparisons with string string rep…
eliasbenb Apr 5, 2025
8fa7d13
refactor: Removed uneeded variable assignment
eliasbenb Apr 7, 2025
cd2c8f7
perf: Convert languageCodes and mediaTypes to lazy-loaded cached prop…
eliasbenb Apr 7, 2025
6ad68ce
refactor: Delegate cache invalidation logic to the reload function in…
eliasbenb Apr 7, 2025
90bf0fd
style: Removed unecessary explicit object inheritence (implicit since…
eliasbenb Apr 7, 2025
8c91769
perf: Lazy load expensive attributes in PlexSession
eliasbenb Apr 7, 2025
ca0b1c8
docs: Make it clearer that Playable, PlexSession, and PlexHistory are…
eliasbenb Apr 7, 2025
88e165f
fix: Handle special reload cache invalidation logic for MyPlexAccount…
eliasbenb Apr 7, 2025
0229729
refactor: Unused import
eliasbenb Apr 7, 2025
28bd9db
style: Reorder functions for more accurate order of operation
eliasbenb Apr 7, 2025
81c16b7
docs: Removed typo
eliasbenb Apr 7, 2025
53364a9
refactor: Fixed all flake8 unused import warning
eliasbenb Apr 8, 2025
65303c2
fix: Invalidate the cache after all PUT queries in the PlayQueue object
eliasbenb Apr 8, 2025
0320360
refactor: Call loadData with invalidation in the Settings class for f…
eliasbenb Apr 9, 2025
a06a43f
refactor: Better align the items and reload functions with the intern…
eliasbenb Apr 9, 2025
c98a9d1
fix: Reset the state of Hub pagination metadata on reloads
eliasbenb Apr 9, 2025
e3fc9c8
docs: Updated docstring for `Hub.reload(...)` changes
eliasbenb Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 105 additions & 28 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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,
Expand Down Expand Up @@ -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'))
Expand All @@ -75,7 +72,6 @@ def _loadData(self, data):
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')
Expand All @@ -88,6 +84,18 @@ def _loadData(self, data):
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
Expand Down Expand Up @@ -205,18 +213,45 @@ def _loadData(self, data):
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():
Expand Down Expand Up @@ -355,12 +390,7 @@ def _loadData(self, data):
""" 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')
Expand All @@ -372,12 +402,41 @@ def _loadData(self, data):
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
Expand Down Expand Up @@ -495,21 +554,15 @@ def _loadData(self, data):
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')
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
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'))
Expand All @@ -525,6 +578,30 @@ def _loadData(self, data):
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
Expand Down
Loading
Loading