Skip to content

Commit d3dfafd

Browse files
authored
Refactor use of manual cached attributes (#1516)
* Remove manual cache attributes * Replace cached_property with cached_data_property * Fix calls to hub.items * Invalidate cached libraries after adding/removing a section
1 parent 594d312 commit d3dfafd

File tree

7 files changed

+149
-126
lines changed

7 files changed

+149
-126
lines changed

plexapi/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,7 @@ def sessions(self):
10441044
def transcodeSessions(self):
10451045
return [self.transcodeSession] if self.transcodeSession else []
10461046

1047-
@cached_property
1047+
@cached_data_property
10481048
def user(self):
10491049
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
10501050
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.

plexapi/collection.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,6 @@ def _loadData(self, data):
104104
self.type = data.attrib.get('type')
105105
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
106106
self.userRating = utils.cast(float, data.attrib.get('userRating'))
107-
self._items = None # cache for self.items
108-
self._section = None # cache for self.section
109-
self._filters = None # cache for self.filters
110107

111108
@cached_data_property
112109
def fields(self):
@@ -174,20 +171,26 @@ def isPhoto(self):
174171
def children(self):
175172
return self.items()
176173

174+
@cached_data_property
175+
def _filters(self):
176+
""" Cache for filters. """
177+
return self._parseFilters(self.content)
178+
177179
def filters(self):
178180
""" Returns the search filter dict for smart collection.
179181
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
180182
to get the list of items.
181183
"""
182-
if self.smart and self._filters is None:
183-
self._filters = self._parseFilters(self.content)
184184
return self._filters
185185

186+
@cached_data_property
187+
def _section(self):
188+
""" Cache for section. """
189+
return super(Collection, self).section()
190+
186191
def section(self):
187192
""" Returns the :class:`~plexapi.library.LibrarySection` this collection belongs to.
188193
"""
189-
if self._section is None:
190-
self._section = super(Collection, self).section()
191194
return self._section
192195

193196
def item(self, title):
@@ -204,12 +207,14 @@ def item(self, title):
204207
return item
205208
raise NotFound(f'Item with title "{title}" not found in the collection')
206209

210+
@cached_data_property
211+
def _items(self):
212+
""" Cache for the items. """
213+
key = f'{self.key}/children'
214+
return self.fetchItems(key)
215+
207216
def items(self):
208217
""" Returns a list of all items in the collection. """
209-
if self._items is None:
210-
key = f'{self.key}/children'
211-
items = self.fetchItems(key)
212-
self._items = items
213218
return self._items
214219

215220
def visibility(self):

plexapi/library.py

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import warnings
77
from collections import defaultdict
88
from datetime import datetime
9-
from functools import cached_property
109
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse
1110

1211
from plexapi import log, media, utils
@@ -44,9 +43,8 @@ def _loadData(self, data):
4443
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
4544
self.title1 = data.attrib.get('title1')
4645
self.title2 = data.attrib.get('title2')
47-
self._sectionsByID = {} # cached sections by key
48-
self._sectionsByTitle = {} # cached sections by title
4946

47+
@cached_data_property
5048
def _loadSections(self):
5149
""" Loads and caches all the library sections. """
5250
key = '/library/sections'
@@ -64,15 +62,23 @@ def _loadSections(self):
6462
sectionsByID[section.key] = section
6563
sectionsByTitle[section.title.lower().strip()].append(section)
6664

67-
self._sectionsByID = sectionsByID
68-
self._sectionsByTitle = dict(sectionsByTitle)
65+
return sectionsByID, dict(sectionsByTitle)
66+
67+
@property
68+
def _sectionsByID(self):
69+
""" Returns a dictionary of all library sections by ID. """
70+
return self._loadSections[0]
71+
72+
@property
73+
def _sectionsByTitle(self):
74+
""" Returns a dictionary of all library sections by title. """
75+
return self._loadSections[1]
6976

7077
def sections(self):
7178
""" Returns a list of all media sections in this library. Library sections may be any of
7279
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
7380
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
7481
"""
75-
self._loadSections()
7682
return list(self._sectionsByID.values())
7783

7884
def section(self, title):
@@ -87,8 +93,6 @@ def section(self, title):
8793
:exc:`~plexapi.exceptions.NotFound`: The library section title is not found on the server.
8894
"""
8995
normalized_title = title.lower().strip()
90-
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
91-
self._loadSections()
9296
try:
9397
sections = self._sectionsByTitle[normalized_title]
9498
except KeyError:
@@ -110,8 +114,6 @@ def sectionByID(self, sectionID):
110114
Raises:
111115
:exc:`~plexapi.exceptions.NotFound`: The library section ID is not found on the server.
112116
"""
113-
if not self._sectionsByID or sectionID not in self._sectionsByID:
114-
self._loadSections()
115117
try:
116118
return self._sectionsByID[sectionID]
117119
except KeyError:
@@ -385,7 +387,9 @@ def add(self, name='', type='', agent='', scanner='', location='', language='en-
385387
if kwargs:
386388
prefs_params = {f'prefs[{k}]': v for k, v in kwargs.items()}
387389
part += f'&{urlencode(prefs_params)}'
388-
return self._server.query(part, method=self._server._session.post)
390+
data = self._server.query(part, method=self._server._session.post)
391+
self._invalidateCachedProperties()
392+
return data
389393

390394
def history(self, maxresults=None, mindate=None):
391395
""" Get Play History for all library Sections for the owner.
@@ -448,35 +452,25 @@ def _loadData(self, data):
448452
self.type = data.attrib.get('type')
449453
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
450454
self.uuid = data.attrib.get('uuid')
451-
# Private attrs as we don't want a reload.
452-
self._filterTypes = None
453-
self._fieldTypes = None
454-
self._totalViewSize = None
455-
self._totalDuration = None
456-
self._totalStorage = None
457455

458456
@cached_data_property
459457
def locations(self):
460458
return self.listAttrs(self._data, 'path', etag='Location')
461459

462-
@cached_property
460+
@cached_data_property
463461
def totalSize(self):
464462
""" Returns the total number of items in the library for the default library type. """
465463
return self.totalViewSize(includeCollections=False)
466464

467465
@property
468466
def totalDuration(self):
469467
""" Returns the total duration (in milliseconds) of items in the library. """
470-
if self._totalDuration is None:
471-
self._getTotalDurationStorage()
472-
return self._totalDuration
468+
return self._getTotalDurationStorage[0]
473469

474470
@property
475471
def totalStorage(self):
476472
""" Returns the total storage (in bytes) of items in the library. """
477-
if self._totalStorage is None:
478-
self._getTotalDurationStorage()
479-
return self._totalStorage
473+
return self._getTotalDurationStorage[1]
480474

481475
def __getattribute__(self, attr):
482476
# Intercept to call EditFieldMixin and EditTagMixin methods
@@ -492,6 +486,7 @@ def __getattribute__(self, attr):
492486
)
493487
return value
494488

489+
@cached_data_property
495490
def _getTotalDurationStorage(self):
496491
""" Queries the Plex server for the total library duration and storage and caches the values. """
497492
data = self._server.query('/media/providers?includeStorage=1')
@@ -502,8 +497,10 @@ def _getTotalDurationStorage(self):
502497
)
503498
directory = next(iter(data.findall(xpath)), None)
504499
if directory:
505-
self._totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
506-
self._totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
500+
totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
501+
totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
502+
return totalDuration, totalStorage
503+
return None, None
507504

508505
def totalViewSize(self, libtype=None, includeCollections=True):
509506
""" Returns the total number of items in the library for a specified libtype.
@@ -534,7 +531,9 @@ def totalViewSize(self, libtype=None, includeCollections=True):
534531
def delete(self):
535532
""" Delete a library section. """
536533
try:
537-
return self._server.query(f'/library/sections/{self.key}', method=self._server._session.delete)
534+
data = self._server.query(f'/library/sections/{self.key}', method=self._server._session.delete)
535+
self._server.library._invalidateCachedProperties()
536+
return data
538537
except BadRequest: # pragma: no cover
539538
msg = f'Failed to delete library {self.key}'
540539
msg += 'You may need to allow this permission in your Plex settings.'
@@ -874,6 +873,7 @@ def deleteMediaPreviews(self):
874873
self._server.query(key, method=self._server._session.delete)
875874
return self
876875

876+
@cached_data_property
877877
def _loadFilters(self):
878878
""" Retrieves and caches the list of :class:`~plexapi.library.FilteringType` and
879879
list of :class:`~plexapi.library.FilteringFieldType` for this library section.
@@ -883,23 +883,23 @@ def _loadFilters(self):
883883

884884
key = _key.format(key=self.key, filter='all')
885885
data = self._server.query(key)
886-
self._filterTypes = self.findItems(data, FilteringType, rtag='Meta')
887-
self._fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
886+
filterTypes = self.findItems(data, FilteringType, rtag='Meta')
887+
fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
888888

889889
if self.TYPE != 'photo': # No collections for photo library
890890
key = _key.format(key=self.key, filter='collections')
891891
data = self._server.query(key)
892-
self._filterTypes.extend(self.findItems(data, FilteringType, rtag='Meta'))
892+
filterTypes.extend(self.findItems(data, FilteringType, rtag='Meta'))
893893

894894
# Manually add guid field type, only allowing "is" operator
895895
guidFieldType = '<FieldType type="guid"><Operator key="=" title="is"/></FieldType>'
896-
self._fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
896+
fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
897+
898+
return filterTypes, fieldTypes
897899

898900
def filterTypes(self):
899901
""" Returns a list of available :class:`~plexapi.library.FilteringType` for this library section. """
900-
if self._filterTypes is None:
901-
self._loadFilters()
902-
return self._filterTypes
902+
return self._loadFilters[0]
903903

904904
def getFilterType(self, libtype=None):
905905
""" Returns a :class:`~plexapi.library.FilteringType` for a specified libtype.
@@ -921,9 +921,7 @@ def getFilterType(self, libtype=None):
921921

922922
def fieldTypes(self):
923923
""" Returns a list of available :class:`~plexapi.library.FilteringFieldType` for this library section. """
924-
if self._fieldTypes is None:
925-
self._loadFilters()
926-
return self._fieldTypes
924+
return self._loadFilters[1]
927925

928926
def getFieldType(self, fieldType):
929927
""" Returns a :class:`~plexapi.library.FilteringFieldType` for a specified fieldType.
@@ -1972,7 +1970,7 @@ def albums(self):
19721970

19731971
def stations(self):
19741972
""" Returns a list of :class:`~plexapi.playlist.Playlist` stations in this section. """
1975-
return next((hub.items for hub in self.hubs() if hub.context == 'hub.music.stations'), None)
1973+
return next((hub._partialItems for hub in self.hubs() if hub.context == 'hub.music.stations'), None)
19761974

19771975
def searchArtists(self, **kwargs):
19781976
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
@@ -2232,26 +2230,38 @@ def _loadData(self, data):
22322230
self.style = data.attrib.get('style')
22332231
self.title = data.attrib.get('title')
22342232
self.type = data.attrib.get('type')
2235-
self._section = None # cache for self.section
2233+
2234+
def __len__(self):
2235+
return self.size
22362236

22372237
@cached_data_property
2238-
def items(self):
2238+
def _partialItems(self):
2239+
""" Cache for partial items. """
2240+
return self.findItems(self._data)
2241+
2242+
@cached_data_property
2243+
def _items(self):
2244+
""" Cache for items. """
22392245
if self.more and self.key: # If there are more items to load, fetch them
22402246
items = self.fetchItems(self.key)
22412247
self.more = False
22422248
self.size = len(items)
22432249
return items
22442250
# Otherwise, all the data is in the initial _data XML response
2245-
return self.findItems(self._data)
2251+
return self._partialItems
22462252

2247-
def __len__(self):
2248-
return self.size
2253+
def items(self):
2254+
""" Returns a list of all items in the hub. """
2255+
return self._items
2256+
2257+
@cached_data_property
2258+
def _section(self):
2259+
""" Cache for section. """
2260+
return self._server.library.sectionByID(self.librarySectionID)
22492261

22502262
def section(self):
22512263
""" Returns the :class:`~plexapi.library.LibrarySection` this hub belongs to.
22522264
"""
2253-
if self._section is None:
2254-
self._section = self._server.library.sectionByID(self.librarySectionID)
22552265
return self._section
22562266

22572267
def _reload(self, **kwargs):

0 commit comments

Comments
 (0)