diff --git a/plexapi/base.py b/plexapi/base.py index a2cc06af4..101a0b435 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -324,6 +324,8 @@ def analyze(self): Playing screen to show a graphical representation of where playback is. Video preview thumbnails creation is a CPU-intensive process akin to transcoding the file. + * Generate intro video markers: Detects show intros, exposing the + 'Skip Intro' button in clients. """ key = '/%s/analyze' % self.key.lstrip('/') self._server.query(key, method=self._server._session.put) diff --git a/plexapi/media.py b/plexapi/media.py index 44bc9fa40..65895c3e3 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -695,6 +695,26 @@ def _loadData(self, data): self.end = cast(int, data.attrib.get('endTimeOffset')) +@utils.registerPlexObject +class Marker(PlexObject): + """ Represents a single Marker media tag. + + Attributes: + TAG (str): 'Marker' + """ + TAG = 'Marker' + + def _loadData(self, data): + self._data = data + self.filter = data.attrib.get('filter') + self.type = data.attrib.get('type') + _tag, _id = self.filter.split('=') + self.tag = self.type + _tag.capitalize() + self.id = _id + self.start = cast(int, data.attrib.get('startTimeOffset')) + self.end = cast(int, data.attrib.get('endTimeOffset')) + + @utils.registerPlexObject class Field(PlexObject): """ Represents a single Field. diff --git a/plexapi/settings.py b/plexapi/settings.py index 7d804b92b..88b8e4f68 100644 --- a/plexapi/settings.py +++ b/plexapi/settings.py @@ -155,3 +155,15 @@ def set(self, value): def toUrl(self): """Helper for urls""" return '%s=%s' % (self.id, self._value or self.value) + + +@utils.registerPlexObject +class Preferences(Setting): + """ Represents a single Preferences. + + Attributes: + TAG (str): 'Preferences' + FILTER (str): 'preferences' + """ + TAG = 'Preferences' + FILTER = 'preferences' diff --git a/plexapi/video.py b/plexapi/video.py index f7d8d49fb..5396d87fa 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -2,7 +2,7 @@ import os from urllib.parse import quote_plus, urlencode -from plexapi import media, utils +from plexapi import media, utils, settings, library from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound @@ -390,6 +390,10 @@ class Show(Video): TYPE = 'show' METADATA_TYPE = 'episode' + _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' + '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' + '&includeMarkers=1&includeConcerts=1&includePreferences=1') + def __iter__(self): for season in self.seasons(): yield season @@ -399,6 +403,7 @@ def _loadData(self, data): Video._loadData(self, data) # fix key if loaded from search self.key = self.key.replace('/children', '') + self._details_key = self.key + self._include self.art = data.attrib.get('art') self.banner = data.attrib.get('banner') self.childCount = utils.cast(int, data.attrib.get('childCount')) @@ -431,6 +436,29 @@ def isWatched(self): """ Returns True if this show is fully watched. """ return bool(self.viewedLeafCount == self.leafCount) + 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('Preferences'): + for elem in item: + items.append(settings.Preferences(data=elem, server=self._server)) + + return items + + def hubs(self): + """ Returns a list of :class:`~plexapi.library.Hub` objects. """ + data = self._server.query(self._details_key) + for item in data.iter('Related'): + return self.findItems(item, library.Hub) + + def onDeck(self): + """ Returns shows On Deck :class:`~plexapi.video.Video` object. + If show is unwatched, return will likely be the first episode. + """ + data = self._server.query(self._details_key) + return self.findItems([item for item in data.iter('OnDeck')][0])[0] + def seasons(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Season` objects. """ key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey @@ -645,7 +673,7 @@ class Episode(Playable, Video): _include = ('?checkFiles=1&includeExtras=1&includeRelated=1' '&includeOnDeck=1&includeChapters=1&includePopularLeaves=1' - '&includeConcerts=1&includePreferences=1') + '&includeMarkers=1&includeConcerts=1&includePreferences=1') def _loadData(self, data): """ Load attribute values from Plex XML response. """ @@ -681,6 +709,7 @@ def _loadData(self, data): self.labels = self.findItems(data, media.Label) self.collections = self.findItems(data, media.Collection) self.chapters = self.findItems(data, media.Chapter) + self.markers = self.findItems(data, media.Marker) def __repr__(self): return '<%s>' % ':'.join([p for p in [ @@ -712,6 +741,13 @@ def seasonEpisode(self): """ Returns the s00e00 string containing the season and episode. """ return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2)) + @property + def hasIntroMarker(self): + """ Returns True if this episode has an intro marker in the xml. """ + if not self.isFullObject(): + self.reload() + return any(marker.type == 'intro' for marker in self.markers) + def season(self): """" Return this episodes :func:`~plexapi.video.Season`.. """ return self.fetchItem(self.parentKey)