Skip to content

Refactor use of manual cached attributes #1516

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
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ def sessions(self):
def transcodeSessions(self):
return [self.transcodeSession] if self.transcodeSession else []

@cached_property
@cached_data_property
def user(self):
""" Returns the :class:`~plexapi.myplex.MyPlexAccount` object (for admin)
or :class:`~plexapi.myplex.MyPlexUser` object (for users) for this session.
Expand Down
27 changes: 16 additions & 11 deletions plexapi/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,6 @@ def _loadData(self, data):
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.userRating = utils.cast(float, data.attrib.get('userRating'))
self._items = None # cache for self.items
self._section = None # cache for self.section
self._filters = None # cache for self.filters

@cached_data_property
def fields(self):
Expand Down Expand Up @@ -174,20 +171,26 @@ def isPhoto(self):
def children(self):
return self.items()

@cached_data_property
def _filters(self):
""" Cache for filters. """
return self._parseFilters(self.content)

def filters(self):
""" Returns the search filter dict for smart collection.
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
to get the list of items.
"""
if self.smart and self._filters is None:
self._filters = self._parseFilters(self.content)
return self._filters

@cached_data_property
def _section(self):
""" Cache for section. """
return super(Collection, self).section()

def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this collection belongs to.
"""
if self._section is None:
self._section = super(Collection, self).section()
return self._section

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

@cached_data_property
def _items(self):
""" Cache for the items. """
key = f'{self.key}/children'
return self.fetchItems(key)

def items(self):
""" Returns a list of all items in the collection. """
if self._items is None:
key = f'{self.key}/children'
items = self.fetchItems(key)
self._items = items
return self._items

def visibility(self):
Expand Down
100 changes: 55 additions & 45 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import warnings
from collections import defaultdict
from datetime import datetime
from functools import cached_property
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse

from plexapi import log, media, utils
Expand Down Expand Up @@ -44,9 +43,8 @@ def _loadData(self, data):
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached sections by key
self._sectionsByTitle = {} # cached sections by title

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

self._sectionsByID = sectionsByID
self._sectionsByTitle = dict(sectionsByTitle)
return sectionsByID, dict(sectionsByTitle)

@property
def _sectionsByID(self):
""" Returns a dictionary of all library sections by ID. """
return self._loadSections[0]

@property
def _sectionsByTitle(self):
""" Returns a dictionary of all library sections by title. """
return self._loadSections[1]

def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
"""
self._loadSections()
return list(self._sectionsByID.values())

def section(self, title):
Expand All @@ -87,8 +93,6 @@ def section(self, title):
:exc:`~plexapi.exceptions.NotFound`: The library section title is not found on the server.
"""
normalized_title = title.lower().strip()
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
self._loadSections()
try:
sections = self._sectionsByTitle[normalized_title]
except KeyError:
Expand All @@ -110,8 +114,6 @@ def sectionByID(self, sectionID):
Raises:
:exc:`~plexapi.exceptions.NotFound`: The library section ID is not found on the server.
"""
if not self._sectionsByID or sectionID not in self._sectionsByID:
self._loadSections()
try:
return self._sectionsByID[sectionID]
except KeyError:
Expand Down Expand Up @@ -385,7 +387,9 @@ def add(self, name='', type='', agent='', scanner='', location='', language='en-
if kwargs:
prefs_params = {f'prefs[{k}]': v for k, v in kwargs.items()}
part += f'&{urlencode(prefs_params)}'
return self._server.query(part, method=self._server._session.post)
data = self._server.query(part, method=self._server._session.post)
self._invalidateCachedProperties()
return data

def history(self, maxresults=None, mindate=None):
""" Get Play History for all library Sections for the owner.
Expand Down Expand Up @@ -448,35 +452,25 @@ def _loadData(self, data):
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.uuid = data.attrib.get('uuid')
# Private attrs as we don't want a reload.
self._filterTypes = None
self._fieldTypes = None
self._totalViewSize = None
self._totalDuration = None
self._totalStorage = None

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

@cached_property
@cached_data_property
def totalSize(self):
""" Returns the total number of items in the library for the default library type. """
return self.totalViewSize(includeCollections=False)

@property
def totalDuration(self):
""" Returns the total duration (in milliseconds) of items in the library. """
if self._totalDuration is None:
self._getTotalDurationStorage()
return self._totalDuration
return self._getTotalDurationStorage[0]

@property
def totalStorage(self):
""" Returns the total storage (in bytes) of items in the library. """
if self._totalStorage is None:
self._getTotalDurationStorage()
return self._totalStorage
return self._getTotalDurationStorage[1]

def __getattribute__(self, attr):
# Intercept to call EditFieldMixin and EditTagMixin methods
Expand All @@ -492,6 +486,7 @@ def __getattribute__(self, attr):
)
return value

@cached_data_property
def _getTotalDurationStorage(self):
""" Queries the Plex server for the total library duration and storage and caches the values. """
data = self._server.query('/media/providers?includeStorage=1')
Expand All @@ -502,8 +497,10 @@ def _getTotalDurationStorage(self):
)
directory = next(iter(data.findall(xpath)), None)
if directory:
self._totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
self._totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
return totalDuration, totalStorage
return None, None

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

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

key = _key.format(key=self.key, filter='all')
data = self._server.query(key)
self._filterTypes = self.findItems(data, FilteringType, rtag='Meta')
self._fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
filterTypes = self.findItems(data, FilteringType, rtag='Meta')
fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')

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

# Manually add guid field type, only allowing "is" operator
guidFieldType = '<FieldType type="guid"><Operator key="=" title="is"/></FieldType>'
self._fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))

return filterTypes, fieldTypes

def filterTypes(self):
""" Returns a list of available :class:`~plexapi.library.FilteringType` for this library section. """
if self._filterTypes is None:
self._loadFilters()
return self._filterTypes
return self._loadFilters[0]

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

def fieldTypes(self):
""" Returns a list of available :class:`~plexapi.library.FilteringFieldType` for this library section. """
if self._fieldTypes is None:
self._loadFilters()
return self._fieldTypes
return self._loadFilters[1]

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

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

def searchArtists(self, **kwargs):
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
Expand Down Expand Up @@ -2232,26 +2230,38 @@ def _loadData(self, data):
self.style = data.attrib.get('style')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self._section = None # cache for self.section

def __len__(self):
return self.size

@cached_data_property
def items(self):
def _partialItems(self):
""" Cache for partial items. """
return self.findItems(self._data)

@cached_data_property
def _items(self):
""" Cache for items. """
if self.more and self.key: # If there are more items to load, fetch them
items = self.fetchItems(self.key)
self.more = False
self.size = len(items)
return items
# Otherwise, all the data is in the initial _data XML response
return self.findItems(self._data)
return self._partialItems

def __len__(self):
return self.size
def items(self):
""" Returns a list of all items in the hub. """
return self._items

@cached_data_property
def _section(self):
""" Cache for section. """
return self._server.library.sectionByID(self.librarySectionID)

def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this hub belongs to.
"""
if self._section is None:
self._section = self._server.library.sectionByID(self.librarySectionID)
return self._section

def _reload(self, **kwargs):
Expand Down
Loading
Loading