Major Plex Companion overhaul, part 3
This commit is contained in:
parent
80c106d57f
commit
c0e7c78a11
3 changed files with 168 additions and 166 deletions
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
Loading…
Reference in a new issue