Skip to content

Commit 0d4124f

Browse files
Lazy loading and caching for attributes set in _loadData(..) (#1510)
* feat: Implemented a caching mecahnism for PlexObject classes - Cached properties are defined using a `cached_data_property` decorator - Property caches are automatically invalidated whenever the `_data` atribute is changed - The implementation uses a metaclass to collect and track all cached properties across class inheritances * perf: Cache all data attributes that are computation heavy These attributes include those that call `findItems` and `listAttrs` * fix: Don't invalidate property cache on object initialization * refactor: For all Plex objects, call the base class's loadData function to do cache invalidation * perf: Convert attributes that call `findItem` to cached data properties * perf: Attempt to parse XML strings without cleaning (which is expensive) before trying again with cleaning * Revert "perf: Attempt to parse XML strings without cleaning (which is expensive) before trying again with cleaning" This reverts commit e8348df. * fix: Use the correct attribute name when deleting invalidated cached data * fix: Follow the same behavior as before the introduction of cached properties and don't call the super class' _loadData * fix: Typo in declaring cached data property attributes * test: Don't use ` __dict__` to access attributes * test: Don't reload objects for the test_video_Movie_reload_kwargs test * fix: Ensure `PlexObject._loadData` is called in child classes that override loadData * fix: Handle special cache invalidation for LibrarySection objects * test: Tests for cache invalidation in library and video objects * test: Removed unexpected exception from test_library_section_cache_invalidation * test: Replaced incorrect object ID comparisons with string string repr comparisons * refactor: Removed uneeded variable assignment Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> * perf: Convert languageCodes and mediaTypes to lazy-loaded cached properties * refactor: Delegate cache invalidation logic to the reload function instead of _loadData * style: Removed unecessary explicit object inheritence (implicit since Python3) * perf: Lazy load expensive attributes in PlexSession * docs: Make it clearer that Playable, PlexSession, and PlexHistory are mixins * fix: Handle special reload cache invalidation logic for MyPlexAccount and PlexClient * refactor: Unused import * style: Reorder functions for more accurate order of operation * docs: Removed typo * refactor: Fixed all flake8 unused import warning * fix: Invalidate the cache after all PUT queries in the PlayQueue object * refactor: Call loadData with invalidation in the Settings class for future compatability * refactor: Better align the items and reload functions with the internal caching mechanism * fix: Reset the state of Hub pagination metadata on reloads * docs: Updated docstring for `Hub.reload(...)` changes --------- Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
1 parent 80edff1 commit 0d4124f

17 files changed

+651
-225
lines changed

plexapi/audio.py

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Dict, List, Optional, TypeVar
99

1010
from plexapi import media, utils
11-
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
11+
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession, cached_data_property
1212
from plexapi.exceptions import BadRequest
1313
from plexapi.mixins import (
1414
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
@@ -59,14 +59,11 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
5959

6060
def _loadData(self, data):
6161
""" Load attribute values from Plex XML response. """
62-
self._data = data
6362
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
6463
self.art = data.attrib.get('art')
6564
self.artBlurHash = data.attrib.get('artBlurHash')
6665
self.distance = utils.cast(float, data.attrib.get('distance'))
67-
self.fields = self.findItems(data, media.Field)
6866
self.guid = data.attrib.get('guid')
69-
self.images = self.findItems(data, media.Image)
7067
self.index = utils.cast(int, data.attrib.get('index'))
7168
self.key = data.attrib.get('key', '')
7269
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
@@ -75,7 +72,6 @@ def _loadData(self, data):
7572
self.librarySectionKey = data.attrib.get('librarySectionKey')
7673
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
7774
self.listType = 'audio'
78-
self.moods = self.findItems(data, media.Mood)
7975
self.musicAnalysisVersion = utils.cast(int, data.attrib.get('musicAnalysisVersion'))
8076
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
8177
self.summary = data.attrib.get('summary')
@@ -88,6 +84,18 @@ def _loadData(self, data):
8884
self.userRating = utils.cast(float, data.attrib.get('userRating'))
8985
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
9086

87+
@cached_data_property
88+
def fields(self):
89+
return self.findItems(self._data, media.Field)
90+
91+
@cached_data_property
92+
def images(self):
93+
return self.findItems(self._data, media.Image)
94+
95+
@cached_data_property
96+
def moods(self):
97+
return self.findItems(self._data, media.Mood)
98+
9199
def url(self, part):
92100
""" Returns the full URL for the audio item. Typically used for getting a specific track. """
93101
return self._server.url(part, includeToken=True) if part else None
@@ -205,18 +213,45 @@ def _loadData(self, data):
205213
Audio._loadData(self, data)
206214
self.albumSort = utils.cast(int, data.attrib.get('albumSort', '-1'))
207215
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
208-
self.collections = self.findItems(data, media.Collection)
209-
self.countries = self.findItems(data, media.Country)
210-
self.genres = self.findItems(data, media.Genre)
211-
self.guids = self.findItems(data, media.Guid)
212216
self.key = self.key.replace('/children', '') # FIX_BUG_50
213-
self.labels = self.findItems(data, media.Label)
214-
self.locations = self.listAttrs(data, 'path', etag='Location')
215217
self.rating = utils.cast(float, data.attrib.get('rating'))
216-
self.similar = self.findItems(data, media.Similar)
217-
self.styles = self.findItems(data, media.Style)
218218
self.theme = data.attrib.get('theme')
219-
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
219+
220+
@cached_data_property
221+
def collections(self):
222+
return self.findItems(self._data, media.Collection)
223+
224+
@cached_data_property
225+
def countries(self):
226+
return self.findItems(self._data, media.Country)
227+
228+
@cached_data_property
229+
def genres(self):
230+
return self.findItems(self._data, media.Genre)
231+
232+
@cached_data_property
233+
def guids(self):
234+
return self.findItems(self._data, media.Guid)
235+
236+
@cached_data_property
237+
def labels(self):
238+
return self.findItems(self._data, media.Label)
239+
240+
@cached_data_property
241+
def locations(self):
242+
return self.listAttrs(self._data, 'path', etag='Location')
243+
244+
@cached_data_property
245+
def similar(self):
246+
return self.findItems(self._data, media.Similar)
247+
248+
@cached_data_property
249+
def styles(self):
250+
return self.findItems(self._data, media.Style)
251+
252+
@cached_data_property
253+
def ultraBlurColors(self):
254+
return self.findItem(self._data, media.UltraBlurColors)
220255

221256
def __iter__(self):
222257
for album in self.albums():
@@ -355,12 +390,7 @@ def _loadData(self, data):
355390
""" Load attribute values from Plex XML response. """
356391
Audio._loadData(self, data)
357392
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
358-
self.collections = self.findItems(data, media.Collection)
359-
self.formats = self.findItems(data, media.Format)
360-
self.genres = self.findItems(data, media.Genre)
361-
self.guids = self.findItems(data, media.Guid)
362393
self.key = self.key.replace('/children', '') # FIX_BUG_50
363-
self.labels = self.findItems(data, media.Label)
364394
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
365395
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
366396
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
@@ -372,12 +402,41 @@ def _loadData(self, data):
372402
self.parentTitle = data.attrib.get('parentTitle')
373403
self.rating = utils.cast(float, data.attrib.get('rating'))
374404
self.studio = data.attrib.get('studio')
375-
self.styles = self.findItems(data, media.Style)
376-
self.subformats = self.findItems(data, media.Subformat)
377-
self.ultraBlurColors = self.findItem(data, media.UltraBlurColors)
378405
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
379406
self.year = utils.cast(int, data.attrib.get('year'))
380407

408+
@cached_data_property
409+
def collections(self):
410+
return self.findItems(self._data, media.Collection)
411+
412+
@cached_data_property
413+
def formats(self):
414+
return self.findItems(self._data, media.Format)
415+
416+
@cached_data_property
417+
def genres(self):
418+
return self.findItems(self._data, media.Genre)
419+
420+
@cached_data_property
421+
def guids(self):
422+
return self.findItems(self._data, media.Guid)
423+
424+
@cached_data_property
425+
def labels(self):
426+
return self.findItems(self._data, media.Label)
427+
428+
@cached_data_property
429+
def styles(self):
430+
return self.findItems(self._data, media.Style)
431+
432+
@cached_data_property
433+
def subformats(self):
434+
return self.findItems(self._data, media.Subformat)
435+
436+
@cached_data_property
437+
def ultraBlurColors(self):
438+
return self.findItem(self._data, media.UltraBlurColors)
439+
381440
def __iter__(self):
382441
for track in self.tracks():
383442
yield track
@@ -495,21 +554,15 @@ def _loadData(self, data):
495554
Audio._loadData(self, data)
496555
Playable._loadData(self, data)
497556
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
498-
self.chapters = self.findItems(data, media.Chapter)
499557
self.chapterSource = data.attrib.get('chapterSource')
500-
self.collections = self.findItems(data, media.Collection)
501558
self.duration = utils.cast(int, data.attrib.get('duration'))
502-
self.genres = self.findItems(data, media.Genre)
503559
self.grandparentArt = data.attrib.get('grandparentArt')
504560
self.grandparentGuid = data.attrib.get('grandparentGuid')
505561
self.grandparentKey = data.attrib.get('grandparentKey')
506562
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
507563
self.grandparentTheme = data.attrib.get('grandparentTheme')
508564
self.grandparentThumb = data.attrib.get('grandparentThumb')
509565
self.grandparentTitle = data.attrib.get('grandparentTitle')
510-
self.guids = self.findItems(data, media.Guid)
511-
self.labels = self.findItems(data, media.Label)
512-
self.media = self.findItems(data, media.Media)
513566
self.originalTitle = data.attrib.get('originalTitle')
514567
self.parentGuid = data.attrib.get('parentGuid')
515568
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
@@ -525,6 +578,30 @@ def _loadData(self, data):
525578
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
526579
self.year = utils.cast(int, data.attrib.get('year'))
527580

581+
@cached_data_property
582+
def chapters(self):
583+
return self.findItems(self._data, media.Chapter)
584+
585+
@cached_data_property
586+
def collections(self):
587+
return self.findItems(self._data, media.Collection)
588+
589+
@cached_data_property
590+
def genres(self):
591+
return self.findItems(self._data, media.Genre)
592+
593+
@cached_data_property
594+
def guids(self):
595+
return self.findItems(self._data, media.Guid)
596+
597+
@cached_data_property
598+
def labels(self):
599+
return self.findItems(self._data, media.Label)
600+
601+
@cached_data_property
602+
def media(self):
603+
return self.findItems(self._data, media.Media)
604+
528605
@property
529606
def locations(self):
530607
""" This does not exist in plex xml response but is added to have a common

0 commit comments

Comments
 (0)