474 lines
16 KiB
Python
474 lines
16 KiB
Python
import plexobjects
|
|
import media
|
|
import plexmedia
|
|
import plexstream
|
|
import exceptions
|
|
import compat
|
|
import plexlibrary
|
|
import util
|
|
|
|
|
|
class PlexVideoItemList(plexobjects.PlexItemList):
|
|
def __init__(self, data, initpath=None, server=None, container=None):
|
|
self._data = data
|
|
self._initpath = initpath
|
|
self._server = server
|
|
self._container = container
|
|
self._items = None
|
|
|
|
@property
|
|
def items(self):
|
|
if self._items is None:
|
|
if self._data is not None:
|
|
self._items = [plexobjects.buildItem(self._server, elem, self._initpath, container=self._container) for elem in self._data]
|
|
else:
|
|
self._items = []
|
|
|
|
return self._items
|
|
|
|
|
|
class Video(media.MediaItem):
|
|
TYPE = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._settings = None
|
|
media.MediaItem.__init__(self, *args, **kwargs)
|
|
|
|
def __eq__(self, other):
|
|
return other and self.ratingKey == other.ratingKey
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
@property
|
|
def settings(self):
|
|
if not self._settings:
|
|
import plexapp
|
|
self._settings = plexapp.PlayerSettingsInterface()
|
|
|
|
return self._settings
|
|
|
|
@settings.setter
|
|
def settings(self, value):
|
|
self._settings = value
|
|
|
|
def selectedAudioStream(self):
|
|
if self.audioStreams:
|
|
for stream in self.audioStreams:
|
|
if stream.isSelected():
|
|
return stream
|
|
return None
|
|
|
|
def selectedSubtitleStream(self):
|
|
if self.subtitleStreams:
|
|
for stream in self.subtitleStreams:
|
|
if stream.isSelected():
|
|
return stream
|
|
return None
|
|
|
|
def selectStream(self, stream, async=True):
|
|
self.mediaChoice.part.setSelectedStream(stream.streamType.asInt(), stream.id, async)
|
|
|
|
def isVideoItem(self):
|
|
return True
|
|
|
|
def _findStreams(self, streamtype):
|
|
idx = 0
|
|
streams = []
|
|
for media_ in self.media():
|
|
for part in media_.parts:
|
|
for stream in part.streams:
|
|
if stream.streamType.asInt() == streamtype:
|
|
stream.typeIndex = idx
|
|
streams.append(stream)
|
|
idx += 1
|
|
return streams
|
|
|
|
def analyze(self):
|
|
""" The primary purpose of media analysis is to gather information about that media
|
|
item. All of the media you add to a Library has properties that are useful to
|
|
know - whether it's a video file, a music track, or one of your photos.
|
|
"""
|
|
self.server.query('/%s/analyze' % self.key)
|
|
|
|
def markWatched(self):
|
|
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
|
self.server.query(path)
|
|
self.reload()
|
|
|
|
def markUnwatched(self):
|
|
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
|
self.server.query(path)
|
|
self.reload()
|
|
|
|
# def play(self, client):
|
|
# client.playMedia(self)
|
|
|
|
def refresh(self):
|
|
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
|
|
|
|
def _getStreamURL(self, **params):
|
|
if self.TYPE not in ('movie', 'episode', 'track'):
|
|
raise exceptions.Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
|
|
mvb = params.get('maxVideoBitrate')
|
|
vr = params.get('videoResolution')
|
|
|
|
# import plexapp
|
|
|
|
params = {
|
|
'path': self.key,
|
|
'offset': params.get('offset', 0),
|
|
'copyts': params.get('copyts', 1),
|
|
'protocol': params.get('protocol', 'hls'),
|
|
'mediaIndex': params.get('mediaIndex', 0),
|
|
'directStream': '1',
|
|
'directPlay': '0',
|
|
'X-Plex-Platform': params.get('platform', 'Chrome'),
|
|
# 'X-Plex-Platform': params.get('platform', plexapp.INTERFACE.getGlobal('platform')),
|
|
'maxVideoBitrate': max(mvb, 64) if mvb else None,
|
|
'videoResolution': '{0}x{1}'.format(*vr) if vr else None
|
|
}
|
|
|
|
final = {}
|
|
|
|
for k, v in params.items():
|
|
if v is not None: # remove None values
|
|
final[k] = v
|
|
|
|
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
|
|
server = self.getTranscodeServer(True, self.TYPE)
|
|
|
|
return server.buildUrl('/{0}/:/transcode/universal/start.m3u8?{1}'.format(streamtype, compat.urlencode(final)), includeToken=True)
|
|
# path = "/video/:/transcode/universal/" + command + "?session=" + AppSettings().GetGlobal("clientIdentifier")
|
|
|
|
def resolutionString(self):
|
|
res = self.media[0].videoResolution
|
|
if not res:
|
|
return ''
|
|
|
|
if res.isdigit():
|
|
return '{0}p'.format(self.media[0].videoResolution)
|
|
else:
|
|
return res.upper()
|
|
|
|
def audioCodecString(self):
|
|
codec = (self.media[0].audioCodec or '').lower()
|
|
|
|
if codec in ('dca', 'dca-ma', 'dts-hd', 'dts-es', 'dts-hra'):
|
|
codec = "DTS"
|
|
else:
|
|
codec = codec.upper()
|
|
|
|
return codec
|
|
|
|
def audioChannelsString(self, translate_func=util.dummyTranslate):
|
|
channels = self.media[0].audioChannels.asInt()
|
|
|
|
if channels == 1:
|
|
return translate_func("Mono")
|
|
elif channels == 2:
|
|
return translate_func("Stereo")
|
|
elif channels > 0:
|
|
return "{0}.1".format(channels - 1)
|
|
else:
|
|
return ""
|
|
|
|
def available(self):
|
|
return self.media()[0].isAccessible()
|
|
|
|
|
|
class PlayableVideo(Video):
|
|
TYPE = None
|
|
|
|
def _setData(self, data):
|
|
Video._setData(self, data)
|
|
if self.isFullObject():
|
|
self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self)
|
|
|
|
def reload(self, *args, **kwargs):
|
|
if not kwargs.get('_soft'):
|
|
if self.get('viewCount'):
|
|
del self.viewCount
|
|
if self.get('viewOffset'):
|
|
del self.viewOffset
|
|
Video.reload(self, *args, **kwargs)
|
|
return self
|
|
|
|
def postPlay(self, **params):
|
|
query = '/hubs/metadata/{0}/postplay'.format(self.ratingKey)
|
|
data = self.server.query(query, params=params)
|
|
container = plexobjects.PlexContainer(data, initpath=query, server=self.server, address=query)
|
|
|
|
hubs = {}
|
|
for elem in data:
|
|
hub = plexlibrary.Hub(elem, server=self.server, container=container)
|
|
hubs[hub.hubIdentifier] = hub
|
|
return hubs
|
|
|
|
|
|
@plexobjects.registerLibType
|
|
class Movie(PlayableVideo):
|
|
TYPE = 'movie'
|
|
|
|
def _setData(self, data):
|
|
PlayableVideo._setData(self, data)
|
|
if self.isFullObject():
|
|
self.collections = plexobjects.PlexItemList(data, media.Collection, media.Collection.TYPE, server=self.server)
|
|
self.countries = plexobjects.PlexItemList(data, media.Country, media.Country.TYPE, server=self.server)
|
|
self.directors = plexobjects.PlexItemList(data, media.Director, media.Director.TYPE, server=self.server)
|
|
self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server)
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
self.producers = plexobjects.PlexItemList(data, media.Producer, media.Producer.TYPE, server=self.server)
|
|
self.roles = plexobjects.PlexItemList(data, media.Role, media.Role.TYPE, server=self.server, container=self.container)
|
|
self.writers = plexobjects.PlexItemList(data, media.Writer, media.Writer.TYPE, server=self.server)
|
|
self.related = plexobjects.PlexItemList(data.find('Related'), plexlibrary.Hub, plexlibrary.Hub.TYPE, server=self.server, container=self)
|
|
else:
|
|
if data.find(media.Media.TYPE) is not None:
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
|
|
self._videoStreams = None
|
|
self._audioStreams = None
|
|
self._subtitleStreams = None
|
|
|
|
# data for active sessions
|
|
self.sessionKey = plexobjects.PlexValue(data.attrib.get('sessionKey', ''), self)
|
|
self.user = self._findUser(data)
|
|
self.player = self._findPlayer(data)
|
|
self.transcodeSession = self._findTranscodeSession(data)
|
|
|
|
@property
|
|
def maxHeight(self):
|
|
height = 0
|
|
for m in self.media:
|
|
if m.height.asInt() > height:
|
|
height = m.height.asInt()
|
|
return height
|
|
|
|
@property
|
|
def videoStreams(self):
|
|
if self._videoStreams is None:
|
|
self._videoStreams = self._findStreams(plexstream.PlexStream.TYPE_VIDEO)
|
|
return self._videoStreams
|
|
|
|
@property
|
|
def audioStreams(self):
|
|
if self._audioStreams is None:
|
|
self._audioStreams = self._findStreams(plexstream.PlexStream.TYPE_AUDIO)
|
|
return self._audioStreams
|
|
|
|
@property
|
|
def subtitleStreams(self):
|
|
if self._subtitleStreams is None:
|
|
self._subtitleStreams = self._findStreams(plexstream.PlexStream.TYPE_SUBTITLE)
|
|
return self._subtitleStreams
|
|
|
|
@property
|
|
def actors(self):
|
|
return self.roles
|
|
|
|
@property
|
|
def isWatched(self):
|
|
return self.get('viewCount').asInt() > 0
|
|
|
|
def getStreamURL(self, **params):
|
|
return self._getStreamURL(**params)
|
|
|
|
|
|
@plexobjects.registerLibType
|
|
class Show(Video):
|
|
TYPE = 'show'
|
|
|
|
def _setData(self, data):
|
|
Video._setData(self, data)
|
|
if self.isFullObject():
|
|
self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server)
|
|
self.roles = plexobjects.PlexItemList(data, media.Role, media.Role.TYPE, server=self.server, container=self.container)
|
|
self.related = plexobjects.PlexItemList(data.find('Related'), plexlibrary.Hub, plexlibrary.Hub.TYPE, server=self.server, container=self)
|
|
self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self)
|
|
|
|
@property
|
|
def unViewedLeafCount(self):
|
|
return self.leafCount.asInt() - self.viewedLeafCount.asInt()
|
|
|
|
@property
|
|
def isWatched(self):
|
|
return self.viewedLeafCount == self.leafCount
|
|
|
|
def seasons(self):
|
|
path = self.key
|
|
return plexobjects.listItems(self.server, path, Season.TYPE)
|
|
|
|
def season(self, title):
|
|
path = self.key
|
|
return plexobjects.findItem(self.server, path, title)
|
|
|
|
def episodes(self, watched=None):
|
|
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
|
return plexobjects.listItems(self.server, leavesKey, watched=watched)
|
|
|
|
def episode(self, title):
|
|
path = '/library/metadata/%s/allLeaves' % self.ratingKey
|
|
return plexobjects.findItem(self.server, path, title)
|
|
|
|
def all(self):
|
|
return self.episodes()
|
|
|
|
def watched(self):
|
|
return self.episodes(watched=True)
|
|
|
|
def unwatched(self):
|
|
return self.episodes(watched=False)
|
|
|
|
def refresh(self):
|
|
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
|
|
|
|
def sectionOnDeck(self):
|
|
query = '/library/sections/{0}/onDeck'.format(self.getLibrarySectionId())
|
|
return plexobjects.listItems(self.server, query)
|
|
|
|
|
|
@plexobjects.registerLibType
|
|
class Season(Video):
|
|
TYPE = 'season'
|
|
|
|
def _setData(self, data):
|
|
Video._setData(self, data)
|
|
if self.isFullObject():
|
|
self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self)
|
|
|
|
@property
|
|
def defaultTitle(self):
|
|
return self.parentTitle or self.title
|
|
|
|
@property
|
|
def unViewedLeafCount(self):
|
|
return self.leafCount.asInt() - self.viewedLeafCount.asInt()
|
|
|
|
@property
|
|
def isWatched(self):
|
|
return self.viewedLeafCount == self.leafCount
|
|
|
|
def episodes(self, watched=None):
|
|
path = self.key
|
|
return plexobjects.listItems(self.server, path, watched=watched)
|
|
|
|
def episode(self, title):
|
|
path = self.key
|
|
return plexobjects.findItem(self.server, path, title)
|
|
|
|
def all(self):
|
|
return self.episodes()
|
|
|
|
def show(self):
|
|
return plexobjects.listItems(self.server, self.parentKey)[0]
|
|
|
|
def watched(self):
|
|
return self.episodes(watched=True)
|
|
|
|
def unwatched(self):
|
|
return self.episodes(watched=False)
|
|
|
|
|
|
@plexobjects.registerLibType
|
|
class Episode(PlayableVideo):
|
|
TYPE = 'episode'
|
|
|
|
def init(self, data):
|
|
self._show = None
|
|
self._season = None
|
|
|
|
def _setData(self, data):
|
|
PlayableVideo._setData(self, data)
|
|
if self.isFullObject():
|
|
self.directors = plexobjects.PlexItemList(data, media.Director, media.Director.TYPE, server=self.server)
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
self.writers = plexobjects.PlexItemList(data, media.Writer, media.Writer.TYPE, server=self.server)
|
|
else:
|
|
if data.find(media.Media.TYPE) is not None:
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
|
|
self._videoStreams = None
|
|
self._audioStreams = None
|
|
self._subtitleStreams = None
|
|
|
|
# data for active sessions
|
|
self.sessionKey = plexobjects.PlexValue(data.attrib.get('sessionKey', ''), self)
|
|
self.user = self._findUser(data)
|
|
self.player = self._findPlayer(data)
|
|
self.transcodeSession = self._findTranscodeSession(data)
|
|
|
|
@property
|
|
def defaultTitle(self):
|
|
return self.grandparentTitle or self.parentTitle or self.title
|
|
|
|
@property
|
|
def defaultThumb(self):
|
|
return self.grandparentThumb or self.parentThumb or self.thumb
|
|
|
|
@property
|
|
def videoStreams(self):
|
|
if self._videoStreams is None:
|
|
self._videoStreams = self._findStreams(plexstream.PlexStream.TYPE_VIDEO)
|
|
return self._videoStreams
|
|
|
|
@property
|
|
def audioStreams(self):
|
|
if self._audioStreams is None:
|
|
self._audioStreams = self._findStreams(plexstream.PlexStream.TYPE_AUDIO)
|
|
return self._audioStreams
|
|
|
|
@property
|
|
def subtitleStreams(self):
|
|
if self._subtitleStreams is None:
|
|
self._subtitleStreams = self._findStreams(plexstream.PlexStream.TYPE_SUBTITLE)
|
|
return self._subtitleStreams
|
|
|
|
@property
|
|
def isWatched(self):
|
|
return self.get('viewCount').asInt() > 0
|
|
|
|
def getStreamURL(self, **params):
|
|
return self._getStreamURL(**params)
|
|
|
|
def season(self):
|
|
if not self._season:
|
|
self._season = plexobjects.listItems(self.server, self.parentKey)[0]
|
|
return self._season
|
|
|
|
def show(self):
|
|
if not self._show:
|
|
self._show = plexobjects.listItems(self.server, self.grandparentKey)[0]
|
|
return self._show
|
|
|
|
@property
|
|
def genres(self):
|
|
return self.show().genres
|
|
|
|
@property
|
|
def roles(self):
|
|
return self.show().roles
|
|
|
|
@property
|
|
def related(self):
|
|
self.show().reload(_soft=True, includeRelated=1, includeRelatedCount=10)
|
|
return self.show().related
|
|
|
|
|
|
@plexobjects.registerLibType
|
|
class Clip(PlayableVideo):
|
|
TYPE = 'clip'
|
|
|
|
def _setData(self, data):
|
|
PlayableVideo._setData(self, data)
|
|
if self.isFullObject():
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
else:
|
|
if data.find(media.Media.TYPE) is not None:
|
|
self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self)
|
|
|
|
@property
|
|
def isWatched(self):
|
|
return self.get('viewCount').asInt() > 0
|
|
|
|
def getStreamURL(self, **params):
|
|
return self._getStreamURL(**params)
|