Switch Companion to use json_rpc.py

This commit is contained in:
tomkat83 2017-12-09 13:47:19 +01:00
parent cceb110354
commit 843bedbee6
7 changed files with 119 additions and 309 deletions

View file

@ -8,8 +8,8 @@ from urllib import urlencode
from xbmc import sleep, executebuiltin from xbmc import sleep, executebuiltin
from utils import settings, thread_methods from utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, functions, \ from plexbmchelper import listener, plexgdm, subscribers, httppersist, \
httppersist, plexsettings plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml from playlist_func import get_pms_playqueue, get_plextype_from_xml
@ -196,9 +196,8 @@ class PlexCompanion(Thread):
# Start up instances # Start up instances
requestMgr = httppersist.RequestMgr() requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager( subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.mgr) requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100) queue = Queue.Queue(maxsize=100)
self.queue = queue self.queue = queue
@ -211,7 +210,6 @@ class PlexCompanion(Thread):
httpd = listener.ThreadedHTTPServer( httpd = listener.ThreadedHTTPServer(
client, client,
subscriptionManager, subscriptionManager,
jsonClass,
self.settings, self.settings,
queue, queue,
('', self.settings['myport']), ('', self.settings['myport']),

View file

@ -3,7 +3,7 @@ Collection of functions using the Kodi JSON RPC interface.
See http://kodi.wiki/view/JSON-RPC_API See http://kodi.wiki/view/JSON-RPC_API
""" """
from json import loads, dumps from json import loads, dumps
from utils import milliseconds_to_kodi_time from utils import millis_to_kodi_time
from xbmc import executeJSONRPC from xbmc import executeJSONRPC
@ -49,7 +49,7 @@ def get_players():
'picture': ... 'picture': ...
} }
""" """
info = jsonrpc("Player.GetActivePlayers").execute()['result'] or [] info = jsonrpc("Player.GetActivePlayers").execute()['result']
ret = {} ret = {}
for player in info: for player in info:
player['playerid'] = int(player['playerid']) player['playerid'] = int(player['playerid'])
@ -152,7 +152,7 @@ def seek_to(offset):
for playerid in get_player_ids(): for playerid in get_player_ids():
jsonrpc("Player.Seek").execute( jsonrpc("Player.Seek").execute(
{"playerid": playerid, {"playerid": playerid,
"value": milliseconds_to_kodi_time(offset)}) "value": millis_to_kodi_time(offset)})
def smallforward(): def smallforward():
@ -240,6 +240,13 @@ def input_back():
return jsonrpc("Input.Back").execute() return jsonrpc("Input.Back").execute()
def input_sendtext(text):
"""
Tells Kodi the user sent text [unicode]
"""
return jsonrpc("Input.SendText").execute({'test': text, 'done': False})
def playlist_get_items(playlistid, properties): def playlist_get_items(playlistid, properties):
""" """
playlistid: [int] id of the Kodi playlist playlistid: [int] id of the Kodi playlist
@ -350,6 +357,33 @@ def get_episodes(params):
return ret return ret
def get_player_props(playerid):
"""
Returns a dict for the active Kodi player with the following values:
{
'type' [str] the Kodi player type, e.g. 'video'
'time' The current item's time in Kodi time
'totaltime' The current item's total length in Kodi time
'speed' [int] playback speed, defaults to 0
'shuffled' [bool] True if shuffled
'repeat' [str] 'off', 'one', 'all'
'position' [int] position in playlist (or -1)
'playlistid' [int] the Kodi playlist id (or -1)
}
"""
ret = jsonrpc('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['type',
'time',
'totaltime',
'speed',
'shuffled',
'repeat',
'position',
'playlistid']})
return ret['result']
def current_audiostream(playerid): def current_audiostream(playerid):
""" """
Returns a dict of the active audiostream for playerid [int]: Returns a dict of the active audiostream for playerid [int]:
@ -402,3 +436,10 @@ def subtitle_enabled(playerid):
except (KeyError, TypeError): except (KeyError, TypeError):
ret = False ret = False
return ret return ret
def ping():
"""
Pings the JSON RPC interface
"""
return jsonrpc('JSONRPC.Ping').execute()

View file

