diff --git a/plexapi/audio.py b/plexapi/audio.py index eefadecb..fbc18f30 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -91,9 +91,9 @@ class Artist(Audio): self.guid = data.attrib.get('guid') self.key = self.key.replace('/children', '') # FIX_BUG_50 self.location = utils.findLocations(data, single=True) - self.countries = self._buildSubitems(data, media.Country) - self.genres = self._buildSubitems(data, media.Genre) - self.similar = self._buildSubitems(data, media.Similar) + self.countries = self._buildItems(data, media.Country) + self.genres = self._buildItems(data, media.Genre) + self.similar = self._buildItems(data, media.Similar) def album(self, title): """ Returns the :class:`~plexapi.audio.Album` that matches the specified title. @@ -186,7 +186,7 @@ class Album(Audio): self.parentTitle = data.attrib.get('parentTitle') self.studio = data.attrib.get('studio') self.year = utils.cast(int, data.attrib.get('year')) - self.genres = self._buildSubitems(data, media.Genre) + self.genres = self._buildItems(data, media.Genre) def track(self, title): """ Returns the :class:`~plexapi.audio.Track` that matches the specified title. @@ -293,8 +293,8 @@ class Track(Audio, Playable): self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year')) - self.media = self._buildSubitems(data, media.Media) - self.moods = self._buildSubitems(data, media.Mood) + self.media = self._buildItems(data, media.Media) + self.moods = self._buildItems(data, media.Mood) # data for active sessions and history self.sessionKey = utils.cast(int, data.attrib.get('sessionKey')) self.username = utils.findUsername(data) diff --git a/plexapi/base.py b/plexapi/base.py index 93c73190..2772ba4b 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -135,17 +135,37 @@ class PlexObject(object): return cls(self._root, elem, initpath) raise UnknownType('Unknown library type: %s' % libtype) - def _buildSubitems(self, data, cls, tag=None, filters=None, *args): - """ Build and return a list of items (optionally filtered by tag). """ + def _buildItems(self, data, cls=None, tag=None, attrs=None, safe=False): + """ Build and return a list of items (optionally filtered by tag). + + Parameters: + data (ElementTree): XML data to search for items. + cls (PlexObject): Optionally specify the PlexObject to be built. If not specified + _buildItem will be called and the best guess item will be built. + tag (str): Only build items with the specified tag. If not specified and + cls is specified, tag will be set to cls.TYPE. + attrs (dict): Dict containing attributes to filter the elements by. If not + specified, all elements will be considered. + safe (bool): If True, dont raise an exception when unable to build an object. + """ items = [] - tag = tag or cls.TYPE - filters = filters or {} + tag = cls.TYPE if not tag and cls else tag + attrs = attrs or {} for elem in data: - if elem.tag == tag: - for attr, value in filters.items(): - if elem.attrib.get(attr) != str(value): - continue - items.append(cls(self._root, elem, self._initpath, *args)) + try: + if not tag or elem.tag == tag: + for attr, value in attrs.items(): + if elem.attrib.get(attr) != str(value): + continue + if cls is not None: + items.append(cls(self._root, elem, self._initpath)) + else: + items.append(self._buildItem(elem, self._initpath)) + except Exception as err: + if safe: + log.warn('Failed to build %s (type=%s); %s' % (elem.tag, elem.attrib.get('type', 'NA'), err)) + continue + raise return items def _fetchItem(self, key, title=None, name=None): diff --git a/plexapi/library.py b/plexapi/library.py index cf408d27..3b38ff04 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import logging from plexapi import X_PLEX_CONTAINER_SIZE, log, utils from plexapi.base import PlexObject from plexapi.compat import unquote @@ -81,11 +80,11 @@ class Library(PlexObject): def onDeck(self): """ Returns a list of all media items on deck. """ - return utils.listItems(self.server, '/library/onDeck') + return self._fetchItems('/library/onDeck') def recentlyAdded(self): """ Returns a list of all media items recently added. """ - return utils.listItems(self.server, '/library/recentlyAdded') + return self._fetchItems('/library/recentlyAdded') def get(self, title): # this should use hub search when its merged """ Return the first item from all items with the specified title. @@ -121,8 +120,8 @@ class Library(PlexObject): args['type'] = utils.searchType(libtype) for attr, value in kwargs.items(): args[attr] = value - query = '/library/all%s' % utils.joinArgs(args) - return utils.listItems(self.server, query) + key = '/library/all%s' % utils.joinArgs(args) + return self._fetchItems(key) def cleanBundles(self): """ Poster images and other metadata for items in your library are kept in "bundle" @@ -475,23 +474,17 @@ class PhotoSection(LibrarySection): @utils.register_libtype -class Hub(object): +class Hub(PlexObject): TYPE = 'Hub' - HUBTYPES = {'genre':Genre, 'director':Director, 'actor':Role} + FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role} - def __init__(self, server, data, initpath): + def _loadData(self, data): self._data = data - self.server = server - self.initpath = initpath 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') - if self.type in self.HUBTYPES: - mediacls = self.HUBTYPES[self.type] - self.items = [mediacls(self.server, elem) for elem in data] - else: - self.items = self._safe_builditems(data) + self.items = self._buildItems(data) def __repr__(self): return '' % self.title.encode('utf8') @@ -499,14 +492,11 @@ class Hub(object): def __len__(self): return self.size - def _safe_builditems(self, data): - items = [] - for elem in data: - try: - items.append(utils.buildItem(self.server, elem, '/hubs')) - except Exception as err: - logging.warn('Failed %s to build %s; Error: %s' % (self.type, self.title, err)) - return items + def _buildItems(self, data): + if self.type in self.FILTERTYPES: + cls = self.FILTERTYPES[self.type] + return [cls(self._root, elem, self._initpath) for elem in data] + return super(Hub, self)._buildItems(data, safe=True) @utils.register_libtype diff --git a/plexapi/media.py b/plexapi/media.py index b4c04b33..886b1495 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -52,7 +52,7 @@ class Media(PlexObject): self.videoFrameRate = data.attrib.get('videoFrameRate') self.videoResolution = data.attrib.get('videoResolution') self.width = cast(int, data.attrib.get('width')) - self.parts = self._buildSubitems(data, MediaPart) + self.parts = self._buildItems(data, MediaPart) def __repr__(self): title = self.video.title.replace(' ','.')[0:20] @@ -84,13 +84,31 @@ class MediaPart(PlexObject): self.id = cast(int, data.attrib.get('id')) self.key = data.attrib.get('key') self.size = cast(int, data.attrib.get('size')) - self.videoStreams = self._buildSubitems(data, VideoStream, 'Stream', {'streamType':VideoStream.STREAMTYPE}) - self.audioStreams = self._buildSubitems(data, AudioStream, 'Stream', {'streamType':AudioStream.STREAMTYPE}) - self.subtitleStreams = self._buildSubitems(data, SubtitleStream, 'Stream', {'streamType':SubtitleStream.STREAMTYPE}) - + self.streams = self._buildStreams(data) + def __repr__(self): return '<%s:%s>' % (self.__class__.__name__, self.id) + def _buildStreams(self, data): + streams = [] + for elem in data: + for cls in (VideoStream, AudioStream, SubtitleStream): + if elem.attrib.get('streamType') == str(cls.STREAMTYPE): + streams.append(cls(self._root, elem, self._initpath)) + return streams + + @property + def videoStreams(self): + return [s for s in self.streams if s.streamType == VideoStream.STREAMTYPE] + + @property + def audioStreams(self): + return [s for s in self.streams if s.streamType == AudioStream.STREAMTYPE] + + @property + def subtitleStreams(self): + return [s for s in self.streams if s.streamType == SubtitleStream.STREAMTYPE] + class MediaPartStream(PlexObject): """ Base class for media streams. These consist of video, audio and subtitles. diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 830e127b..13f9e19d 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -252,7 +252,7 @@ class MyPlexResource(PlexObject): self.home = utils.cast(bool, data.attrib.get('home')) self.synced = utils.cast(bool, data.attrib.get('synced')) self.presence = utils.cast(bool, data.attrib.get('presence')) - self.connections = self._buildSubitems(data, ResourceConnection) + self.connections = self._buildItems(data, ResourceConnection) def __repr__(self): return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8')) diff --git a/plexapi/photo.py b/plexapi/photo.py index a8f3d2cc..1f171fbb 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -110,7 +110,7 @@ class Photo(PlexPartialObject): self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.year = utils.cast(int, data.attrib.get('year')) - self.media = self._buildSubitems(data, media.Media) + self.media = self._buildItems(data, media.Media) def photoalbum(self): """ Return this photo's :class:`~plexapi.photo.Photoalbum`. """ diff --git a/plexapi/server.py b/plexapi/server.py index 3a1e7986..914e4b55 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -279,8 +279,8 @@ class PlexServer(PlexObject): params['section'] = utils.SEARCHTYPES[mediatype] if limit: params['limit'] = limit - url = '/hubs/search?%s' % urlencode(params) - for hub in utils.listItems(self, url, bytag=True): + key = '/hubs/search?%s' % urlencode(params) + for hub in self._fetchItems(key, bytag=True): results += hub.items return results diff --git a/plexapi/video.py b/plexapi/video.py index 54a25d2e..1163bd7a 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -92,15 +92,15 @@ class Movie(Video, Playable): self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year')) - self.collections = self._buildSubitems(data, media.Collection) - self.countries = self._buildSubitems(data, media.Country) - self.directors = self._buildSubitems(data, media.Director) - self.fields = self._buildSubitems(data, media.Field) - self.genres = self._buildSubitems(data, media.Genre) - self.media = self._buildSubitems(data, media.Media) - self.producers = self._buildSubitems(data, media.Producer) - self.roles = self._buildSubitems(data, media.Role) - self.writers = self._buildSubitems(data, media.Writer) + self.collections = self._buildItems(data, media.Collection) + self.countries = self._buildItems(data, media.Country) + self.directors = self._buildItems(data, media.Director) + self.fields = self._buildItems(data, media.Field) + self.genres = self._buildItems(data, media.Genre) + self.media = self._buildItems(data, media.Media) + self.producers = self._buildItems(data, media.Producer) + self.roles = self._buildItems(data, media.Role) + self.writers = self._buildItems(data, media.Writer) # self.videoStreams = utils.findStreams(self.media, 'videostream') # these dont go here # self.audioStreams = utils.findStreams(self.media, 'audiostream') # these dont go here # self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') # these dont go here @@ -167,8 +167,8 @@ class Show(Video): self.theme = data.attrib.get('theme') self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) - self.genres = self._buildSubitems(data, media.Genre) - self.roles = self._buildSubitems(data, media.Role) + self.genres = self._buildItems(data, media.Genre) + self.roles = self._buildItems(data, media.Role) @property def actors(self): @@ -417,12 +417,9 @@ class Episode(Video, Playable): self.rating = utils.cast(float, data.attrib.get('rating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.year = utils.cast(int, data.attrib.get('year')) - self.directors = self._buildSubitems(data, media.Director) - self.media = self._buildSubitems(data, media.Media) - self.writers = self._buildSubitems(data, media.Writer) - # self.videoStreams = utils.findStreams(self.media, 'videostream') - # self.audioStreams = utils.findStreams(self.media, 'audiostream') - # self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') + self.directors = self._buildItems(data, media.Director) + self.media = self._buildItems(data, media.Media) + self.writers = self._buildItems(data, media.Writer) # data for active sessions and history self.sessionKey = utils.cast(int, data.attrib.get('sessionKey')) self.username = utils.findUsername(data) diff --git a/tools/plexattrs.py b/tools/plexattrs.py index 5e233719..2d5edf46 100755 --- a/tools/plexattrs.py +++ b/tools/plexattrs.py @@ -48,6 +48,11 @@ DONT_RELOAD = ( 'client.PlexClient', # we dont have the token to reload. #'server.PlexServer', # setting version to None? :( ) +TAGATTRS = { + 'Media': 'media', + 'Country': 'countries', + +} STOP_RECURSING_AT = ( #'media.MediaPart', ) @@ -64,16 +69,17 @@ class PlexAttributes(): def run(self): starttime = time.time() - # self._parse_myplex() - # self._parse_server() - # self._parse_library() - # self._parse_audio() - # self._parse_photo() - # self._parse_movie() - # self._parse_show() - # self._parse_client() - # self._parse_playlist() - # self._parse_sync() + self._parse_myplex() + self._parse_server() + self._parse_search() + self._parse_library() + self._parse_audio() + self._parse_photo() + self._parse_movie() + self._parse_show() + self._parse_client() + self._parse_playlist() + self._parse_sync() self.runtime = round((time.time() - starttime) / 60.0, 1) return self @@ -88,21 +94,24 @@ class PlexAttributes(): def _parse_server(self): self._load_attrs(self.plex, 'serv') self._load_attrs(self.plex.account(), 'serv') - self._load_attrs(self.plex.history()[:20], 'hist') - # self._load_attrs(self.plex.playlists()) - # for search in ('cre', 'ani', 'mik', 'she'): - # self._load_attrs(self.plex.search('cre')) - # self._load_attrs(self.plex.sessions(), 'sess') + self._load_attrs(self.plex.history()[:50], 'hist') + self._load_attrs(self.plex.history()[50:], 'hist') + self._load_attrs(self.plex.sessions(), 'sess') + + def _parse_search(self): + for search in ('cre', 'ani', 'mik', 'she', 'bea'): + self._load_attrs(self.plex.search(search), 'hub') def _parse_library(self): cat = 'lib' self._load_attrs(self.plex.library, cat) - # self._load_attrs(self.plex.library.sections()) - # self._load_attrs(self.plex.library.all()[:20]) - # self._load_attrs(self.plex.library.onDeck()[:20]) - # self._load_attrs(self.plex.library.recentlyAdded()[:20]) - # for search in ('cat', 'dog', 'rat'): - # self._load_attrs(self.plex.library.search(search)[:20]) + #self._load_attrs(self.plex.library.all()[:50], 'all') + self._load_attrs(self.plex.library.onDeck()[:50], 'deck') + self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add') + for search in ('cat', 'dog', 'rat', 'gir', 'mou'): + self._load_attrs(self.plex.library.search(search)[:50], 'srch') + # TODO: Implement section search (remove library search?) + # TODO: Implement section search filters def _parse_audio(self): cat = 'lib' @@ -142,7 +151,7 @@ class PlexAttributes(): for show in showsection.all(): self._load_attrs(show, cat) for season in show.seasons(): - self._load_attrs(show, cat) + self._load_attrs(season, cat) for episode in season.episodes(): self._load_attrs(episode, cat) @@ -200,6 +209,9 @@ class PlexAttributes(): if cat: categories[attr].add(cat) if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) + for subelem in elem: + attrname = TAGATTRS.get(subelem.tag, '%ss' % subelem.tag.lower()) + attrs['%s[]' % attrname] += 1 def _load_obj_attrs(self, clsname, obj, attrs): if clsname in STOP_RECURSING_AT: return None