Major Plex Companion overhaul, part 3

This commit is contained in:
croneter 2017-12-14 08:29:38 +01:00
parent 80c106d57f
commit c0e7c78a11
3 changed files with 168 additions and 166 deletions

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- """
The Plex Companion master python file
"""
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
from Queue import Queue, Empty from Queue import Queue, Empty
@ -18,7 +20,7 @@ import state
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
@ -26,39 +28,24 @@ log = getLogger("PLEX."+__name__)
@thread_methods(add_suspends=['PMS_STATUS']) @thread_methods(add_suspends=['PMS_STATUS'])
class PlexCompanion(Thread): class PlexCompanion(Thread):
""" """
Plex Companion monitoring class. Invoke only once
""" """
def __init__(self, callback=None): def __init__(self, callback=None):
log.info("----===## Starting PlexCompanion ##===----") LOG.info("----===## Starting PlexCompanion ##===----")
if callback is not None: if callback is not None:
self.mgr = callback self.mgr = callback
# Start GDM for server/client discovery # Start GDM for server/client discovery
self.client = plexgdm.plexgdm() self.client = plexgdm.plexgdm()
self.client.clientDetails() self.client.clientDetails()
log.debug("Registration string is:\n%s" LOG.debug("Registration string is:\n%s",
% self.client.getClientDetails()) self.client.getClientDetails())
# kodi player instance # kodi player instance
self.player = player.PKC_Player() self.player = player.PKC_Player()
self.httpd = False
self.queue = None
Thread.__init__(self) Thread.__init__(self)
def _getStartItem(self, string): def _process_tasks(self, task):
"""
Grabs the Plex id from e.g. '/library/metadata/12987'
and returns the tuple (typus, id) where typus is either 'queueId' or
'plexId' and id is the corresponding id as a string
"""
typus = 'plexId'
if string.startswith('/library/metadata'):
try:
string = string.split('/')[3]
except IndexError:
string = ''
else:
log.error('Unknown string! %s' % string)
return typus, string
def processTasks(self, task):
""" """
Processes tasks picked up e.g. by Companion listener, e.g. Processes tasks picked up e.g. by Companion listener, e.g.
{'action': 'playlist', {'action': 'playlist',
@ -73,7 +60,7 @@ class PlexCompanion(Thread):
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd', 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
'type': 'video'}} 'type': 'video'}}
""" """
log.debug('Processing: %s' % task) LOG.debug('Processing: %s', task)
data = task['data'] data = task['data']
# Get the token of the user flinging media (might be different one) # Get the token of the user flinging media (might be different one)
@ -84,11 +71,11 @@ class PlexCompanion(Thread):
try: try:
xml[0].attrib xml[0].attrib
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
log.error('Could not download Plex metadata') LOG.error('Could not download Plex metadata')
return return
api = API(xml[0]) api = API(xml[0])
if api.getType() == v.PLEX_TYPE_ALBUM: if api.getType() == v.PLEX_TYPE_ALBUM:
log.debug('Plex music album detected') LOG.debug('Plex music album detected')
queue = self.mgr.playqueue.init_playqueue_from_plex_children( queue = self.mgr.playqueue.init_playqueue_from_plex_children(
api.getRatingKey()) api.getRatingKey())
queue.plex_transient_token = token queue.plex_transient_token = token
@ -120,11 +107,11 @@ class PlexCompanion(Thread):
elif task['action'] == 'playlist': elif task['action'] == 'playlist':
# Get the playqueue ID # Get the playqueue ID
try: try:
typus, ID, query = ParseContainerKey(data['containerKey']) _, plex_id, query = ParseContainerKey(data['containerKey'])
except Exception as e: except:
log.error('Exception while processing: %s' % e) LOG.error('Exception while processing')
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) LOG.error("Traceback:\n%s", traceback.format_exc())
return return
try: try:
playqueue = self.mgr.playqueue.get_playqueue_from_type( playqueue = self.mgr.playqueue.get_playqueue_from_type(
@ -136,14 +123,14 @@ class PlexCompanion(Thread):
try: try:
xml[0].attrib xml[0].attrib
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
log.error('Could not download Plex metadata') LOG.error('Could not download Plex metadata')
return return
api = API(xml[0]) api = API(xml[0])
playqueue = self.mgr.playqueue.get_playqueue_from_type( playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
self.mgr.playqueue.update_playqueue_from_PMS( self.mgr.playqueue.update_playqueue_from_PMS(
playqueue, playqueue,
ID, plex_id,
repeat=query.get('repeat'), repeat=query.get('repeat'),
offset=data.get('offset')) offset=data.get('offset'))
playqueue.plex_transient_token = token playqueue.plex_transient_token = token
@ -154,7 +141,7 @@ class PlexCompanion(Thread):
if xml is None: if xml is None:
return return
if len(xml) == 0: if len(xml) == 0:
log.debug('Empty playqueue received - clearing playqueue') LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml) plex_type = get_plextype_from_xml(xml)
if plex_type is None: if plex_type is None:
return return
@ -169,9 +156,11 @@ class PlexCompanion(Thread):
data['playQueueID']) data['playQueueID'])
def run(self): def run(self):
# Ensure that sockets will be closed no matter what """
Ensure that sockets will be closed no matter what
"""
try: try:
self.__run() self._run()
finally: finally:
try: try:
self.httpd.socket.shutdown(SHUT_RDWR) self.httpd.socket.shutdown(SHUT_RDWR)
@ -182,10 +171,9 @@ class PlexCompanion(Thread):
self.httpd.socket.close() self.httpd.socket.close()
except AttributeError: except AttributeError:
pass pass
log.info("----===## Plex Companion stopped ##===----") LOG.info("----===## Plex Companion stopped ##===----")
def __run(self): def _run(self):
self.httpd = False
httpd = self.httpd httpd = self.httpd
# Cache for quicker while loops # Cache for quicker while loops
client = self.client client = self.client
@ -193,10 +181,9 @@ class PlexCompanion(Thread):
thread_suspended = self.thread_suspended thread_suspended = self.thread_suspended
# Start up instances # Start up instances
requestMgr = httppersist.RequestMgr() request_mgr = httppersist.RequestMgr()
subscriptionManager = subscribers.SubscriptionManager( subscription_manager = subscribers.SubscriptionMgr(
requestMgr, self.player, self.mgr) request_mgr, self.player, self.mgr)
queue = Queue(maxsize=100) queue = Queue(maxsize=100)
self.queue = queue self.queue = queue
@ -207,33 +194,28 @@ class PlexCompanion(Thread):
try: try:
httpd = listener.ThreadedHTTPServer( httpd = listener.ThreadedHTTPServer(
client, client,
subscriptionManager, subscription_manager,
queue, queue,
('', v.COMPANION_PORT), ('', v.COMPANION_PORT),
listener.MyHandler) listener.MyHandler)
httpd.timeout = 0.95 httpd.timeout = 0.95
break break
except: except:
log.error("Unable to start PlexCompanion. Traceback:") LOG.error("Unable to start PlexCompanion. Traceback:")
import traceback import traceback
log.error(traceback.print_exc()) LOG.error(traceback.print_exc())
sleep(3000) sleep(3000)
if start_count == 3: if start_count == 3:
log.error("Error: Unable to start web helper.") LOG.error("Error: Unable to start web helper.")
httpd = False httpd = False
break break
start_count += 1 start_count += 1
else: else:
log.info('User deactivated Plex Companion') LOG.info('User deactivated Plex Companion')
client.start_all() client.start_all()
message_count = 0 message_count = 0
if httpd: if httpd:
t = Thread(target=httpd.handle_request) thread = Thread(target=httpd.handle_request)
while not thread_stopped(): while not thread_stopped():
# If we are not authorized, sleep # If we are not authorized, sleep
@ -246,30 +228,30 @@ class PlexCompanion(Thread):
try: try:
message_count += 1 message_count += 1
if httpd: if httpd:
if not t.isAlive(): if not thread.isAlive():
# Use threads cause the method will stall # Use threads cause the method will stall
t = Thread(target=httpd.handle_request) thread = Thread(target=httpd.handle_request)
t.start() thread.start()
if message_count == 3000: if message_count == 3000:
message_count = 0 message_count = 0
if client.check_client_registration(): if client.check_client_registration():
log.debug("Client is still registered") LOG.debug('Client is still registered')
else: else:
log.debug("Client is no longer registered. " LOG.debug('Client is no longer registered. Plex '
"Plex Companion still running on port %s" 'Companion still running on port %s',
% v.COMPANION_PORT) v.COMPANION_PORT)
client.register_as_client() client.register_as_client()
# Get and set servers # Get and set servers
if message_count % 30 == 0: if message_count % 30 == 0:
subscriptionManager.serverlist = client.getServerList() subscription_manager.serverlist = client.getServerList()
subscriptionManager.notify() subscription_manager.notify()
if not httpd: if not httpd:
message_count = 0 message_count = 0
except: except:
log.warn("Error in loop, continuing anyway. Traceback:") LOG.warn("Error in loop, continuing anyway. Traceback:")
import traceback import traceback
log.warn(traceback.format_exc()) LOG.warn(traceback.format_exc())
# See if there's anything we need to process # See if there's anything we need to process
try: try:
task = queue.get(block=False) task = queue.get(block=False)
@ -277,10 +259,9 @@ class PlexCompanion(Thread):
pass pass
else: else:
# Got instructions, process them # Got instructions, process them
self.processTasks(task) self._process_tasks(task)
queue.task_done() queue.task_done()
# Don't sleep # Don't sleep
continue continue
sleep(50) sleep(50)
client.stop_all() client.stop_all()

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- """
import logging Plex Companion listener
"""
from logging import getLogger
from re import sub from re import sub
from SocketServer import ThreadingMixIn from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
@ -7,40 +9,33 @@ 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
import json_rpc as js import json_rpc as js
from clientinfo import getXArgsDeviceInfo from clientinfo import getXArgsDeviceInfo
import variables as v import variables as v
############################################################################### ###############################################################################
log = logging.getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
class MyHandler(BaseHTTPRequestHandler): class MyHandler(BaseHTTPRequestHandler):
"""
BaseHTTPRequestHandler implementation of Plex Companion listener
"""
protocol_version = 'HTTP/1.1' protocol_version = 'HTTP/1.1'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
BaseHTTPRequestHandler.__init__(self, *args, **kwargs) BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
self.serverlist = [] self.serverlist = []
def getServerByHost(self, host):
if len(self.serverlist) == 1:
return self.serverlist[0]
for server in self.serverlist:
if (server.get('serverName') in host or
server.get('server') in host):
return server
return {}
def do_HEAD(self): def do_HEAD(self):
log.debug("Serving HEAD request...") LOG.debug("Serving HEAD request...")
self.answer_request(0) self.answer_request(0)
def do_GET(self): def do_GET(self):
log.debug("Serving GET request...") LOG.debug("Serving GET request...")
self.answer_request(1) self.answer_request(1)
def do_OPTIONS(self): def do_OPTIONS(self):
@ -65,7 +60,8 @@ class MyHandler(BaseHTTPRequestHandler):
def sendOK(self): def sendOK(self):
self.send_response(200) self.send_response(200)
def response(self, body, headers={}, code=200): def response(self, body, headers=None, code=200):
headers = {} if headers is None else headers
try: try:
self.send_response(code) self.send_response(code)
for key in headers: for key in headers:
@ -78,9 +74,9 @@ class MyHandler(BaseHTTPRequestHandler):
except: except:
pass pass
def answer_request(self, sendData): def answer_request(self, send_data):
self.serverlist = self.server.client.getServerList() self.serverlist = self.server.client.getServerList()
subMgr = self.server.subscriptionManager sub_mgr = self.server.subscription_manager
try: try:
request_path = self.path[1:] request_path = self.path[1:]
@ -90,9 +86,9 @@ class MyHandler(BaseHTTPRequestHandler):
params = {} params = {}
for key in paramarrays: for key in paramarrays:
params[key] = paramarrays[key][0] params[key] = paramarrays[key][0]
log.debug("remote request_path: %s" % request_path) LOG.debug("remote request_path: %s", request_path)
log.debug("params received from remote: %s" % params) LOG.debug("params received from remote: %s", params)
subMgr.updateCommandID(self.headers.get( sub_mgr.update_command_id(self.headers.get(
'X-Plex-Client-Identifier', 'X-Plex-Client-Identifier',
self.client_address[0]), self.client_address[0]),
params.get('commandID', False)) params.get('commandID', False))
@ -123,7 +119,7 @@ class MyHandler(BaseHTTPRequestHandler):
v.PKC_MACHINE_IDENTIFIER, v.PKC_MACHINE_IDENTIFIER,
v.PLATFORM, v.PLATFORM,
v.ADDON_VERSION)) v.ADDON_VERSION))
log.debug("crafted resources response: %s" % resp) LOG.debug("crafted resources response: %s", resp)
self.response(resp, getXArgsDeviceInfo(include_token=False)) self.response(resp, getXArgsDeviceInfo(include_token=False))
elif "/subscribe" in request_path: elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE, self.response(v.COMPANION_OK_MESSAGE,
@ -132,20 +128,20 @@ class MyHandler(BaseHTTPRequestHandler):
host = self.client_address[0] host = self.client_address[0]
port = params.get('port', False) port = params.get('port', False)
uuid = self.headers.get('X-Plex-Client-Identifier', "") uuid = self.headers.get('X-Plex-Client-Identifier', "")
commandID = params.get('commandID', 0) command_id = params.get('commandID', 0)
subMgr.addSubscriber(protocol, sub_mgr.add_subscriber(protocol,
host, host,
port, port,
uuid, uuid,
commandID) command_id)
elif "/poll" in request_path: elif "/poll" in request_path:
if params.get('wait', False) == '1': if params.get('wait', False) == '1':
sleep(950) sleep(950)
commandID = params.get('commandID', 0) command_id = params.get('commandID', 0)
self.response( self.response(
sub(r"INSERTCOMMANDID", sub(r"INSERTCOMMANDID",
str(commandID), str(command_id),
subMgr.msg(js.get_players())), sub_mgr.msg(js.get_players())),
{ {
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0', 'X-Plex-Protocol': '1.0',
@ -160,29 +156,32 @@ class MyHandler(BaseHTTPRequestHandler):
getXArgsDeviceInfo(include_token=False)) getXArgsDeviceInfo(include_token=False))
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) sub_mgr.remove_subscriber(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('', getXArgsDeviceInfo(include_token=False)) self.response('', getXArgsDeviceInfo(include_token=False))
subMgr.notify() sub_mgr.notify()
except: except:
log.error('Error encountered. Traceback:') LOG.error('Error encountered. Traceback:')
import traceback import traceback
log.error(traceback.print_exc()) LOG.error(traceback.print_exc())
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""
Using ThreadingMixIn Thread magic
"""
daemon_threads = True daemon_threads = True
def __init__(self, client, subscriptionManager, queue, *args, **kwargs): def __init__(self, client, subscription_manager, 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 same for SubscriptionMgr
""" """
self.client = client self.client = client
self.subscriptionManager = subscriptionManager self.subscription_manager = subscription_manager
self.queue = queue self.queue = queue
HTTPServer.__init__(self, *args, **kwargs) HTTPServer.__init__(self, *args, **kwargs)

View file

@ -6,7 +6,7 @@ from logging import getLogger
from re import sub from re import sub
from threading import Thread, RLock from threading import Thread, RLock
import downloadutils from downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis from utils import window, kodi_time_to_millis
import state import state
import variables as v import variables as v
@ -22,20 +22,22 @@ LOG = getLogger("PLEX." + __name__)
CONTROLLABLE = { CONTROLLABLE = {
v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop',
v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
'skipPrevious,skipNext,stepBack,stepForward', 'skipPrevious,skipNext,stepBack,stepForward',
v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
'subtitleStream,seekTo,skipPrevious,skipNext,stepBack,stepForward' 'subtitleStream,seekTo,skipPrevious,skipNext,'
'stepBack,stepForward'
} }
class SubscriptionManager:
class SubscriptionMgr(object):
""" """
Manages Plex companion subscriptions Manages Plex companion subscriptions
""" """
def __init__(self, RequestMgr, player, mgr): def __init__(self, request_mgr, player, mgr):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
self.containerKey = None self.container_key = None
self.ratingkey = None self.ratingkey = None
self.server = "" self.server = ""
self.protocol = "http" self.protocol = "http"
@ -44,10 +46,9 @@ class SubscriptionManager:
self.last_params = {} self.last_params = {}
self.lastplayers = {} self.lastplayers = {}
self.doUtils = downloadutils.DownloadUtils
self.xbmcplayer = player self.xbmcplayer = player
self.playqueue = mgr.playqueue self.playqueue = mgr.playqueue
self.RequestMgr = RequestMgr self.request_mgr = request_mgr
@staticmethod @staticmethod
def _headers(): def _headers():
@ -63,7 +64,7 @@ class SubscriptionManager:
'X-Plex-Protocol': "1.0" 'X-Plex-Protocol': "1.0"
} }
def getServerByHost(self, host): def _server_by_host(self, host):
if len(self.serverlist) == 1: if len(self.serverlist) == 1:
return self.serverlist[0] return self.serverlist[0]
for server in self.serverlist: for server in self.serverlist:
@ -72,17 +73,17 @@ class SubscriptionManager:
return server return server
return {} return {}
def msg(self, players): def _msg(self, players):
LOG.debug('players: %s', players) LOG.debug('players: %s', players)
msg = v.XML_HEADER msg = v.XML_HEADER
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"' msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
msg += ' machineIdentifier="%s">\n' % v.PKC_MACHINE_IDENTIFIER msg += ' machineIdentifier="%s">\n' % v.PKC_MACHINE_IDENTIFIER
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_AUDIO), msg += self._timeline_xml(players.get(v.KODI_TYPE_AUDIO),
v.PLEX_TYPE_AUDIO) v.PLEX_TYPE_AUDIO)
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_PHOTO), msg += self._timeline_xml(players.get(v.KODI_TYPE_PHOTO),
v.PLEX_TYPE_PHOTO) v.PLEX_TYPE_PHOTO)
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_VIDEO), msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO),
v.PLEX_TYPE_VIDEO) v.PLEX_TYPE_VIDEO)
msg += "</MediaContainer>" msg += "</MediaContainer>"
LOG.debug('msg is: %s', msg) LOG.debug('msg is: %s', msg)
return msg return msg
@ -104,7 +105,7 @@ class SubscriptionManager:
state.PLAYER_STATES[playerid]['plex_id'] state.PLAYER_STATES[playerid]['plex_id']
return key return key
def get_timeline_xml(self, player, ptype): def _timeline_xml(self, player, ptype):
if player is None: if player is None:
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \ return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype) 'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
@ -124,7 +125,7 @@ class SubscriptionManager:
muted = '1' if info['muted'] is True else '0' muted = '1' if info['muted'] is True else '0'
ret += ' mute="%s"' % muted ret += ' mute="%s"' % muted
pbmc_server = window('pms_server') pbmc_server = window('pms_server')
server = self.getServerByHost(self.server) server = self._server_by_host(self.server)
if pbmc_server: if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':') (self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '') self.server = self.server.replace('/', '')
@ -136,16 +137,16 @@ class SubscriptionManager:
playqueue = self.playqueue.playqueues[playerid] playqueue = self.playqueue.playqueues[playerid]
key = self._get_container_key(playerid) key = self._get_container_key(playerid)
if key is not None and key.startswith('/playQueues'): if key is not None and key.startswith('/playQueues'):
self.containerKey = key self.container_key = key
ret += ' containerKey="%s"' % self.containerKey ret += ' containerKey="%s"' % self.container_key
pos = info['position'] pos = info['position']
ret += ' playQueueItemID="%s"' % playqueue.items[pos].id or 'null' ret += ' playQueueItemID="%s"' % playqueue.items[pos].id or 'null'
ret += ' playQueueID="%s"' % playqueue.id or 'null' ret += ' playQueueID="%s"' % playqueue.id or 'null'
ret += ' playQueueVersion="%s"' % playqueue.version or 'null' ret += ' playQueueVersion="%s"' % playqueue.version or 'null'
ret += ' guid="%s"' % playqueue.items[pos].guid or 'null' ret += ' guid="%s"' % playqueue.items[pos].guid or 'null'
elif key: elif key:
self.containerKey = key self.container_key = key
ret += ' containerKey="%s"' % self.containerKey ret += ' containerKey="%s"' % self.container_key
ret += ' machineIdentifier="%s"' % server.get('uuid', "") ret += ' machineIdentifier="%s"' % server.get('uuid', "")
ret += ' protocol="%s"' % server.get('protocol', 'http') ret += ' protocol="%s"' % server.get('protocol', 'http')
ret += ' address="%s"' % server.get('server', self.server) ret += ' address="%s"' % server.get('server', self.server)
@ -162,26 +163,34 @@ class SubscriptionManager:
ret += '/>\n' ret += '/>\n'
return ret return ret
def updateCommandID(self, uuid, commandID): def update_command_id(self, uuid, command_id):
if commandID and self.subscribers.get(uuid, False): """
self.subscribers[uuid].commandID = int(commandID) Updates the Plex Companien client with the machine identifier uuid with
command_id
"""
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
def notify(self, event=False): def notify(self):
self.cleanup() """
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
self._cleanup()
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback? # Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
players = js.get_players() 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)
if self.subscribers: if self.subscribers:
with RLock(): with RLock():
for subscriber in self.subscribers.values(): for subscriber in self.subscribers.values():
subscriber.send_update(msg, len(players) == 0) subscriber.send_update(msg, not players)
self.notifyServer(players) self._notify_server(players)
self.lastplayers = players self.lastplayers = players
return True return True
def notifyServer(self, players): def _notify_server(self, players):
for typus, player in players.iteritems(): for typus, player in players.iteritems():
self._send_pms_notification( self._send_pms_notification(
player['playerid'], self._get_pms_params(player['playerid'])) player['playerid'], self._get_pms_params(player['playerid']))
@ -204,10 +213,10 @@ class SubscriptionManager:
'time': kodi_time_to_millis(info['time']), 'time': kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime']) 'duration': kodi_time_to_millis(info['totaltime'])
} }
if self.containerKey: if self.container_key:
params['containerKey'] = self.containerKey params['containerKey'] = self.container_key
if self.containerKey is not None and \ if self.container_key is not None and \
self.containerKey.startswith('/playQueues/'): self.container_key.startswith('/playQueues/'):
playqueue = self.playqueue.playqueues[playerid] playqueue = self.playqueue.playqueues[playerid]
params['playQueueVersion'] = playqueue.version params['playQueueVersion'] = playqueue.version
params['playQueueItemID'] = playqueue.id params['playQueueItemID'] = playqueue.id
@ -215,7 +224,7 @@ class SubscriptionManager:
return params return params
def _send_pms_notification(self, playerid, params): def _send_pms_notification(self, playerid, params):
serv = self.getServerByHost(self.server) serv = self._server_by_host(self.server)
xargs = self._headers() xargs = self._headers()
playqueue = self.playqueue.playqueues[playerid] playqueue = self.playqueue.playqueues[playerid]
if state.PLEX_TRANSIENT_TOKEN: if state.PLEX_TRANSIENT_TOKEN:
@ -225,32 +234,39 @@ class SubscriptionManager:
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'), serv.get('server', 'localhost'),
serv.get('port', '32400')) serv.get('port', '32400'))
self.doUtils().downloadUrl( DU().downloadUrl(url, parameters=params, headerOptions=xargs)
url, parameters=params, headerOptions=xargs)
# Save to be able to signal a stop at the end # Save to be able to signal a stop at the end
LOG.debug("Sent server notification with parameters: %s to %s", LOG.debug("Sent server notification with parameters: %s to %s",
params, url) params, url)
def addSubscriber(self, protocol, host, port, uuid, commandID): def add_subscriber(self, protocol, host, port, uuid, command_id):
"""
Adds a new Plex Companion subscriber to PKC.
"""
subscriber = Subscriber(protocol, subscriber = Subscriber(protocol,
host, host,
port, port,
uuid, uuid,
commandID, command_id,
self, self,
self.RequestMgr) self.request_mgr)
with RLock(): with RLock():
self.subscribers[subscriber.uuid] = subscriber self.subscribers[subscriber.uuid] = subscriber
return subscriber return subscriber
def removeSubscriber(self, uuid): def remove_subscriber(self, uuid):
"""
Removes a connected Plex Companion subscriber with machine identifier
uuid from PKC notifications.
(Calls the cleanup() method of the subscriber)
"""
with RLock(): with RLock():
for subscriber in self.subscribers.values(): for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid: if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup() subscriber.cleanup()
del self.subscribers[subscriber.uuid] del self.subscribers[subscriber.uuid]
def cleanup(self): def _cleanup(self):
with RLock(): with RLock():
for subscriber in self.subscribers.values(): for subscriber in self.subscribers.values():
if subscriber.age > 30: if subscriber.age > 30:
@ -258,27 +274,35 @@ class SubscriptionManager:
del self.subscribers[subscriber.uuid] del self.subscribers[subscriber.uuid]
class Subscriber: class Subscriber(object):
def __init__(self, protocol, host, port, uuid, commandID, """
subMgr, RequestMgr): Plex Companion subscribing device
"""
def __init__(self, protocol, host, port, uuid, command_id, sub_mgr,
request_mgr):
self.protocol = protocol or "http" self.protocol = protocol or "http"
self.host = host self.host = host
self.port = port or 32400 self.port = port or 32400
self.uuid = uuid or host self.uuid = uuid or host
self.commandID = int(commandID) or 0 self.command_id = int(command_id) or 0
self.navlocationsent = False self.navlocationsent = False
self.age = 0 self.age = 0
self.doUtils = downloadutils.DownloadUtils self.sub_mgr = sub_mgr
self.subMgr = subMgr self.request_mgr = request_mgr
self.RequestMgr = RequestMgr
def __eq__(self, other): def __eq__(self, other):
return self.uuid == other.uuid return self.uuid == other.uuid
def cleanup(self): def cleanup(self):
self.RequestMgr.closeConnection(self.protocol, self.host, self.port) """
Closes the connection to the Plex Companion client
"""
self.request_mgr.closeConnection(self.protocol, self.host, self.port)
def send_update(self, msg, is_nav): def send_update(self, msg, is_nav):
"""
Sends msg to the Plex Companion client (via .../:/timeline)
"""
self.age += 1 self.age += 1
if not is_nav: if not is_nav:
self.navlocationsent = False self.navlocationsent = False
@ -286,21 +310,19 @@ class Subscriber:
return True return True
else: else:
self.navlocationsent = True self.navlocationsent = True
msg = sub(r"INSERTCOMMANDID", str(self.commandID), msg) msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg)
LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
self.uuid, self.commandID, msg) self.uuid, self.command_id, msg)
url = self.protocol + '://' + self.host + ':' + self.port \ url = self.protocol + '://' + self.host + ':' + self.port \
+ "/:/timeline" + "/:/timeline"
t = Thread(target=self.threadedSend, args=(url, msg)) thread = Thread(target=self._threaded_send, args=(url, msg))
t.start() thread.start()
def threadedSend(self, url, msg): def _threaded_send(self, url, msg):
""" """
Threaded POST request, because they stall due to PMS response missing Threaded POST request, because they stall due to PMS response missing
the Content-Length header :-( the Content-Length header :-(
""" """
response = self.doUtils().downloadUrl(url, response = DU().downloadUrl(url, postBody=msg, action_type="POST")
postBody=msg, if response in (False, None, 401):
action_type="POST") self.sub_mgr.remove_subscriber(self.uuid)
if response in [False, None, 401]:
self.subMgr.removeSubscriber(self.uuid)