diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 2b1f6f03..2b5c375d 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -41,7 +41,6 @@ import xbmc import struct import time -import urllib import urllib2 import httplib import socket @@ -52,6 +51,7 @@ import Queue import traceback import re +import json try: import xml.etree.cElementTree as etree @@ -87,6 +87,12 @@ class PlexAPI(): self.deviceName = client.getDeviceName() self.plexversion = client.getVersion() self.platform = client.getPlatform() + self.userId = utils.window('emby_currUser') + self.token = utils.window('emby_accessToken%s' % self.userId) + self.server = utils.window('emby_server%s' % self.userId) + self.plexLogin = utils.settings('plexLogin') + self.plexToken = utils.settings('plexToken') + self.machineIdentifier = utils.window('plex_machineIdentifier') self.doUtils = downloadutils.DownloadUtils() @@ -167,6 +173,7 @@ class PlexAPI(): headerOptions={'X-Plex-Token': token} ) self.logMsg("Response was: %s" % r, 2) + # List of exception returns, when connection failed exceptionlist = [ '', 401 @@ -596,22 +603,30 @@ class PlexAPI(): requests. An authentication option is NOT yet added. Inputs: + JSON=True will enforce a JSON answer options: dictionary of options that will override the standard header options otherwise set. - JSON=True will enforce a JSON answer, never mind any options Output: header dictionary """ # Get addon infos - xargs = dict() - xargs['User-agent'] = self.addonName - xargs['X-Plex-Device'] = self.deviceName - # xargs['X-Plex-Model'] = '' - xargs['X-Plex-Platform'] = self.platform - xargs['X-Plex-Client-Platform'] = self.platform - xargs['X-Plex-Product'] = self.addonName - xargs['X-Plex-Version'] = self.plexversion - xargs['X-Plex-Client-Identifier'] = self.clientId + xargs = { + 'User-agent': self.addonName, + 'X-Plex-Device': self.deviceName, + 'X-Plex-Platform': self.platform, + 'X-Plex-Client-Platform': self.platform, + 'X-Plex-Product': self.addonName, + 'X-Plex-Version': self.plexversion, + 'X-Plex-Client-Identifier': self.clientId, + 'machineIdentifier': self.machineIdentifier, + 'Accept': 'application/xml' + } + + try: + xargs['X-Plex-Token'] = self.token + except NameError: + # no token needed/saved yet + pass if JSON: xargs['Accept'] = 'application/json' if options: @@ -821,31 +836,6 @@ class PlexAPI(): dprint(__name__, 1, "====== MyPlex sign out XML finished ======") dprint(__name__, 0, 'MyPlex Sign Out done') - def UserAccessRestricted(self, username): - """ - Returns True if the user's access is restricted (parental restrictions) - False otherwise. - - Returns False also if access cannot be checked because plex.tv cannot - be reached. - """ - plexToken = utils.settings('plexToken') - users = self.MyPlexListHomeUsers(plexToken) - # If an error is encountered, set to False - if not users: - self.logMsg("Could not check user access restrictions.", 1) - self.logMsg("Setting restrictions to False.", 1) - return False - for user in users: - if username in user['title']: - restricted = user['restricted'] - if restricted == '1': - restricted = True - else: - restricted = False - self.logMsg("Successfully checked user parental access for %s: restricted access is set to %s" % (username, restricted), 1) - return restricted - def GetUserArtworkURL(self, username): """ Returns the URL for the user's Avatar. Or False if something went @@ -877,8 +867,8 @@ class PlexAPI(): Will return empty strings if failed. """ string = self.__language__ - plexToken = utils.settings('plexToken') - plexLogin = utils.settings('plexLogin') + plexLogin = self.plexLogin + plexToken = self.plexToken self.logMsg("Getting user list.", 1) # Get list of Plex home users users = self.MyPlexListHomeUsers(plexToken) @@ -1021,54 +1011,6 @@ class PlexAPI(): users.append(user.attrib) return users - def getTranscodeVideoPath(self, path, AuthToken, options, action, quality, subtitle, audio, partIndex): - """ - Transcode Video support - - parameters: - path - AuthToken - options - dict() of PlexConnect-options as received from aTV - action - transcoder action: Auto, Directplay, Transcode - quality - (resolution, quality, bitrate) - subtitle - {'selected', 'dontBurnIn', 'size'} - audio - {'boost'} - result: - final path to pull in PMS transcoder - """ - UDID = options['PlexConnectUDID'] - - transcodePath = '/video/:/transcode/universal/start.m3u8?' - - vRes = quality[0] - vQ = quality[1] - mVB = quality[2] - dprint(__name__, 1, "Setting transcode quality Res:{0} Q:{1} {2}Mbps", vRes, vQ, mVB) - dprint(__name__, 1, "Subtitle: selected {0}, dontBurnIn {1}, size {2}", subtitle['selected'], subtitle['dontBurnIn'], subtitle['size']) - dprint(__name__, 1, "Audio: boost {0}", audio['boost']) - - args = dict() - args['session'] = UDID - args['protocol'] = 'hls' - args['videoResolution'] = vRes - args['maxVideoBitrate'] = mVB - args['videoQuality'] = vQ - args['directStream'] = '0' if action=='Transcode' else '1' - # 'directPlay' - handled by the client in MEDIARUL() - args['subtitleSize'] = subtitle['size'] - args['skipSubtitles'] = subtitle['dontBurnIn'] #'1' # shut off PMS subtitles. Todo: skip only for aTV native/SRT (or other supported) - args['audioBoost'] = audio['boost'] - args['fastSeek'] = '1' - args['path'] = path - args['partIndex'] = partIndex - - xargs = getXArgsDeviceInfo(options) - xargs['X-Plex-Client-Capabilities'] = "protocols=http-live-streaming,http-mp4-streaming,http-streaming-video,http-streaming-video-720p,http-mp4-video,http-mp4-video-720p;videoDecoders=h264{profile:high&resolution:1080&level:41};audioDecoders=mp3,aac{bitrate:160000}" - if not AuthToken=='': - xargs['X-Plex-Token'] = AuthToken - - return transcodePath + urlencode(args) + '&' + urlencode(xargs) - def getDirectVideoPath(self, key, AuthToken): """ Direct Video Play support @@ -1309,7 +1251,7 @@ class PlexAPI(): 'includePopularLeaves': 1, 'includeConcerts': 1 } - url = url + '?' + urllib.urlencode(arguments) + url = url + '?' + urlencode(arguments) headerOptions = {'Accept': 'application/xml'} xml = self.doUtils.downloadUrl(url, headerOptions=headerOptions) if not xml: @@ -1324,6 +1266,7 @@ class API(): self.item = item self.clientinfo = clientinfo.ClientInfo() self.addonName = self.clientinfo.getAddonName() + self.clientId = self.clientinfo.getDeviceId() self.userId = utils.window('emby_currUser') self.server = utils.window('emby_server%s' % self.userId) self.token = utils.window('emby_accessToken%s' % self.userId) @@ -1771,7 +1714,7 @@ class API(): token = {'X-Plex-Token': self.token} xargs = PlexAPI().getXArgsDeviceInfo(options=token) xargs.update(arguments) - url = "%s?%s" % (url, urllib.urlencode(xargs)) + url = "%s?%s" % (url, urlencode(xargs)) return url def getMediaStreams(self): @@ -1939,5 +1882,141 @@ class API(): "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" % (server, parentId, maxWidth, maxHeight, parentTag, customquery)) allartworks['Primary'] = artwork - return allartworks + + def getTranscodeVideoPath(self, action, quality={}, subtitle={}, audioboost=None, partIndex=None, options={}): + """ + Transcode Video support + + Input: + action 'Transcode' OR any other string + quality: {'videoResolution': 'resolution', + 'videoQuality': 'quality', + 'maxVideoBitrate': 'bitrate'} + subtitle {'selected', 'dontBurnIn', 'size'} + audioboost Guess this takes an int as str + partIndex No idea + options dict() of PlexConnect-options as received from aTV + Output: + final path to pull in PMS transcoder + """ + # path to item + transcodePath = self.server + \ + '/video/:/transcode/universal/start.m3u8?' + + ID = self.getKey() + path = self.server + '/library/metadata/' + ID + args = { + 'session': self.clientId, + 'protocol': 'hls', + 'fastSeek': '1', + 'path': path, + 'X-Plex-Client-Capabilities': "protocols=http-live-streaming," + "http-streaming-video," + "http-streaming-video-720p," + "http-streaming-video-1080p," + "http-mp4-streaming," + "http-mp4-video," + "http-mp4-video-720p," + "http-mp4-video-1080p;" + "videoDecoders=" + "h264{profile:high&resolution:1080&level:51};" + "audioDecoders=" + "mp3," + "aac{bitrate:160000}," + "ac3{channels:6}," + "dts{channels:6}" + # 'partIndex': partIndex What do we do with this?!? + } + # All the settings + if subtitle: + args_update = { + 'subtitleSize': subtitle['size'], + 'skipSubtitles': subtitle['dontBurnIn'] # '1': shut off PMS + } + args.update(args_update) + self.logMsg( + "Subtitle: selected %s, dontBurnIn %s, size %s" + % (subtitle['selected'], subtitle['dontBurnIn'], + subtitle['size']), + 1 + ) + if audioboost: + args_update = { + 'audioBoost': audioboost + } + args.update(args_update) + self.logMsg("audioboost: %s" % audioboost, 1) + if action == 'Transcode': + # Possible Plex settings: + # 'videoResolution': vRes, + # 'maxVideoBitrate': mVB, + # : vQ + self.logMsg("Setting transcode quality to: %s" % quality, 1) + args['directStream'] = '0' + args.update(quality) + else: + args['directStream'] = '1' + + xargs = PlexAPI().getXArgsDeviceInfo(options=options) + return transcodePath + urlencode(args) + '&' + urlencode(xargs) + + def adjustResume(self, resume_seconds): + + resume = 0 + if resume_seconds: + resume = round(float(resume_seconds), 6) + jumpback = int(utils.settings('resumeJumpBack')) + if resume > jumpback: + # To avoid negative bookmark + resume = resume - jumpback + + return resume + + def returnParts(self): + """ + TODO + """ + item = self.item + PartCount = 0 + # Run through parts + for child in item[0][0]: + if child.tag == 'Part': + PartCount += PartCount + + def externalSubs(self, playurl): + + externalsubs = [] + mapping = {} + + item = self.item + itemid = self.getKey() + try: + mediastreams = item[0][0][0] + except (TypeError, KeyError, IndexError): + return + + kodiindex = 0 + for stream in mediastreams: + + # index = stream['Index'] + index = stream.attrib['id'] + # Since Emby returns all possible tracks together, have to pull only external subtitles. + # IsTextSubtitleStream if true, is available to download from emby. + if (stream.attrib['streamType'] == "3" and + stream.attrib['format']): + + # Direct stream + # PLEX: TODO!! + url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (self.server, itemid, itemid, index)) + + # map external subtitles for mapping + mapping[kodiindex] = index + externalsubs.append(url) + kodiindex += 1 + + mapping = json.dumps(mapping) + utils.window('emby_%s.indexMapping' % playurl, value=mapping) + + return externalsubs diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 7c1bc090..cb2e2b31 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -25,13 +25,14 @@ import playbackutils as pbutils import playutils import api +import PlexAPI + ################################################################################################# def doPlayback(itemid, dbid): - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemid) + item = PlexAPI.PlexAPI().GetPlexMetadata(itemid) # Now xml, not json! pbutils.PlaybackUtils(item).play(itemid, dbid) ##### DO RESET AUTH ##### diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9e9f7a40..7c37a7b9 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -360,7 +360,7 @@ class Movies(Items): utils.window('emby_pathverified', value="true") else: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.emby.movies/" params = { 'filename': filename.encode('utf-8'), @@ -704,7 +704,7 @@ class HomeVideos(Items): utils.window('emby_pathverified', value="true") else: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.emby.movies/" params = { 'filename': filename.encode('utf-8'), diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index affa2b81..da6b4c9d 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -18,6 +18,8 @@ import playlist import read_embyserver as embyserver import utils +import PlexAPI + ################################################################################################# @@ -27,7 +29,7 @@ class PlaybackUtils(): def __init__(self, item): self.item = item - self.API = api.API(self.item) + self.API = PlexAPI.API(self.item) self.clientInfo = clientinfo.ClientInfo() self.addonName = self.clientInfo.getAddonName() @@ -107,8 +109,9 @@ class PlaybackUtils(): currentPosition += 1 ############### -- CHECK FOR INTROS ################ - - if utils.settings('enableCinema') == "true" and not seektime: + # PLEX: todo. Seems like Plex returns a playlist WITH trailers + # if utils.settings('enableCinema') == "true" and not seektime: + if False: # if we have any play them when the movie/show is not being resumed url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid intros = doUtils.downloadUrl(url) @@ -145,14 +148,17 @@ class PlaybackUtils(): # Extend our current playlist with the actual item to play # only if there's no playlist first self.logMsg("Adding main item to playlist.", 1) - self.pl.addtoPlaylist(dbid, item['Type'].lower()) + # self.pl.addtoPlaylist(dbid, item['Type'].lower()) + self.pl.addtoPlaylist(dbid, item[0].attrib['type'].lower()) # Ensure that additional parts are played after the main item currentPosition += 1 ############### -- CHECK FOR ADDITIONAL PARTS ################ - if item.get('PartCount'): + # Plex: TODO. Guess parts are sent back like trailers. + # if item.get('PartCount'): + if False: # Only add to the playlist after intros have played partcount = item['PartCount'] url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid @@ -215,11 +221,14 @@ class PlaybackUtils(): def setProperties(self, playurl, listitem): # Set all properties necessary for plugin path playback item = self.item - itemid = item['Id'] - itemtype = item['Type'] + # itemid = item['Id'] + itemid = self.API.getKey() + # itemtype = item['Type'] + itemtype = item[0].attrib['type'] + resume, runtime = self.API.getRuntime() embyitem = "emby_%s" % playurl - utils.window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks'))) + utils.window('%s.runtime' % embyitem, value=str(runtime)) utils.window('%s.type' % embyitem, value=itemtype) utils.window('%s.itemid' % embyitem, value=itemid) @@ -231,7 +240,8 @@ class PlaybackUtils(): # Append external subtitles to stream playmethod = utils.window('%s.playmethod' % embyitem) # Only for direct play and direct stream - subtitles = self.externalSubs(playurl) + # subtitles = self.externalSubs(playurl) + subtitles = self.API.externalSubs(playurl) if playmethod in ("DirectStream", "Transcode"): # Direct play automatically appends external listitem.setSubtitles(subtitles) @@ -278,7 +288,8 @@ class PlaybackUtils(): item = self.item artwork = self.artwork - allartwork = artwork.getAllArtwork(item, parentInfo=True) + # allartwork = artwork.getAllArtwork(item, parentInfo=True) + allartwork = self.API.getAllArtwork(parentInfo=True) # Set artwork for listitem arttypes = { diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 688ca883..844e530e 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -9,6 +9,8 @@ import xbmcvfs import clientinfo import utils +import PlexAPI + ################################################################################################# @@ -24,6 +26,9 @@ class PlayUtils(): self.userid = utils.window('emby_currUser') self.server = utils.window('emby_server%s' % self.userid) + self.machineIdentifier = utils.window('plex_machineIdentifier') + + self.plx = PlexAPI.API(item) def logMsg(self, msg, lvl=1): @@ -36,34 +41,39 @@ class PlayUtils(): item = self.item playurl = None - if item['MediaSources'][0]['Protocol'] == "Http": - # Only play as http - self.logMsg("File protocol is http.", 1) - playurl = self.httpPlay() - utils.window('emby_%s.playmethod' % playurl, value="DirectStream") + # if item['MediaSources'][0]['Protocol'] == "Http": + # # Only play as http + # self.logMsg("File protocol is http.", 1) + # playurl = self.httpPlay() + # utils.window('emby_%s.playmethod' % playurl, value="DirectStream") - elif self.isDirectPlay(): + # elif self.isDirectPlay(): - self.logMsg("File is direct playing.", 1) - playurl = self.directPlay() - playurl = playurl.encode('utf-8') - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") + # self.logMsg("File is direct playing.", 1) + # playurl = self.directPlay() + # playurl = playurl.encode('utf-8') + # # Set playmethod property + # utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") - elif self.isDirectStream(): + if self.isDirectStream(): self.logMsg("File is direct streaming.", 1) - playurl = self.directStream() + playurl = self.plx.getTranscodeVideoPath('direct') # Set playmethod property utils.window('emby_%s.playmethod' % playurl, value="DirectStream") elif self.isTranscoding(): self.logMsg("File is transcoding.", 1) - playurl = self.transcoding() + quality = { + 'bitrate': self.getBitrate() + } + playurl = self.plx.getTranscodeVideoPath( + 'Transcode', quality=quality + ) # Set playmethod property utils.window('emby_%s.playmethod' % playurl, value="Transcode") - + self.logMsg("The playurl is: %s" % playurl, 1) return playurl def httpPlay(self): @@ -192,13 +202,16 @@ class PlayUtils(): item = self.item if (utils.settings('transcodeH265') == "true" and - item['MediaSources'][0]['Name'].startswith("1080P/H265")): + item[0][0].attrib('videoCodec').startswith("h265") and + item[0][0].attrib('videoResolution').startswith("1080")): # Avoid H265 1080p self.logMsg("Option to transcode 1080P/H265 enabled.", 1) return False # Requirement: BitRate, supported encoding - canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] + # canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] + # Plex: always able?!? + canDirectStream = True # Make sure the server supports it if not canDirectStream: return False @@ -215,16 +228,18 @@ class PlayUtils(): item = self.item server = self.server - itemid = item['Id'] - type = item['Type'] + itemid = self.plx.getKey() + type = item[0].tag - if 'Path' in item and item['Path'].endswith('.strm'): - # Allow strm loading when direct streaming - playurl = self.directPlay() - elif type == "Audio": + # if 'Path' in item and item['Path'].endswith('.strm'): + # # Allow strm loading when direct streaming + # playurl = self.directPlay() + if type == "Audio": playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid) else: playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) + playurl = "{server}/player/playback/playMedia?key=%2Flibrary%2Fmetadata%2F%s&offset=0&X-Plex-Client-Identifier={clientId}&machineIdentifier={SERVER ID}&address={SERVER IP}&port={SERVER PORT}&protocol=http&path=http%3A%2F%2F{SERVER IP}%3A{SERVER PORT}%2Flibrary%2Fmetadata%2F{MEDIA ID}" % (itemid) + playurl = self.plx.replaceURLtags() return playurl @@ -233,7 +248,7 @@ class PlayUtils(): settings = self.getBitrate()*1000 try: - sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) + sourceBitrate = int(self.item[0][0].attrib['bitrate']) except (KeyError, TypeError): self.logMsg("Bitrate value is missing.", 1) else: @@ -245,7 +260,8 @@ class PlayUtils(): return True def isTranscoding(self): - + # I hope Plex transcodes everything + return True item = self.item canTranscode = item['MediaSources'][0]['SupportsTranscoding'] diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index f2d23de4..eb46c336 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -234,12 +234,13 @@ class UserClient(threading.Thread): self.currUserId = userId self.currServer = self.getServer() self.currToken = self.getToken() + self.machineIdentifier = self.getServerId() self.ssl = self.getSSLverify() self.sslcert = self.getSSL() # Test the validity of current token if authenticated == False: - url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) + url = "%s/clients" % (self.currServer) utils.window('emby_currUser', value=userId) utils.window('emby_accessToken%s' % userId, value=self.currToken) result = doUtils.downloadUrl(url) @@ -254,6 +255,7 @@ class UserClient(threading.Thread): utils.window('emby_accessToken%s' % userId, value=self.currToken) utils.window('emby_server%s' % userId, value=self.currServer) utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False)) + utils.window('plex_machineIdentifier', value=self.machineIdentifier) # Set DownloadUtils values doUtils.setUsername(username) @@ -279,6 +281,7 @@ class UserClient(threading.Thread): username = self.getUsername() server = self.getServer() + machineIdentifier = self.getServerId() # If there's no settings.xml if not hasSettings: