66import warnings
77from collections import defaultdict
88from datetime import datetime
9- from functools import cached_property
109from urllib .parse import parse_qs , quote_plus , urlencode , urlparse
1110
1211from 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