diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 9cf71bf5..88e472b4 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -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']), diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 46da124f..9cb0d679 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -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() diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py deleted file mode 100644 index 784a1e77..00000000 --- a/resources/lib/plexbmchelper/functions.py +++ /dev/null @@ -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 '\n' - - -def getOKMsg(): - return getXMLHeader() + '' - - -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) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index c07e9c00..e177212f 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -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' '' @@ -121,15 +120,15 @@ class MyHandler(BaseHTTPRequestHandler): ' deviceClass="pc"' '/>' '' - % (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) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 82d4d833..5345fea4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -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 += ' 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 diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 5184b4d2..9ccc80b9 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -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 diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 856197d3..525bb29c 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -361,3 +361,8 @@ SORT_METHODS_ALBUMS = ( 'SORT_METHOD_ARTIST', 'SORT_METHOD_ALBUM', ) + + +XML_HEADER = '\n' + +COMPANION_OK_MESSAGE = XML_HEADER + ''