@ -1,244 +0,0 @@
import logging
import base64
import json
import string
import xbmc
import plexdb_functions as plexdb
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
def xbmc_photo():
return "photo"
def xbmc_video():
return "video"
def xbmc_audio():
return "audio"
def plex_photo():
return "photo"
def plex_video():
return "video"
def plex_audio():
return "music"
def xbmc_type(plex_type):
if plex_type == plex_photo():
return xbmc_photo()
elif plex_type == plex_video():
return xbmc_video()
elif plex_type == plex_audio():
return xbmc_audio()
def plex_type(xbmc_type):
if xbmc_type == xbmc_photo():
return plex_photo()
elif xbmc_type == xbmc_video():
return plex_video()
elif xbmc_type == xbmc_audio():
return plex_audio()
def getXMLHeader():
return '<?xml version="1.0" encoding="UTF-8"?>\n'
def getOKMsg():
return getXMLHeader() + '<Response code="200" status="OK" />'
def timeToMillis(time):
return (time['hours']*3600 +
time['minutes']*60 +
time['seconds'])*1000 + time['milliseconds']
def millisToTime(t):
millis = int(t)
seconds = millis / 1000
minutes = seconds / 60
hours = minutes / 60
seconds = seconds % 60
minutes = minutes % 60
millis = millis % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': millis}
def textFromXml(element):
return element.firstChild.data
class jsonClass():
def __init__(self, requestMgr, settings):
self.settings = settings
self.requestMgr = requestMgr
def jsonrpc(self, action, arguments={}):
""" put some JSON together for the JSON-RPC APIv6 """
if action.lower() == "sendkey":
request = json.dumps({
"jsonrpc": "2.0",
"method": "Input.SendText",
"params": {
"text": arguments[0],
"done": False
}
})
elif action.lower() == "ping":
request = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "JSONRPC.Ping"
})
elif arguments:
request = json.dumps({
"id": 1,
"jsonrpc": "2.0",
"method": action,
"params": arguments})
else:
request = json.dumps({
"id": 1,
"jsonrpc": "2.0",
"method": action
})
result = self.parseJSONRPC(xbmc.executeJSONRPC(request))
if not result and self.settings['webserver_enabled']:
# xbmc.executeJSONRPC appears to fail on the login screen, but
# going through the network stack works, so let's try the request
# again
result = self.parseJSONRPC(self.requestMgr.post(
"127.0.0.1",
self.settings['port'],
"/jsonrpc",
request,
{'Content-Type': 'application/json',
'Authorization': 'Basic %s' % string.strip(
base64.encodestring('%s:%s'
% (self.settings['user'],
self.settings['passwd'])))
}))
return result
def skipTo(self, plexId, typus):
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
# playerId = self.
with plexdb.Get_Plex_DB() as plex_db:
plexdb_item = plex_db.getItem_byId(plexId)
try:
dbid = plexdb_item[0]
mediatype = plexdb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId)
return
log.debug('plexid: %s, kodi id: %s, type: %s'
% (plexId, dbid, mediatype))
def getPlexHeaders(self):
h = {
"Content-type": "text/xml",
"Access-Control-Allow-Origin": "*",
"X-Plex-Version": self.settings['version'],
"X-Plex-Client-Identifier": self.settings['uuid'],
"X-Plex-Provides": "client,controller,player",
"X-Plex-Product": "PlexKodiConnect",
"X-Plex-Device-Name": self.settings['client_name'],
"X-Plex-Platform": "Kodi",
"X-Plex-Model": self.settings['platform'],
"X-Plex-Device": "PC",
}
if self.settings['myplex_user']:
h["X-Plex-Username"] = self.settings['myplex_user']
return h
def parseJSONRPC(self, jsonraw):
if not jsonraw:
log.debug("Empty response from Kodi")
return {}
else:
parsed = json.loads(jsonraw)
if parsed.get('error', False):
log.error("Kodi returned an error: %s" % parsed.get('error'))
return parsed.get('result', {})
def getPlayers(self):
info = self.jsonrpc("Player.GetActivePlayers") or []
ret = {}
for player in info:
player['playerid'] = int(player['playerid'])
ret[player['type']] = player
return ret
def getPlaylistId(self, typus):
"""
typus: one of the Kodi types, e.g. audio or video
Returns None if nothing was found
"""
for playlist in self.getPlaylists():
if playlist.get('type') == typus:
return playlist.get('playlistid')
def getPlaylists(self):
"""
Returns a list, e.g.
[
{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}
]
"""
return self.jsonrpc('Playlist.GetPlaylists')
def getPlayerIds(self):
ret = []
for player in self.getPlayers().values():
ret.append(player['playerid'])
return ret
def getVideoPlayerId(self, players=False):
if players is None:
players = self.getPlayers()
return players.get(xbmc_video(), {}).get('playerid', None)
def getAudioPlayerId(self, players=False):
if players is None:
players = self.getPlayers()
return players.get(xbmc_audio(), {}).get('playerid', None)
def getPhotoPlayerId(self, players=False):
if players is None:
players = self.getPlayers()
return players.get(xbmc_photo(), {}).get('playerid', None)
def getVolume(self):
answ = self.jsonrpc('Application.GetProperties',
{
"properties": ["volume", 'muted']
})
vol = str(answ.get('volume', 100))
mute = ("0", "1")[answ.get('muted', False)]
return (vol, mute)

