Skip to content

Commit b98d2e9

Browse files
authored
Merge pull request #498 from pkkid/intro_marker
intro_marker
2 parents 785a2f0 + 1d8d76e commit b98d2e9

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

plexapi/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ def analyze(self):
324324
Playing screen to show a graphical representation of where playback
325325
is. Video preview thumbnails creation is a CPU-intensive process akin
326326
to transcoding the file.
327+
* Generate intro video markers: Detects show intros, exposing the
328+
'Skip Intro' button in clients.
327329
"""
328330
key = '/%s/analyze' % self.key.lstrip('/')
329331
self._server.query(key, method=self._server._session.put)

plexapi/media.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,26 @@ def _loadData(self, data):
695695
self.end = cast(int, data.attrib.get('endTimeOffset'))
696696

697697

698+
@utils.registerPlexObject
699+
class Marker(PlexObject):
700+
""" Represents a single Marker media tag.
701+
702+
Attributes:
703+
TAG (str): 'Marker'
704+
"""
705+
TAG = 'Marker'
706+
707+
def _loadData(self, data):
708+
self._data = data
709+
self.filter = data.attrib.get('filter')
710+
self.type = data.attrib.get('type')
711+
_tag, _id = self.filter.split('=')
712+
self.tag = self.type + _tag.capitalize()
713+
self.id = _id
714+
self.start = cast(int, data.attrib.get('startTimeOffset'))
715+
self.end = cast(int, data.attrib.get('endTimeOffset'))
716+
717+
698718
@utils.registerPlexObject
699719
class Field(PlexObject):
700720
""" Represents a single Field.

plexapi/settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,15 @@ def set(self, value):
155155
def toUrl(self):
156156
"""Helper for urls"""
157157
return '%s=%s' % (self.id, self._value or self.value)
158+
159+
160+
@utils.registerPlexObject
161+
class Preferences(Setting):
162+
""" Represents a single Preferences.
163+
164+
Attributes:
165+
TAG (str): 'Preferences'
166+
FILTER (str): 'preferences'
167+
"""
168+
TAG = 'Preferences'
169+
FILTER = 'preferences'

plexapi/video.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
from urllib.parse import quote_plus, urlencode
44

5-
from plexapi import media, utils
5+
from plexapi import media, utils, settings, library
66
from plexapi.base import Playable, PlexPartialObject
77
from plexapi.exceptions import BadRequest, NotFound
88

@@ -390,6 +390,10 @@ class Show(Video):
390390
TYPE = 'show'
391391
METADATA_TYPE = 'episode'
392392

393+
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
394+
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
395+
'&includeMarkers=1&includeConcerts=1&includePreferences=1')
396+
393397
def __iter__(self):
394398
for season in self.seasons():
395399
yield season
@@ -399,6 +403,7 @@ def _loadData(self, data):
399403
Video._loadData(self, data)
400404
# fix key if loaded from search
401405
self.key = self.key.replace('/children', '')
406+
self._details_key = self.key + self._include
402407
self.art = data.attrib.get('art')
403408
self.banner = data.attrib.get('banner')
404409
self.childCount = utils.cast(int, data.attrib.get('childCount'))
@@ -431,6 +436,29 @@ def isWatched(self):
431436
""" Returns True if this show is fully watched. """
432437
return bool(self.viewedLeafCount == self.leafCount)
433438

439+
def preferences(self):
440+
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
441+
items = []
442+
data = self._server.query(self._details_key)
443+
for item in data.iter('Preferences'):
444+
for elem in item:
445+
items.append(settings.Preferences(data=elem, server=self._server))
446+
447+
return items
448+
449+
def hubs(self):
450+
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
451+
data = self._server.query(self._details_key)
452+
for item in data.iter('Related'):
453+
return self.findItems(item, library.Hub)
454+
455+
def onDeck(self):
456+
""" Returns shows On Deck :class:`~plexapi.video.Video` object.
457+
If show is unwatched, return will likely be the first episode.
458+
"""
459+
data = self._server.query(self._details_key)
460+
return self.findItems([item for item in data.iter('OnDeck')][0])[0]
461+
434462
def seasons(self, **kwargs):
435463
""" Returns a list of :class:`~plexapi.video.Season` objects. """
436464
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
@@ -645,7 +673,7 @@ class Episode(Playable, Video):
645673

646674
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
647675
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
648-
'&includeConcerts=1&includePreferences=1')
676+
'&includeMarkers=1&includeConcerts=1&includePreferences=1')
649677

650678
def _loadData(self, data):
651679
""" Load attribute values from Plex XML response. """
@@ -681,6 +709,7 @@ def _loadData(self, data):
681709
self.labels = self.findItems(data, media.Label)
682710
self.collections = self.findItems(data, media.Collection)
683711
self.chapters = self.findItems(data, media.Chapter)
712+
self.markers = self.findItems(data, media.Marker)
684713

685714
def __repr__(self):
686715
return '<%s>' % ':'.join([p for p in [
@@ -712,6 +741,13 @@ def seasonEpisode(self):
712741
""" Returns the s00e00 string containing the season and episode. """
713742
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
714743

744+
@property
745+
def hasIntroMarker(self):
746+
""" Returns True if this episode has an intro marker in the xml. """
747+
if not self.isFullObject():
748+
self.reload()
749+
return any(marker.type == 'intro' for marker in self.markers)
750+
715751
def season(self):
716752
"""" Return this episodes :func:`~plexapi.video.Season`.. """
717753
return self.fetchItem(self.parentKey)

0 commit comments

Comments
 (0)