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 utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from plexbmchelper import listener, plexgdm, subscribers, httppersist, \
plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml
@ -196,9 +196,8 @@ class PlexCompanion(Thread):
# Start up instances
requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.mgr)
requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100)
self.queue = queue
@ -211,7 +210,6 @@ class PlexCompanion(Thread):
httpd = listener.ThreadedHTTPServer(
client,
subscriptionManager,
jsonClass,
self.settings,
queue,
('', 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
"""
from json import loads, dumps
from utils import milliseconds_to_kodi_time
from utils import millis_to_kodi_time
from xbmc import executeJSONRPC
@ -49,7 +49,7 @@ def get_players():
'picture': ...
}
"""
info = jsonrpc("Player.GetActivePlayers").execute()['result'] or []
info = jsonrpc("Player.GetActivePlayers").execute()['result']
ret = {}
for player in info:
player['playerid'] = int(player['playerid'])
@ -152,7 +152,7 @@ def seek_to(offset):
for playerid in get_player_ids():
jsonrpc("Player.Seek").execute(
{"playerid": playerid,
"value": milliseconds_to_kodi_time(offset)})
"value": millis_to_kodi_time(offset)})
def smallforward():
@ -240,6 +240,13 @@ def input_back():
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):
"""
playlistid: [int] id of the Kodi playlist
@ -350,6 +357,33 @@ def get_episodes(params):
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):
"""
Returns a dict of the active audiostream for playerid [int]:
@ -402,3 +436,10 @@ def subtitle_enabled(playerid):
except (KeyError, TypeError):
ret = False
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 companion import process_command
from utils import window
from functions import *
import json_rpc as js
from clientinfo import getXArgsDeviceInfo
import variables as v
###############################################################################
@ -82,7 +82,6 @@ class MyHandler(BaseHTTPRequestHandler):
def answer_request(self, sendData):
self.serverlist = self.server.client.getServerList()
subMgr = self.server.subscriptionManager
js = self.server.jsonClass
settings = self.server.settings
try:
@ -105,7 +104,7 @@ class MyHandler(BaseHTTPRequestHandler):
% settings['version'])
elif request_path == "verify":
self.response("XBMC JSON connection test:\n" +
js.jsonrpc("ping"))
js.ping())
elif "resources" == request_path:
resp = ('%s'
'<MediaContainer>'
@ -121,15 +120,15 @@ class MyHandler(BaseHTTPRequestHandler):
' deviceClass="pc"'
'/>'
'</MediaContainer>'
% (getXMLHeader(),
% (v.XML_HEADER,
settings['client_name'],
settings['uuid'],
settings['platform'],
settings['plexbmc_version']))
log.debug("crafted resources response: %s" % resp)
self.response(resp, js.getPlexHeaders())
self.response(resp, getXArgsDeviceInfo())
elif "/subscribe" in request_path:
self.response(getOKMsg(), js.getPlexHeaders())
self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo())
protocol = params.get('protocol', False)
host = self.client_address[0]
port = params.get('port', False)
@ -147,7 +146,7 @@ class MyHandler(BaseHTTPRequestHandler):
self.response(
sub(r"INSERTCOMMANDID",
str(commandID),
subMgr.msg(js.getPlayers())),
subMgr.msg(js.get_players())),
{
'X-Plex-Client-Identifier': settings['uuid'],
'Access-Control-Expose-Headers':
@ -156,14 +155,14 @@ class MyHandler(BaseHTTPRequestHandler):
'Content-Type': 'text/xml'
})
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) \
or self.client_address[0]
subMgr.removeSubscriber(uuid)
else:
# Throw it to companion.py
process_command(request_path, params, self.server.queue)
self.response('', js.getPlexHeaders())
self.response('', getXArgsDeviceInfo())
subMgr.notify()
except:
log.error('Error encountered. Traceback:')
@ -174,17 +173,16 @@ class MyHandler(BaseHTTPRequestHandler):
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
def __init__(self, client, subscriptionManager, jsonClass, settings,
def __init__(self, client, subscriptionManager, settings,
queue, *args, **kwargs):
"""
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
date serverlist without instantiating anything
same for SubscriptionManager and jsonClass
same for SubscriptionManager
"""
self.client = client
self.subscriptionManager = subscriptionManager
self.jsonClass = jsonClass
self.settings = settings
self.queue = queue
HTTPServer.__init__(self, *args, **kwargs)

View file

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

View file

@ -179,9 +179,15 @@ def dialog(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
"""
seconds = milliseconds / 1000
@ -196,6 +202,22 @@ def milliseconds_to_kodi_time(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'):
"""
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_ALBUM',
)
XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'
COMPANION_OK_MESSAGE = XML_HEADER + '<Response code="200" status="OK" />'