View file

@ -8,9 +8,9 @@ from urlparse import urlparse, parse_qs
from xbmc import sleep from xbmc import sleep
from companion import process_command from companion import process_command
from utils import window from utils import window
import json_rpc as js
from functions import * from clientinfo import getXArgsDeviceInfo
import variables as v
############################################################################### ###############################################################################
@ -82,7 +82,6 @@ class MyHandler(BaseHTTPRequestHandler):
def answer_request(self, sendData): def answer_request(self, sendData):
self.serverlist = self.server.client.getServerList() self.serverlist = self.server.client.getServerList()
subMgr = self.server.subscriptionManager subMgr = self.server.subscriptionManager
js = self.server.jsonClass
settings = self.server.settings settings = self.server.settings
try: try:
@ -105,7 +104,7 @@ class MyHandler(BaseHTTPRequestHandler):
% settings['version']) % settings['version'])
elif request_path == "verify": elif request_path == "verify":
self.response("XBMC JSON connection test:\n" + self.response("XBMC JSON connection test:\n" +
js.jsonrpc("ping")) js.ping())
elif "resources" == request_path: elif "resources" == request_path:
resp = ('%s' resp = ('%s'
'<MediaContainer>' '<MediaContainer>'
@ -121,15 +120,15 @@ class MyHandler(BaseHTTPRequestHandler):
' deviceClass="pc"' ' deviceClass="pc"'
'/>' '/>'
'</MediaContainer>' '</MediaContainer>'
% (getXMLHeader(), % (v.XML_HEADER,
settings['client_name'], settings['client_name'],
settings['uuid'], settings['uuid'],
settings['platform'], settings['platform'],
settings['plexbmc_version'])) settings['plexbmc_version']))
log.debug("crafted resources response: %s" % resp) log.debug("crafted resources response: %s" % resp)
self.response(resp, js.getPlexHeaders()) self.response(resp, getXArgsDeviceInfo())
elif "/subscribe" in request_path: elif "/subscribe" in request_path:
self.response(getOKMsg(), js.getPlexHeaders()) self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo())
protocol = params.get('protocol', False) protocol = params.get('protocol', False)
host = self.client_address[0] host = self.client_address[0]
port = params.get('port', False) port = params.get('port', False)
@ -147,7 +146,7 @@ class MyHandler(BaseHTTPRequestHandler):
self.response( self.response(
sub(r"INSERTCOMMANDID", sub(r"INSERTCOMMANDID",
str(commandID), str(commandID),
subMgr.msg(js.getPlayers())), subMgr.msg(js.get_players())),
{ {
'X-Plex-Client-Identifier': settings['uuid'], 'X-Plex-Client-Identifier': settings['uuid'],
'Access-Control-Expose-Headers': 'Access-Control-Expose-Headers':
@ -156,14 +155,14 @@ class MyHandler(BaseHTTPRequestHandler):
'Content-Type': 'text/xml' 'Content-Type': 'text/xml'
}) })
elif "/unsubscribe" in request_path: elif "/unsubscribe" in request_path:
self.response(getOKMsg(), js.getPlexHeaders()) self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo())
uuid = self.headers.get('X-Plex-Client-Identifier', False) \ uuid = self.headers.get('X-Plex-Client-Identifier', False) \
or self.client_address[0] or self.client_address[0]
subMgr.removeSubscriber(uuid) subMgr.removeSubscriber(uuid)
else: else:
# Throw it to companion.py # Throw it to companion.py
process_command(request_path, params, self.server.queue) process_command(request_path, params, self.server.queue)
self.response('', js.getPlexHeaders()) self.response('', getXArgsDeviceInfo())
subMgr.notify() subMgr.notify()
except: except:
log.error('Error encountered. Traceback:') log.error('Error encountered. Traceback:')
@ -174,17 +173,16 @@ class MyHandler(BaseHTTPRequestHandler):
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True daemon_threads = True
def __init__(self, client, subscriptionManager, jsonClass, settings, def __init__(self, client, subscriptionManager, settings,
queue, *args, **kwargs): queue, *args, **kwargs):
""" """
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to- client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
date serverlist without instantiating anything date serverlist without instantiating anything
same for SubscriptionManager and jsonClass same for SubscriptionManager
""" """
self.client = client self.client = client
self.subscriptionManager = subscriptionManager self.subscriptionManager = subscriptionManager
self.jsonClass = jsonClass
self.settings = settings self.settings = settings
self.queue = queue self.queue = queue
HTTPServer.__init__(self, *args, **kwargs) HTTPServer.__init__(self, *args, **kwargs)

View file

@ -2,12 +2,15 @@ import logging
import re import re
import threading import threading
from xbmc import sleep
import downloadutils import downloadutils
from clientinfo import getXArgsDeviceInfo from clientinfo import getXArgsDeviceInfo
from utils import window from utils import window, kodi_time_to_millis
import PlexFunctions as pf import PlexFunctions as pf
import state import state
from functions import * import variables as v
import json_rpc as js
############################################################################### ###############################################################################
@ -17,7 +20,7 @@ log = logging.getLogger("PLEX."+__name__)
class SubscriptionManager: class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr, player, mgr): def __init__(self, RequestMgr, player, mgr):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
@ -30,8 +33,6 @@ class SubscriptionManager:
'audio': {}, 'audio': {},
'picture': {} 'picture': {}
} }
self.volume = 0
self.mute = '0'
self.server = "" self.server = ""
self.protocol = "http" self.protocol = "http"
self.port = "" self.port = ""
@ -40,7 +41,6 @@ class SubscriptionManager:
self.xbmcplayer = player self.xbmcplayer = player
self.playqueue = mgr.playqueue self.playqueue = mgr.playqueue
self.js = jsonClass
self.RequestMgr = RequestMgr self.RequestMgr = RequestMgr
def getServerByHost(self, host): def getServerByHost(self, host):
@ -52,32 +52,34 @@ class SubscriptionManager:
return server return server
return {} return {}
def getVolume(self):
self.volume, self.mute = self.js.getVolume()
def msg(self, players): def msg(self, players):
msg = getXMLHeader() log.debug('players: %s', players)
msg = v.XML_HEADER
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"' msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
msg += ' machineIdentifier="%s" location="fullScreenVideo">' % window('plex_client_Id') msg += ' machineIdentifier="%s" location="fullScreenVideo">' % window('plex_client_Id')
msg += self.getTimelineXML(self.js.getAudioPlayerId(players), plex_audio()) msg += self.getTimelineXML(players.get(v.KODI_TYPE_AUDIO),
msg += self.getTimelineXML(self.js.getPhotoPlayerId(players), plex_photo()) v.PLEX_TYPE_AUDIO)
msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video()) msg += self.getTimelineXML(players.get(v.KODI_TYPE_PHOTO),
v.PLEX_TYPE_PHOTO)
msg += self.getTimelineXML(players.get(v.KODI_TYPE_VIDEO),
v.PLEX_TYPE_VIDEO)
msg += "\n</MediaContainer>" msg += "\n</MediaContainer>"
return msg return msg
def getTimelineXML(self, playerid, ptype): def getTimelineXML(self, player, ptype):
if playerid is not None: if player is None:
status = 'stopped'
time = 0
else:
playerid = player['playerid']
info = self.getPlayerProperties(playerid) info = self.getPlayerProperties(playerid)
# save this info off so the server update can use it too # save this info off so the server update can use it too
self.playerprops[playerid] = info self.playerprops[playerid] = info
status = info['state'] status = info['state']
time = info['time'] time = info['time']
else:
status = "stopped"
time = 0
ret = ('\n <Timeline state="%s" time="%s" type="%s"' ret = ('\n <Timeline state="%s" time="%s" type="%s"'
% (status, time, ptype)) % (status, time, ptype))
if playerid is None: if player is None:
ret += ' />' ret += ' />'
return ret return ret
@ -89,10 +91,10 @@ class SubscriptionManager:
keyid = None keyid = None
count = 0 count = 0
while not keyid: while not keyid:
if count > 300: if count > 30:
break break
keyid = window('plex_currently_playing_itemid') keyid = window('plex_currently_playing_itemid')
xbmc.sleep(100) sleep(100)
count += 1 count += 1
if keyid: if keyid:
self.lastkey = "/library/metadata/%s" % keyid self.lastkey = "/library/metadata/%s" % keyid
@ -119,7 +121,7 @@ class SubscriptionManager:
ret += ' port="%s"' % serv.get('port', self.port) ret += ' port="%s"' % serv.get('port', self.port)
ret += ' volume="%s"' % info['volume'] ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle'] ret += ' shuffle="%s"' % info['shuffle']
ret += ' mute="%s"' % self.mute ret += ' mute="%s"' % info['mute']
ret += ' repeat="%s"' % info['repeat'] ret += ' repeat="%s"' % info['repeat']
ret += ' itemType="%s"' % ptype ret += ' itemType="%s"' % ptype
if state.PLEX_TRANSIENT_TOKEN: if state.PLEX_TRANSIENT_TOKEN:
@ -145,7 +147,7 @@ class SubscriptionManager:
if (not window('plex_currently_playing_itemid') if (not window('plex_currently_playing_itemid')
and not self.lastplayers): and not self.lastplayers):
return True return True
players = self.js.getPlayers() players = js.get_players()
# fetch the message, subscribers or not, since the server # fetch the message, subscribers or not, since the server
# will need the info anyway # will need the info anyway
msg = self.msg(players) msg = self.msg(players)
@ -233,27 +235,15 @@ class SubscriptionManager:
# Get the playqueue # Get the playqueue
playqueue = self.playqueue.playqueues[playerid] playqueue = self.playqueue.playqueues[playerid]
# get info from the player # get info from the player
props = self.js.jsonrpc( props = js.get_player_props(playerid)
"Player.GetProperties",
{"playerid": playerid,
"properties": ["type",
"time",
"totaltime",
"speed",
"shuffled",
"repeat"]})
info = { info = {
'time': timeToMillis(props['time']), 'time': kodi_time_to_millis(props['time']),
'duration': timeToMillis(props['totaltime']), 'duration': kodi_time_to_millis(props['totaltime']),
'state': ("paused", "playing")[int(props['speed'])], 'state': ("paused", "playing")[int(props['speed'])],
'shuffle': ("0", "1")[props.get('shuffled', False)], 'shuffle': ("0", "1")[props.get('shuffled', False)],
'repeat': pf.getPlexRepeat(props.get('repeat')), 'repeat': pf.getPlexRepeat(props.get('repeat')),
} }
# Get the playlist position pos = props['position']
pos = self.js.jsonrpc(
"Player.GetProperties",
{"playerid": playerid,
"properties": ["position"]})['position']
try: try:
info['playQueueItemID'] = playqueue.items[pos].ID or 'null' info['playQueueItemID'] = playqueue.items[pos].ID or 'null'
info['guid'] = playqueue.items[pos].guid or 'null' info['guid'] = playqueue.items[pos].guid or 'null'
@ -274,8 +264,8 @@ class SubscriptionManager:
} }
# get the volume from the application # get the volume from the application
info['volume'] = self.volume info['volume'] = js.get_volume()
info['mute'] = self.mute info['mute'] = js.get_muted()
info['plex_transient_token'] = playqueue.plex_transient_token info['plex_transient_token'] = playqueue.plex_transient_token

