mirror of
https://github.com/pkkid/python-plexapi
synced 2025-02-16 21:08:27 +00:00
Merge pull request #518 from pkkid/library_hubs
Library Hubs and Music Stations
This commit is contained in:
commit
6a5981c888
2 changed files with 265 additions and 53 deletions
|
@ -312,9 +312,6 @@ class LibrarySection(PlexObject):
|
|||
""" Base class for a single library section.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (tuple): ()
|
||||
ALLOWED_SORT (tuple): ()
|
||||
BOOLEAN_FILTERS (tuple<str>): ('unwatched', 'duplicate')
|
||||
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||
initpath (str): Path requested when building this object.
|
||||
agent (str): Unknown (com.plexapp.agents.imdb, etc)
|
||||
|
@ -336,9 +333,6 @@ class LibrarySection(PlexObject):
|
|||
totalSize (int): Total number of item in the library
|
||||
|
||||
"""
|
||||
ALLOWED_FILTERS = ()
|
||||
ALLOWED_SORT = ()
|
||||
BOOLEAN_FILTERS = ('unwatched', 'duplicate')
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
|
@ -455,6 +449,52 @@ class LibrarySection(PlexObject):
|
|||
key = '/library/sections/%s/all%s' % (self.key, sortStr)
|
||||
return self.fetchItems(key, **kwargs)
|
||||
|
||||
def folders(self):
|
||||
""" Returns a list of available `:class:`~plexapi.library.Folder` for this library section.
|
||||
"""
|
||||
key = '/library/sections/%s/folder' % self.key
|
||||
return self.fetchItems(key, Folder)
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of available `:class:`~plexapi.library.Hub` for this library section.
|
||||
"""
|
||||
key = '/hubs/sections/%s' % self.key
|
||||
return self.fetchItems(key)
|
||||
|
||||
def _filters(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Filter` from this library section. """
|
||||
key = '/library/sections/%s/filters' % self.key
|
||||
return self.fetchItems(key, cls=Filter)
|
||||
|
||||
def _sorts(self, mediaType=None):
|
||||
""" Returns a list of available `:class:`~plexapi.library.Sort` for this library section.
|
||||
"""
|
||||
items = []
|
||||
for data in self.listChoices('sorts', mediaType):
|
||||
sort = Sort(server=self._server, data=data._data)
|
||||
sort._initpath = data._initpath
|
||||
items.append(sort)
|
||||
return items
|
||||
|
||||
def filterFields(self, mediaType=None):
|
||||
""" Returns a list of available `:class:`~plexapi.library.FilterField` for this library section.
|
||||
"""
|
||||
items = []
|
||||
key = '/library/sections/%s/filters?includeMeta=1' % self.key
|
||||
data = self._server.query(key)
|
||||
for meta in data.iter('Meta'):
|
||||
for metaType in meta.iter('Type'):
|
||||
if not mediaType or metaType.attrib.get('type') == mediaType:
|
||||
fields = self.findItems(metaType, FilterField)
|
||||
for field in fields:
|
||||
field._initpath = metaType.attrib.get('key')
|
||||
fieldType = [_ for _ in self.findItems(meta, FieldType) if _.type == field.type]
|
||||
field.operators = fieldType[0].operators
|
||||
items += fields
|
||||
if not items and mediaType:
|
||||
raise BadRequest('mediaType (%s) not found.' % mediaType)
|
||||
return items
|
||||
|
||||
def agents(self):
|
||||
""" Returns a list of available `:class:`~plexapi.media.Agent` for this library section.
|
||||
"""
|
||||
|
@ -485,6 +525,10 @@ class LibrarySection(PlexObject):
|
|||
"""
|
||||
return self.search(sort='addedAt:desc', maxresults=maxresults)
|
||||
|
||||
def firstCharacter(self):
|
||||
key = '/library/sections/%s/firstCharacter' % self.key
|
||||
return self.fetchItems(key, cls=FirstCharacter)
|
||||
|
||||
def analyze(self):
|
||||
""" Run an analysis on all of the items in this library section. See
|
||||
See :func:`~plexapi.base.PlexPartialObject.analyze` for more details.
|
||||
|
@ -627,12 +671,14 @@ class LibrarySection(PlexObject):
|
|||
|
||||
def _cleanSearchFilter(self, category, value, libtype=None):
|
||||
# check a few things before we begin
|
||||
categories = [x.key for x in self.filterFields()]
|
||||
booleanFilters = [x.key for x in self.filterFields() if x.type == 'boolean']
|
||||
if category.endswith('!'):
|
||||
if category[:-1] not in self.ALLOWED_FILTERS:
|
||||
if category[:-1] not in categories:
|
||||
raise BadRequest('Unknown filter category: %s' % category[:-1])
|
||||
elif category not in self.ALLOWED_FILTERS:
|
||||
elif category not in categories:
|
||||
raise BadRequest('Unknown filter category: %s' % category)
|
||||
if category in self.BOOLEAN_FILTERS:
|
||||
if category in booleanFilters:
|
||||
return '1' if value else '0'
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = [value]
|
||||
|
@ -656,7 +702,8 @@ class LibrarySection(PlexObject):
|
|||
def _cleanSearchSort(self, sort):
|
||||
sort = '%s:asc' % sort if ':' not in sort else sort
|
||||
scol, sdir = sort.lower().split(':')
|
||||
lookup = {s.lower(): s for s in self.ALLOWED_SORT}
|
||||
allowedSort = [sort.key for sort in self._sorts()]
|
||||
lookup = {s.lower(): s for s in allowedSort}
|
||||
if scol not in lookup:
|
||||
raise BadRequest('Unknown sort column: %s' % scol)
|
||||
if sdir not in ('asc', 'desc'):
|
||||
|
@ -757,21 +804,9 @@ class MovieSection(LibrarySection):
|
|||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
|
||||
'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection',
|
||||
'director', 'actor', 'country', 'studio', 'resolution', 'guid', 'label')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'movie'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
|
||||
'collection', 'director', 'actor', 'country', 'studio', 'resolution',
|
||||
'guid', 'label', 'writer', 'producer', 'subtitleLanguage', 'audioLanguage',
|
||||
'lastViewedAt', 'viewCount', 'addedAt')
|
||||
ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'movie'
|
||||
METADATA_TYPE = 'movie'
|
||||
|
@ -821,21 +856,10 @@ class ShowSection(LibrarySection):
|
|||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
|
||||
'year', 'genre', 'contentRating', 'network', 'collection', 'guid', 'label')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt', 'lastViewedAt',
|
||||
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'show'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection',
|
||||
'guid', 'duplicate', 'label', 'show.title', 'show.year', 'show.userRating',
|
||||
'show.viewCount', 'show.lastViewedAt', 'show.actor', 'show.addedAt', 'episode.title',
|
||||
'episode.originallyAvailableAt', 'episode.resolution', 'episode.subtitleLanguage',
|
||||
'episode.unwatched', 'episode.addedAt', 'episode.userRating', 'episode.viewCount',
|
||||
'episode.lastViewedAt')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
|
||||
'rating', 'unwatched')
|
||||
|
||||
TAG = 'Directory'
|
||||
TYPE = 'show'
|
||||
METADATA_TYPE = 'episode'
|
||||
|
@ -855,7 +879,7 @@ class ShowSection(LibrarySection):
|
|||
Parameters:
|
||||
maxresults (int): Max number of items to return (default 50).
|
||||
"""
|
||||
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
|
||||
return self.search(sort='episode.addedAt:desc', libtype=libtype, maxresults=maxresults)
|
||||
|
||||
def collection(self, **kwargs):
|
||||
""" Returns a list of collections from this library section. """
|
||||
|
@ -901,20 +925,9 @@ class MusicSection(LibrarySection):
|
|||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('genre',
|
||||
'country', 'collection')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'lastViewedAt', 'viewCount', 'titleSort')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'artist'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'year', 'track.userRating', 'artist.title',
|
||||
'artist.userRating', 'artist.genre', 'artist.country', 'artist.collection', 'artist.addedAt',
|
||||
'album.title', 'album.userRating', 'album.genre', 'album.decade', 'album.collection',
|
||||
'album.viewCount', 'album.lastViewedAt', 'album.studio', 'album.addedAt', 'track.title',
|
||||
'track.userRating', 'track.viewCount', 'track.lastViewedAt', 'track.skipCount',
|
||||
'track.lastSkippedAt')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
||||
|
@ -926,6 +939,11 @@ class MusicSection(LibrarySection):
|
|||
key = '/library/sections/%s/albums' % self.key
|
||||
return self.fetchItems(key)
|
||||
|
||||
def stations(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
|
||||
key = '/hubs/sections/%s?includeStations=1' % self.key
|
||||
return self.fetchItems(key, cls=Station)
|
||||
|
||||
def searchArtists(self, **kwargs):
|
||||
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='artist', **kwargs)
|
||||
|
@ -981,15 +999,9 @@ class PhotoSection(LibrarySection):
|
|||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('all', 'iso',
|
||||
'make', 'lens', 'aperture', 'exposure', 'device', 'resolution')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt')
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'photo'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device', 'resolution', 'place',
|
||||
'originallyAvailableAt', 'addedAt', 'title', 'userRating', 'tag', 'year')
|
||||
ALLOWED_SORT = ('addedAt',)
|
||||
TAG = 'Directory'
|
||||
TYPE = 'photo'
|
||||
CONTENT_TYPE = 'photo'
|
||||
|
@ -1124,6 +1136,25 @@ class Location(PlexObject):
|
|||
self.path = data.attrib.get('path')
|
||||
|
||||
|
||||
class Filter(PlexObject):
|
||||
""" Represents a single Filter.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'filter'
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'filter'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.filterType = data.attrib.get('filterType')
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Hub(PlexObject):
|
||||
""" Represents a single Hub (or category) in the PlexServer search.
|
||||
|
@ -1152,6 +1183,179 @@ class Hub(PlexObject):
|
|||
return self.size
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Station(PlexObject):
|
||||
""" Represents the Station area in the MusicSection.
|
||||
|
||||
Attributes:
|
||||
TITLE (str): 'Stations'
|
||||
TYPE (str): 'station'
|
||||
hubIdentifier (str): Unknown.
|
||||
size (int): Number of items found.
|
||||
title (str): Title of this Hub.
|
||||
type (str): Type of items in the Hub.
|
||||
more (str): Unknown.
|
||||
style (str): Unknown
|
||||
items (str): List of items in the Hub.
|
||||
"""
|
||||
TITLE = 'Stations'
|
||||
TYPE = 'station'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.hubIdentifier = data.attrib.get('hubIdentifier')
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.more = data.attrib.get('more')
|
||||
self.style = data.attrib.get('style')
|
||||
self.items = self.findItems(data)
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
|
||||
class Sort(PlexObject):
|
||||
""" Represents a Sort element found in library.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Sort'
|
||||
defaultDirection (str): Default sorting direction.
|
||||
descKey (str): Url key for sorting with desc.
|
||||
key (str): Url key for sorting,
|
||||
title (str): Title of sorting,
|
||||
firstCharacterKey (str): Url path for first character endpoint.
|
||||
"""
|
||||
TAG = 'Sort'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.defaultDirection = data.attrib.get('defaultDirection')
|
||||
self.descKey = data.attrib.get('descKey')
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
self.firstCharacterKey = data.attrib.get('firstCharacterKey')
|
||||
|
||||
|
||||
class FilterField(PlexObject):
|
||||
""" Represents a Filters Field element found in library.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Field'
|
||||
key (str): Url key for filter,
|
||||
title (str): Title of filter.
|
||||
type (str): Type of filter (string, boolean, integer, date, etc).
|
||||
subType (str): Subtype of filter (decade, rating, etc).
|
||||
operators (str): Operators available for this filter.
|
||||
"""
|
||||
TAG = 'Field'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.subType = data.attrib.get('subType')
|
||||
self.operators = []
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Operator(PlexObject):
|
||||
""" Represents an Operator available for filter.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Operator'
|
||||
key (str): Url key for operator.
|
||||
title (str): Title of operator.
|
||||
"""
|
||||
TAG = 'Operator'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
class Folder(PlexObject):
|
||||
""" Represents a Folder inside a library.
|
||||
|
||||
Attributes:
|
||||
key (str): Url key for folder.
|
||||
title (str): Title of folder.
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
|
||||
def subfolders(self):
|
||||
""" Returns a list of available `:class:`~plexapi.library.Folder` for this folder.
|
||||
Continue down subfolders until a mediaType is found.
|
||||
"""
|
||||
if self.key.startswith('/library/metadata'):
|
||||
return self.fetchItems(self.key)
|
||||
else:
|
||||
return self.fetchItems(self.key, Folder)
|
||||
|
||||
def allSubfolders(self):
|
||||
""" Returns a list of all available `:class:`~plexapi.library.Folder` for this folder.
|
||||
Only returns `:class:`~plexapi.library.Folder`.
|
||||
"""
|
||||
folders = []
|
||||
for folder in self.subfolders():
|
||||
if not folder.key.startswith('/library/metadata'):
|
||||
folders.append(folder)
|
||||
while True:
|
||||
for subfolder in folder.subfolders():
|
||||
if not subfolder.key.startswith('/library/metadata'):
|
||||
folders.append(subfolder)
|
||||
continue
|
||||
break
|
||||
return folders
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class FieldType(PlexObject):
|
||||
""" Represents a FieldType for filter.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Operator'
|
||||
type (str): Type of filter (string, boolean, integer, date, etc),
|
||||
operators (str): Operators available for this filter.
|
||||
"""
|
||||
TAG = 'FieldType'
|
||||
|
||||
def __repr__(self):
|
||||
_type = self._clean(self.firstAttr('type'))
|
||||
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, _type] if p])
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.type = data.attrib.get('type')
|
||||
self.operators = self.findItems(data, Operator)
|
||||
|
||||
|
||||
class FirstCharacter(PlexObject):
|
||||
""" Represents a First Character element from a library.
|
||||
|
||||
Attributes:
|
||||
key (str): Url key for character.
|
||||
size (str): Total amount of library items starting with this character.
|
||||
title (str): Character (#, !, A, B, C, ...).
|
||||
"""
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.key = data.attrib.get('key')
|
||||
self.size = data.attrib.get('size')
|
||||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collections(PlexPartialObject):
|
||||
""" Represents a single Collection.
|
||||
|
@ -1224,6 +1428,15 @@ class Collections(PlexPartialObject):
|
|||
def __len__(self):
|
||||
return self.childCount
|
||||
|
||||
def _preferences(self):
|
||||
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
|
||||
items = []
|
||||
data = self._server.query(self._details_key)
|
||||
for item in data.iter('Setting'):
|
||||
items.append(Setting(data=item, server=self._server))
|
||||
|
||||
return items
|
||||
|
||||
def delete(self):
|
||||
part = '/library/metadata/%s' % self.ratingKey
|
||||
return self._server.query(part, method=self._server._session.delete)
|
||||
|
|
|
@ -717,7 +717,6 @@ class Marker(PlexObject):
|
|||
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Field(PlexObject):
|
||||
""" Represents a single Field.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue