Overhaul Plex Companion

This commit is contained in:
tomkat83 2016-02-07 12:38:50 +01:00
parent 125daea6ef
commit 0f14019e5b
15 changed files with 355 additions and 268 deletions

View File

@ -79,6 +79,7 @@ class PlexAPI():
self.deviceName = client.getDeviceName()
self.plexversion = client.getVersion()
self.platform = client.getPlatform()
self.user = utils.window('plex_username')
self.userId = utils.window('emby_currUser')
self.token = utils.window('emby_accessToken%s' % self.userId)
self.server = utils.window('emby_server%s' % self.userId)
@ -782,6 +783,8 @@ class PlexAPI():
"""
# Get addon infos
xargs = {
'Accept': '*/*',
'Connection': 'keep-alive',
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
'X-Plex-Language': 'en',
@ -795,6 +798,9 @@ class PlexAPI():
'X-Plex-Version': self.plexversion,
'X-Plex-Client-Identifier': self.clientId,
'X-Plex-Provides': 'player',
'X-Plex-Username': self.user,
'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac,dts{bitrate:800000&channels:8},ac3{bitrate:800000&channels:8}',
'X-Plex-Client-Profile-Extra': 'add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=*&audioCodec=dca,ac3)',
}
if self.token:
@ -1374,6 +1380,7 @@ class API():
self.part = 0
self.clientinfo = clientinfo.ClientInfo()
self.clientId = self.clientinfo.getDeviceId()
self.user = utils.window('plex_username')
self.userId = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userId)
self.token = utils.window('emby_accessToken%s' % self.userId)
@ -1591,6 +1598,9 @@ class API():
genre.append(child.attrib['tag'])
return genre
def getGuid(self):
return self.item.attrib.get('guid')
def getProvider(self, providername=None):
"""
providername: e.g. 'imdb'
@ -2053,8 +2063,7 @@ class API():
allartworks['Primary'] = artwork
return allartworks
def getTranscodeVideoPath(self, action, quality={}, subtitle={},
audioboost=None, options={}):
def getTranscodeVideoPath(self, action, quality={}):
"""
To be called on a VIDEO level of PMS xml response!
@ -2070,38 +2079,18 @@ class API():
'maxVideoBitrate': e.g. '2000' (in kbits)
}
(one or several of these options)
subtitle {'selected', 'dontBurnIn', 'size'}
audioboost e.g. 100
options dict() of PlexConnect-options as received from aTV
Output:
final URL to pull in PMS transcoder
TODO: mediaIndex
"""
# Set Client capabilities
clientArgs = {
'X-Plex-Client-Capabilities':
'protocols='
'shoutcast,'
'http-video;'
'videoDecoders='
'h264{profile:high&resolution:1080&level:51};'
'audioDecoders='
'mp3,aac,dts{bitrate:800000&channels:8},'
'ac3{bitrate:800000&channels:8}',
'X-Plex-Client-Profile-Extra':
'add-transcode-target-audio-codec'
'(type=videoProfile&'
'context=streaming&'
'protocol=*&'
'audioCodec=dca,ac3)'
}
xargs = PlexAPI().getXArgsDeviceInfo()
xargs = PlexAPI().getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed
if action == "DirectPlay":
path = self.item[0][self.part].attrib['key']
url = self.server + path
# e.g. Trailers already feature an '?'!
if '?' in url:
url += '&' + urlencode(xargs)
else:
@ -2111,49 +2100,20 @@ class API():
# For Direct Streaming or Transcoding
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key']
# transcodePath = self.server + \
# '/video/:/transcode/universal/start.m3u8?'
transcodePath = self.server + \
'/video/:/transcode/universal/start.mkv?'
# args = {
# 'path': path,
# 'mediaIndex': 0, # Probably refering to XML reply sheme
# 'partIndex': self.part,
# 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
# 'offset': 0, # Resume point
# 'fastSeek': 1
# }
'/video/:/transcode/universal/start.m3u8?'
args = {
'copyts': 1,
'path': path,
'mediaIndex': 0, # Probably refering to XML reply sheme
'partIndex': self.part,
'protocol': 'http', # seen in the wild: 'dash', 'http', 'hls'
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': self.clientId,
'offset': 0, # Resume point
# 'offset': 0, # Resume point
'fastSeek': 1
}
# All the settings
if subtitle:
argsUpdate = {
'subtitles': 'burn',
'subtitleSize': subtitle['size'], # E.g. 100
'skipSubtitles': subtitle['dontBurnIn'] # '1': shut off PMS
}
self.logMsg(
"Subtitle: selected %s, dontBurnIn %s, size %s"
% (subtitle['selected'], subtitle['dontBurnIn'],
subtitle['size']),
1
)
args.update(argsUpdate)
if audioboost:
argsUpdate = {
'audioBoost': audioboost
}
self.logMsg("audioboost: %s" % audioboost, 1)
args.update(argsUpdate)
# Currently not used!
if action == "DirectStream":
argsUpdate = {
'directPlay': '0',
@ -2170,7 +2130,6 @@ class API():
args.update(argsUpdate)
url = transcodePath + \
urlencode(clientArgs) + '&' + \
urlencode(xargs) + '&' + \
urlencode(args)
return url
@ -2185,20 +2144,17 @@ class API():
except (TypeError, KeyError, IndexError):
return
itemid = self.getRatingKey()
kodiindex = 0
for stream in mediastreams:
index = stream.attrib['id']
# Since Emby returns all possible tracks together, have to pull only external subtitles.
key = stream.attrib.get('key')
# IsTextSubtitleStream if true, is available to download from emby.
if (stream.attrib.get('streamType', '') == "3" and
stream.attrib.get('format', '')):
if stream.attrib.get('streamType') == "3" and key:
# Direct stream
# PLEX: TODO!!
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, index))
url = ("%s%s" % (self.server, key))
url = self.addPlexCredentialsToUrl(url)
# map external subtitles for mapping
mapping[kodiindex] = index
externalsubs.append(url)

View File

@ -83,10 +83,9 @@ class PlexCompanion(threading.Thread):
if not is_running:
self.logMsg("PleXBMC Helper has started", 0)
is_running = True
if message_count % 1 == 0:
subscribers.subMgr.notify()
subscribers.subMgr.notify()
settings['serverList'] = self.client.getServerList()
except:
self.logMsg("Error in loop, continuing anyway", 1)

View File

@ -3,8 +3,10 @@ from urllib import urlencode
from ast import literal_eval
from urlparse import urlparse, parse_qs
import re
import json
from xbmcaddon import Addon
import xbmc
import downloadutils
from utils import logMsg, settings
@ -85,7 +87,9 @@ def LiteralEval(string):
def GetMethodFromPlexType(plexType):
methods = {
'movie': 'add_update',
'episode': 'add_updateEpisode'
'episode': 'add_updateEpisode',
'show': 'add_update',
'season': 'add_updateSeason'
}
return methods[plexType]
@ -107,6 +111,15 @@ def EmbyItemtypes():
return ['Movie', 'Series', 'Season', 'Episode']
def SelectStreams(url, args):
"""
Does a PUT request to tell the PMS what audio and subtitle streams we have
chosen.
"""
downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), type='PUT')
def GetPlayQueue(playQueueID):
"""
Fetches the PMS playqueue with the playQueueID as an XML
@ -308,3 +321,12 @@ def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'):
logMsg("Error retrieving metadata for %s" % url, -1)
return None
return xml
def getPlexRepeat(kodiRepeat):
plexRepeat = {
'off': '0',
'one': '1',
'all': '2' # does this work?!?
}
return plexRepeat.get(kodiRepeat)

View File

@ -32,7 +32,7 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
class DownloadUtils():
# Borg - multiple instances, shared state
# _shared_state = {}
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
# Requests session
@ -41,8 +41,7 @@ class DownloadUtils():
def __init__(self):
# self.__dict__ = self._shared_state
pass
self.__dict__ = self._shared_state
def setUsername(self, username):
# Reserved for userclient only
@ -215,9 +214,12 @@ class DownloadUtils():
# For Plex Companion
elif type == "POSTXML":
r = s.post(url, postBody, timeout=timeout, headers=header)
elif type == "PUT":
r = s.put(url, timeout=timeout, headers=header)
except AttributeError:
# request session does not exists
self.logMsg("Request session does not exist: start one", 1)
# Get user information
self.userId = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userId)
@ -270,6 +272,13 @@ class DownloadUtils():
cert=cert,
verify=verifyssl)
elif type == "PUT":
r = requests.put(url,
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
# If user is not authenticated
elif not authenticate:
@ -301,6 +310,12 @@ class DownloadUtils():
timeout=timeout,
verify=verifyssl)
elif type == "PUT":
r = requests.put(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
##### THE RESPONSE #####
# self.logMsg(r.url, 2)
if r.status_code == 204:

View File

@ -6,7 +6,6 @@ import json
import os
import sys
import urlparse
import re
import xbmc
import xbmcaddon
@ -27,7 +26,6 @@ import api
import PlexAPI
import PlexFunctions
import embydb_functions
#################################################################################################
@ -81,11 +79,11 @@ def PassPlaylist(xml, resume=None):
"""
# Set window properties to make them available later for other threads
windowArgs = [
# 'containerKey'
'playQueueID',
'playQueueVersion',
'playQueueShuffled']
'playQueueVersion']
for arg in windowArgs:
utils.window(arg, xml.attrib.get(arg, ''))
utils.window(arg, value=xml.attrib.get(arg))
# Get resume point
resume1 = PlexFunctions.ConvertPlexToKodiTime(utils.IntFromStr(

View File

@ -63,7 +63,7 @@ class ThreadedGetMetadata(threading.Thread):
continue
# Download Metadata
plexXML = PlexFunctions.GetPlexMetadata(updateItem['itemId'])
if not plexXML:
if plexXML is None:
# Did not receive a valid XML - skip that item for now
queue.task_done()
continue
@ -116,7 +116,6 @@ class ThreadedProcessMetadata(threading.Thread):
# grabs item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
continue
# Do the work; lock to be sure we've only got 1 Thread
@ -306,7 +305,7 @@ class LibrarySync(threading.Thread):
# Populate self.updatelist
self.GetUpdatelist(items,
PlexFunctions.GetItemClassFromType(plexType),
PlexFunctions.GetItemClassFromType(plexType),
PlexFunctions.GetMethodFromPlexType(plexType),
view['name'],
view['id'])
# Process self.updatelist

View File

@ -72,8 +72,11 @@ class PlaybackUtils():
# Close DB
embyconn.close()
self.logMsg("Playlist size now: %s" % self.playlist.size(), 1)
# Kick off playback
if startPlayer:
self.logMsg("Starting up a new xbmc.Player() instance", 1)
self.logMsg("Starting position: %s" % self.startPos, 1)
Player = xbmc.Player()
Player.play(self.playlist, startpos=self.startPos)
if resume:
@ -82,6 +85,7 @@ class PlaybackUtils():
except:
self.logMsg("Error, could not resume", -1)
else:
self.logMsg("xbmc.Player() instance already up", 1)
# Delete the last playlist item because we have added it already
filename = self.playlist[-1].getfilename()
self.playlist.remove(filename)
@ -131,8 +135,8 @@ class PlaybackUtils():
if playmethod == "Transcode":
utils.window('emby_%s.playmethod' % playurl, clear=True)
playurl = playutils.audioSubsPref(
playurl, listitem, part=counter)
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
listitem, playurl, part=counter)
utils.window('emby_%s.playmethod' % playurl, "Transcode")
self.setProperties(playurl, listitem)
# Update the playurl to the PMS xml response (hence no loop)
@ -161,12 +165,15 @@ class PlaybackUtils():
utils.window('%s.itemid' % embyitem, value=itemid)
# We need to keep track of playQueueItemIDs for Plex Companion
playQueueItemID = self.API.GetPlayQueueItemID()
utils.window(
'plex_%s.playQueueItemID' % playurl, playQueueItemID)
'plex_%s.playQueueItemID'
% playurl, self.API.GetPlayQueueItemID())
utils.window(
'plex_%s.playlistPosition'
% playurl, str(self.currentPosition))
utils.window(
'plex_%s.guid'
% playurl, self.API.getGuid())
if itemtype == "episode":
utils.window('%s.refreshid' % embyitem,

View File

@ -38,6 +38,14 @@ class Player(xbmc.Player):
self.logMsg("Starting playback monitor.", 2)
# Should we start notification or is this done by Plex Companion?
self.doNotify = False if (utils.settings('plexCompanion') == 'true') \
else True
if self.doNotify:
self.logMsg("PMS notifications not done by Plex Companion", 2)
else:
self.logMsg("PMS notifications done by Plex Companion", 2)
def GetPlayStats(self):
return self.playStats
@ -68,7 +76,8 @@ class Player(xbmc.Player):
if currentFile:
self.currentFile = currentFile
# Save currentFile for cleanup later
utils.window('plex_lastPlayedFiled', value=currentFile)
# We may need to wait for info to be set in kodi monitor
itemId = utils.window("emby_%s.itemid" % currentFile)
tryCount = 0
@ -93,6 +102,11 @@ class Player(xbmc.Player):
itemType = utils.window("%s.type" % embyitem)
utils.window('emby_skipWatched%s' % itemId, value="true")
# Suspend library sync thread while movie is playing
self.logMsg("Playing itemtype is: %s" % itemType, 1)
if itemType in ['movie', 'audio']:
self.logMsg("Suspending library sync while playing", 1)
utils.window('suspend_LibraryThread', value='true')
if (utils.window('emby_customPlaylist') == "true" and
utils.window('emby_customPlaylist.seektime')):
@ -214,10 +228,18 @@ class Player(xbmc.Player):
runtime = xbmcplayer.getTotalTime()
self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1)
playQueueVersion = utils.window(
'playQueueVersion')
playQueueID = utils.window(
'playQueueID')
playQueueItemID = utils.window(
'plex_%s.playQueueItemID' % currentFile)
# Save data map for updates and position calls
data = {
'runtime': runtime,
'playQueueVersion': playQueueVersion,
'playQueueID': playQueueID,
'playQueueItemID': playQueueItemID,
'runtime': runtime * 1000,
'item_id': itemId,
'refresh_id': refresh_id,
'currentfile': currentFile,
@ -247,7 +269,10 @@ class Player(xbmc.Player):
self.playStats[playMethod] = 1'''
def reportPlayback(self):
# Don't use if Plex Companion is enabled
if not self.doNotify:
return
self.logMsg("reportPlayback Called", 2)
xbmcplayer = self.xbmcplayer
@ -311,6 +336,12 @@ class Player(xbmc.Player):
'duration': int(duration) * 1000
}
# For PMS playQueues/playlists only
if data.get('playQueueID'):
postdata['containerKey'] = '/playQueues/' + data.get('playQueueID')
postdata['playQueueVersion'] = data.get('playQueueVersion')
postdata['playQueueItemID'] = data.get('playQueueItemID')
if playMethod == "Transcode":
# Track can't be changed, keep reporting the same index
postdata['AudioStreamIndex'] = audioindex
@ -422,12 +453,14 @@ class Player(xbmc.Player):
utils.window('emby_customPlaylist.seektime', clear=True)
utils.window('emby_playbackProps', clear=True)
self.logMsg("Clear playlist properties.", 1)
utils.window('suspend_LibraryThread', clear=True)
self.stopAll()
def onPlayBackEnded( self ):
# Will be called when xbmc stops playing a file
self.logMsg("ONPLAYBACK_ENDED", 2)
utils.window('emby_customPlaylist.seektime', clear=True)
utils.window('suspend_LibraryThread', clear=True)
self.stopAll()
def stopAll(self):
@ -457,7 +490,7 @@ class Player(xbmc.Player):
if currentPosition and runtime:
try:
percentComplete = (currentPosition * 10000) / int(runtime)
percentComplete = currentPosition / int(runtime)
except ZeroDivisionError:
# Runtime is 0.
percentComplete = 0
@ -474,9 +507,8 @@ class Player(xbmc.Player):
# Stop transcoding
if playMethod == "Transcode":
self.logMsg("Transcoding for %s terminated." % itemid, 1)
deviceId = self.clientInfo.getDeviceId()
url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
doUtils.downloadUrl(url, type="DELETE")
url = "{server}/video/:/transcode/universal/stop?session=%s" % self.clientInfo.getDeviceId()
doUtils.downloadUrl(url, type="GET")
# Send the delete action to the server.
offerDelete = False
@ -490,6 +522,8 @@ class Player(xbmc.Player):
# Delete could be disabled, even if the subsetting is enabled.
offerDelete = False
# Plex: never delete
offerDelete = False
if percentComplete >= markPlayedAt and offerDelete:
if utils.settings('skipConfirmDelete') != "true":
resp = xbmcgui.Dialog().yesno(
@ -502,7 +536,22 @@ class Player(xbmc.Player):
url = "{server}/emby/Items/%s?format=json" % itemid
self.logMsg("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, type="DELETE")
# Clean the WINDOW properties
# for filename in self.played_info:
# cleanup = (
# 'emby_%s.itemid' % filename,
# 'emby_%s.runtime' % filename,
# 'emby_%s.refreshid' % filename,
# 'emby_%s.playmethod' % filename,
# 'emby_%s.type' % filename,
# 'plex_%s.playQueueItemID' % filename,
# 'plex_%s.playlistPosition' % filename,
# 'plex_%s.guid' % filename
# )
# for item in cleanup:
# utils.window(item, clear=True)
self.played_info.clear()
def stopPlayback(self, data):
@ -517,7 +566,7 @@ class Player(xbmc.Player):
'ratingKey': itemId,
'state': 'stopped', # 'stopped', 'paused', 'buffering', 'playing'
'time': int(playTime) * 1000,
'duration': int(duration) * 1000
'duration': int(duration)
}
url = url + urlencode(args)
self.doUtils.downloadUrl(url, type="GET")

View File

@ -2,6 +2,8 @@
#################################################################################################
from urllib import urlencode
import xbmc
import xbmcgui
import xbmcvfs
@ -10,6 +12,7 @@ import clientinfo
import utils
import PlexAPI
import PlexFunctions
#################################################################################################
@ -45,16 +48,19 @@ class PlayUtils():
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectPlay")
elif self.isDirectStream():
self.logMsg("File is direct streaming.", 1)
playurl = self.API.getTranscodeVideoPath('DirectStream')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectStream")
# Currently no direct streaming possible - needs investigation
# elif self.isDirectStream():
# self.logMsg("File is direct streaming.", 1)
# playurl = self.API.getTranscodeVideoPath('DirectStream')
# # Set playmethod property
# utils.window('emby_%s.playmethod' % playurl, "DirectStream")
elif self.isTranscoding():
self.logMsg("File is transcoding.", 1)
quality = {
'maxVideoBitrate': self.getBitrate()
'maxVideoBitrate': self.getBitrate(),
'videoResolution': self.getResolution(),
'videoQuality': '100'
}
playurl = self.API.getTranscodeVideoPath('Transcode',
quality=quality)
@ -89,13 +95,9 @@ class PlayUtils():
self.logMsg("Can't direct play, user enabled play from HTTP.", 1)
return False
if not self.h265enabled():
if self.h265enabled():
return False
# Found with e.g. trailers
# if self.API.getDataFromPartOrMedia('optimizedForStreaming') == '1':
# return False
return True
def directPlay(self):
@ -150,22 +152,23 @@ class PlayUtils():
return False
def h265enabled(self):
"""
Returns True if we need to transcode
"""
videoCodec = self.API.getVideoCodec()
codec = videoCodec['videocodec']
resolution = videoCodec['resolution']
# 720p
if ((utils.settings('transcode720H265') == "true") and
("h265" in codec) and
(resolution in "720 1080")):
self.logMsg("Option to transcode 720P/h265 enabled.", 0)
h265 = self.getH265()
if not ('h265' in codec or 'hevc' in codec) or (h265 is None):
return False
# 1080p
if ((utils.settings('transcodeH265') == "true") and
("h265" in codec) and
(resolution == "1080")):
self.logMsg("Option to transcode 1080P/h265 enabled.", 0)
return False
return True
if resolution >= h265:
self.logMsg("Option to transcode h265 enabled. Resolution media: "
"%s, transcoding limit resolution: %s"
% (resolution, h265), 1)
return True
return False
def isDirectStream(self):
if not self.h265enabled():
@ -247,88 +250,106 @@ class PlayUtils():
return playurl
def getBitrate(self):
# get the addon video quality
videoQuality = utils.settings('videoBitrate')
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 664,
'1': 996,
'2': 1320,
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3200,
'5': 4700,
'6': 6200,
'7': 7700,
'8': 9200,
'9': 10700,
'10': 12200,
'11': 13700,
'12': 15200,
'13': 16700,
'14': 18200,
'15': 20000,
'16': 40000,
'17': 100000,
'18': 1000000
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
def audioSubsPref(self, url, listitem, part=None):
def getH265(self):
chosen = utils.settings('transcodeH265')
H265 = {
'0': None,
'1': 480,
'2': 720,
'3': 1080
}
return H265.get(chosen, None)
def getResolution(self):
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '420x420',
'1': '576x320',
'2': '720x480',
'3': '1024x768',
'4': '1280x720',
'5': '1280x720',
'6': '1920x1080',
'7': '1920x1080',
'8': '1920x1080',
'9': '1920x1080',
'10': '1920x1080',
}
return res[chosen]
def audioSubsPref(self, listitem, url, part=None):
# For transcoding only
# Present the list of audio to select from
audioStreamsList = {}
audioStreamsList = []
audioStreams = []
audioStreamsChannelsList = {}
subtitleStreamsList = {}
subtitleStreams = ['No subtitles']
# audioStreamsChannelsList = []
subtitleStreamsList = []
subtitleStreams = ['1 No subtitles']
downloadableStreams = []
selectAudioIndex = ""
# selectAudioIndex = ""
selectSubsIndex = ""
playurlprefs = "%s" % url
playurlprefs = {}
# Set part where we're at
self.API.setPartNumber(part)
item = self.item
try:
mediasources = item[0]
mediastreams = mediasources[part]
mediastreams = self.item[0][part]
except (TypeError, KeyError, IndexError):
return
audioNum = 0
# Remember 'no subtitles'
subNum = 1
for stream in mediastreams:
# Since Emby returns all possible tracks together, have to sort them.
index = stream['index']
type = stream['streamType']
index = stream.attrib['id']
type = stream.attrib['streamType']
# Audio
if type == "2":
codec = stream['codec']
channelLayout = stream.get('audioChannelLayout', "")
codec = stream.attrib['codec']
channelLayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s - %s - %s %s" % (index, stream['language'], codec, channelLayout)
track = "%s %s - %s %s" % (audioNum+1, stream.attrib['language'], codec, channelLayout)
except:
track = "%s - %s %s" % (index, codec, channelLayout)
track = "%s 'unknown' - %s %s" % (audioNum+1, codec, channelLayout)
audioStreamsChannelsList[index] = stream['channels']
audioStreamsList[track] = index
#audioStreamsChannelsList[audioNum] = stream.attrib['channels']
audioStreamsList.append(index)
audioStreams.append(track)
audioNum += 1
# Subtitles
elif type == "3":
'''if stream['IsExternal']:
continue'''
try:
track = "%s - %s" % (index, stream['language'])
track = "%s %s" % (subNum+1, stream.attrib['language'])
except:
track = "%s - %s" % (index, stream['codec'])
track = "%s 'unknown' (%s)" % (subNum+1, stream.attrib['codec'])
default = stream.get('default', None)
forced = stream.get('forced', None)
downloadable = True if stream.get('format', None) else False
default = stream.attrib.get('default')
forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key')
if default:
track = "%s - Default" % track
@ -337,53 +358,58 @@ class PlayUtils():
if downloadable:
downloadableStreams.append(index)
subtitleStreamsList[track] = index
subtitleStreamsList.append(index)
subtitleStreams.append(track)
subNum += 1
if len(audioStreams) > 1:
if audioNum > 1:
resp = xbmcgui.Dialog().select("Choose the audio stream", audioStreams)
if resp > -1:
# User selected audio
selected = audioStreams[resp]
selectAudioIndex = audioStreamsList[selected]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
else: # User backed out of selection
playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
playurlprefs['audioStreamID'] = audioStreamsList[resp]
else: # User backed out of selection - let PMS decide
pass
else: # There's only one audiotrack.
selectAudioIndex = audioStreamsList[audioStreams[0]]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
playurlprefs['audioStreamID'] = audioStreamsList[0]
if len(subtitleStreams) > 1:
# Add audio boost
playurlprefs['audioBoost'] = utils.settings('audioBoost')
if subNum > 1:
resp = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams)
if resp == 0:
# User selected no subtitles
pass
playurlprefs["skipSubtitles"] = 1
elif resp > -1:
# User selected subtitles
selected = subtitleStreams[resp]
selectSubsIndex = subtitleStreamsList[selected]
selectSubsIndex = subtitleStreamsList[resp-1]
# Load subtitles in the listitem if downloadable
if selectSubsIndex in downloadableStreams:
itemid = item['Id']
url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, selectSubsIndex))]
self.logMsg("Set up subtitles: %s %s" % (selectSubsIndex, url), 1)
listitem.setSubtitles(url)
url = "%s/library/streams/%s" \
% (self.server, selectSubsIndex)
url = self.API.addPlexCredentialsToUrl(url)
self.logMsg("Downloadable sub: %s: %s" % (selectSubsIndex, url), 1)
listitem.setSubtitles([url])
else:
# Burn subtitles
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
self.logMsg('Need to burn in subtitle %s' % selectSubsIndex, 1)
playurlprefs["subtitleStreamID"] = selectSubsIndex
playurlprefs["subtitleSize"] = utils.settings('subtitleSize')
else: # User backed out of selection
playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
pass
# Tell the PMS what we want with a PUT request
# url = self.server + '/library/parts/' + self.item[0][part].attrib['id']
# PlexFunctions.SelectStreams(url, playurlprefs)
url += '&' + urlencode(playurlprefs)
# Get number of channels for selected audio track
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
if audioChannels > 2:
playurlprefs += "&AudioBitrate=384000"
else:
playurlprefs += "&AudioBitrate=192000"
# audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
# if audioChannels > 2:
# playurlprefs += "&AudioBitrate=384000"
# else:
# playurlprefs += "&AudioBitrate=192000"
return playurlprefs
return url

View File

@ -115,7 +115,7 @@ def parseJSONRPC(jsonraw):
return parsed.get('result', {})
def getXMLHeader():
return '<?xml version="1.0" encoding="utf-8"?>'+"\r\n"
return '<?xml version="1.0" encoding="utf-8" ?>'+"\r\n"
def getOKMsg():
return getXMLHeader() + '<Response code="200" status="OK" />'
@ -176,7 +176,10 @@ def getPhotoPlayerId(players = False):
return players.get(xbmc_photo(), {}).get('playerid', None)
def getVolume():
return str(jsonrpc('Application.GetProperties', { "properties": [ "volume" ] }).get('volume', 100))
answ = jsonrpc('Application.GetProperties', { "properties": [ "volume", 'muted' ] })
vol = str(answ.get('volume', 100))
mute = ("0", "1")[answ.get('muted', False)]
return (vol, mute)
def timeToMillis(time):
return (time['hours']*3600 + time['minutes']*60 + time['seconds'])*1000 + time['milliseconds']

View File

@ -111,13 +111,6 @@ class MyHandler(BaseHTTPRequestHandler):
printDebug("adjusting the volume to %s%%" % volume)
jsonrpc("Application.SetVolume", {"volume": volume})
elif "/playMedia" in request_path:
playQueueVersion = int(params.get('playQueueVersion', 1))
if playQueueVersion < subMgr.playQueueVersion:
# playQueue was updated; ignore this command for now
return
if playQueueVersion > subMgr.playQueueVersion:
# TODO: we should probably update something else now :-)
subMgr.playQueueVersion = playQueueVersion
s.response(getOKMsg(), getPlexHeaders())
offset = params.get('viewOffset', params.get('offset', "0"))
protocol = params.get('protocol', "http")

View File

@ -1,11 +1,15 @@
import re
import threading
import xbmcgui
from xml.dom.minidom import parseString
from functions import *
from settings import settings
from httppersist import requests
from xbmc import Player
import xbmcgui
import downloadutils
from utils import window
import PlexFunctions as pf
class SubscriptionManager:
def __init__(self):
@ -13,19 +17,18 @@ class SubscriptionManager:
self.info = {}
self.lastkey = ""
self.containerKey = ""
self.playQueueID = ''
self.playQueueVersion = 1
self.lastratingkey = ""
self.volume = 0
self.guid = ""
self.mute = '0'
self.server = ""
self.protocol = "http"
self.port = ""
self.playerprops = {}
self.download = downloadutils.DownloadUtils()
self.xbmcplayer = Player()
def getVolume(self):
self.volume = getVolume()
self.volume, self.mute = getVolume()
def msg(self, players):
msg = getXMLHeader()
@ -42,7 +45,6 @@ class SubscriptionManager:
else:
self.mainlocation = "navigation"
msg += ' location="%s">' % self.mainlocation
msg += self.getTimelineXML(getAudioPlayerId(players), plex_audio())
msg += self.getTimelineXML(getPhotoPlayerId(players), plex_photo())
msg += self.getTimelineXML(getVideoPlayerId(players), plex_video())
@ -59,48 +61,63 @@ class SubscriptionManager:
else:
state = "stopped"
time = 0
ret = "\r\n"+'<Timeline location="%s" state="%s" time="%s" type="%s"' % (self.mainlocation, state, time, ptype)
if playerid is not None:
WINDOW = xbmcgui.Window(10000)
# pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server'))
# userId = str(WINDOW.getProperty('emby_currUser'))
# pbmc_server = str(WINDOW.getProperty('emby_server%s' % userId))
pbmc_server = None
keyid = None
count = 0
while not keyid:
if count > 10:
break
keyid = str(WINDOW.getProperty('Plex_currently_playing_itemid'))
xbmc.sleep(1000)
count += 1
if keyid:
self.lastkey = "/library/metadata/%s"%keyid
self.lastratingkey = keyid
ret += ' containerKey="%s"' % (self.containerKey)
ret += ' key="%s"' % (self.lastkey)
ret += ' ratingKey="%s"' % (self.lastratingkey)
if pbmc_server:
(self.server, self.port) = pbmc_server.split(':')
serv = getServerByHost(self.server)
if self.playQueueID:
ret += ' playQueueID="%s"' % self.playQueueID
ret += ' playQueueVersion="%s"' % self.playQueueVersion
ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable()
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
ret += ' protocol="%s"' % serv.get('protocol', "http")
ret += ' address="%s"' % serv.get('server', self.server)
ret += ' port="%s"' % serv.get('port', self.port)
ret += ' guid="%s"' % info['guid']
ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle']
ret = "\r\n"+' <Timeline state="%s" time="%s" type="%s"' % (state, time, ptype)
if playerid is None:
ret += ' seekRange="0-0"'
ret += ' />'
return ret
WINDOW = xbmcgui.Window(10000)
ret += '/>'
# pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server'))
# userId = str(WINDOW.getProperty('emby_currUser'))
# pbmc_server = str(WINDOW.getProperty('emby_server%s' % userId))
pbmc_server = None
keyid = None
count = 0
while not keyid:
if count > 10:
break
keyid = WINDOW.getProperty('Plex_currently_playing_itemid')
xbmc.sleep(1000)
count += 1
if keyid:
self.lastkey = "/library/metadata/%s"%keyid
self.lastratingkey = keyid
ret += ' location="%s"' % (self.mainlocation)
ret += ' key="%s"' % (self.lastkey)
ret += ' ratingKey="%s"' % (self.lastratingkey)
if pbmc_server:
(self.server, self.port) = pbmc_server.split(':')
serv = getServerByHost(self.server)
if info.get('playQueueID'):
ret += ' playQueueID="%s"' % info.get('playQueueID')
ret += ' playQueueVersion="%s"' % info.get('playQueueVersion')
ret += ' playQueueItemID="%s"' % (info.get('playQueueItemID'))
ret += ' containerKey="/playQueues/%s"' \
% (info.get('playQueueID'))
elif keyid:
ret += ' containerKey="%s"' % (self.containerKey)
ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable()
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
ret += ' protocol="%s"' % serv.get('protocol', "http")
ret += ' address="%s"' % serv.get('server', self.server)
ret += ' port="%s"' % serv.get('port', self.port)
ret += ' guid="%s"' % info['guid']
ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle']
ret += ' mute="%s"' % self.mute
ret += ' repeat="%s"' % info['repeat']
# Might need an update in the future
ret += ' subtitleStreamID="-1"'
ret += ' audioStreamID="-1"'
ret += ' />'
return ret
def updateCommandID(self, uuid, commandID):
if commandID and self.subscribers.get(uuid, False):
self.subscribers[uuid].commandID = int(commandID)
@ -126,14 +143,15 @@ class SubscriptionManager:
info = self.playerprops[p.get('playerid')]
params = {}
params['containerKey'] = (self.containerKey or "/library/metadata/900000")
if self.playQueueID:
params['playQueueID'] = self.playQueueID
if info.get('playQueueID'):
params['containerKey'] = '/playQueues/' + info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion']
params['playQueueItemID'] = info['playQueueItemID']
params['key'] = (self.lastkey or "/library/metadata/900000")
params['ratingKey'] = (self.lastratingkey or "900000")
params['state'] = info['state']
params['time'] = info['time']
params['duration'] = info['duration']
params['playQueueVersion'] = self.playQueueVersion
serv = getServerByHost(self.server)
url = serv.get('protocol', 'http') + '://' \
+ serv.get('server', 'localhost') + ':' \
@ -171,12 +189,20 @@ class SubscriptionManager:
info = {}
try:
# get info from the player
props = jsonrpc("Player.GetProperties", {"playerid": playerid, "properties": ["time", "totaltime", "speed", "shuffled"]})
props = jsonrpc("Player.GetProperties", {"playerid": playerid, "properties": ["time", "totaltime", "speed", "shuffled", "repeat"]})
printDebug(jsonrpc("Player.GetItem", {"playerid": playerid, "properties": ["file", "showlink", "episode", "season"]}))
info['time'] = timeToMillis(props['time'])
info['duration'] = timeToMillis(props['totaltime'])
info['state'] = ("paused", "playing")[int(props['speed'])]
info['shuffle'] = ("0","1")[props.get('shuffled', False)]
info['shuffle'] = ("0","1")[props.get('shuffled', False)]
info['repeat'] = pf.getPlexRepeat(props.get('repeat'))
# New PMS playQueue attributes
cf = self.xbmcplayer.getPlayingFile()
info['playQueueID'] = window('playQueueID')
info['playQueueVersion'] = window('playQueueVersion')
info['playQueueItemID'] = window('plex_%s.playQueueItemID' % cf)
info['guid'] = window('plex_%s.guid' % cf)
except:
info['time'] = 0
info['duration'] = 0
@ -184,7 +210,7 @@ class SubscriptionManager:
info['shuffle'] = False
# get the volume from the application
info['volume'] = self.volume
info['guid'] = self.guid
info['mute'] = self.mute
return info
@ -216,21 +242,11 @@ class Subscriber:
printDebug("sending xml to subscriber %s: %s" % (self.tostr(), msg))
url = self.protocol + '://' + self.host + ':' + self.port \
+ "/:/timeline"
# Override some headers
headerOptions = {
'Content-Range': 'bytes 0-/-1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17',
'Accept': '*/*',
'X-Plex-Username': 'croneter',
'Connection': 'keep-alive',
'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac,dts{bitrate:800000&channels:8},ac3{bitrate:800000&channels:8}',
'X-Plex-Client-Profile-Extra': 'add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=*&audioCodec=dca,ac3)'
}
response = self.download.downloadUrl(
url,
postBody=msg,
type="POSTXML",
headerOptions=headerOptions)
type="POSTXML")
# if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol):
# subMgr.removeSubscriber(self.uuid)
if response in [False, None, 401]:

View File

@ -224,6 +224,7 @@ class UserClient(threading.Thread):
if authenticated == False:
url = "%s/clients" % (self.currServer)
utils.window('emby_currUser', value=userId)
utils.window('plex_username', value=username)
utils.window('emby_accessToken%s' % userId, value=self.currToken)
result = doUtils.downloadUrl(url)
@ -234,6 +235,7 @@ class UserClient(threading.Thread):
# Set to windows property
utils.window('emby_currUser', value=userId)
utils.window('plex_username', value=username)
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))

View File

@ -60,9 +60,11 @@
<setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" />
<setting type="sep" />
<setting id="playFromStream" type="bool" label="30002" default="false" />
<setting id="videoBitrate" type="enum" label="30160" values="664 Kbps SD|996 Kbps HD|1.3 Mbps HD|2.0 Mbps HD|3.2 Mbps HD|4.7 Mbps HD|6.2 Mbps HD|7.7 Mbps HD|9.2 Mbps HD|10.7 Mbps HD|12.2 Mbps HD|13.7 Mbps HD|15.2 Mbps HD|16.7 Mbps HD|18.2 Mbps HD|20.0 Mbps HD|40.0 Mbps HD|100.0 Mbps HD [default]|1000.0 Mbps HD" visible="eq(-1,true)" default="17" />
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled|480p(and higher)|720p(and higher)|1080p" />
<setting id="markPlayed" type="number" visible="false" default="90" />
<setting id="transcoderVideoQualities" type="enum" label="30160" values="420x420, 320Kbps|576x320, 720Kbps|720x480, 1.5Mbps|1024x768, 2Mbps|1280x720, 3Mbps|1280x720, 4Mbps|1920x1080, 8Mbps|1920x1080, 10Mbps|1920x1080, 12Mbps|1920x1080, 20Mbps|1920x1080, 40Mbps" visible="eq(-1,true)" default="11" />
<setting id="audioBoost" type="slider" label="Boost audio when transcoding" default="0" range="0,10,100" option="int"/>
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled (default)|480p (and higher)|720p (and higher)|1080p" />
<setting id="subtitleSize" label="Subtitle size when transcoding" type="slider" option="int" range="0,30,300" default="100" />
<setting id="markPlayed" type="number" visible="false" default="95" />
<setting id="failedCount" type="number" visible="false" default="0" />
<setting id="networkCreds" type="text" visible="false" default="" />
</category>

View File

@ -81,7 +81,7 @@ class Service():
"emby_syncRunning", "emby_dbCheck", "emby_kodiScan",
"emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId",
"emby_initialScan", "emby_customplaylist", "emby_playbackProps",
"plex_runLibScan"
"plex_runLibScan", "plex_username"
]
for prop in properties:
window(prop, clear=True)