View file

@ -179,9 +179,15 @@ def dialog(typus, *args, **kwargs):
return types[typus](*args, **kwargs) return types[typus](*args, **kwargs)
def milliseconds_to_kodi_time(milliseconds): def millis_to_kodi_time(milliseconds):
""" """
Converts time in milliseconds to the time dict used by the Kodi JSON RPC Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
Pass in the time in milliseconds as an int Pass in the time in milliseconds as an int
""" """
seconds = milliseconds / 1000 seconds = milliseconds / 1000
@ -196,6 +202,22 @@ def milliseconds_to_kodi_time(milliseconds):
'milliseconds': milliseconds} 'milliseconds': milliseconds}
def kodi_time_to_millis(time):
"""
Converts the Kodi time dict
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
to milliseconds [int]
"""
return (time['hours']*3600 +
time['minutes']*60 +
time['seconds'])*1000 + time['milliseconds']
def tryEncode(uniString, encoding='utf-8'): def tryEncode(uniString, encoding='utf-8'):
""" """
Will try to encode uniString (in unicode) to encoding. This possibly Will try to encode uniString (in unicode) to encoding. This possibly

View file

@ -361,3 +361,8 @@ SORT_METHODS_ALBUMS = (
'SORT_METHOD_ARTIST', 'SORT_METHOD_ARTIST',
'SORT_METHOD_ALBUM', 'SORT_METHOD_ALBUM',
) )
XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'
COMPANION_OK_MESSAGE = XML_HEADER + '<Response code="200" status="OK" />'