PlexKodiConnect/resources/lib/PlexCompanion.py

314 lines
12 KiB
Python
Raw Normal View History

2017-12-14 18:29:38 +11:00
"""
The Plex Companion master python file
"""
2017-12-10 00:35:08 +11:00
from logging import getLogger
2016-12-21 02:38:04 +11:00
from threading import Thread
2017-12-10 00:35:08 +11:00
from Queue import Queue, Empty
2016-12-21 02:38:04 +11:00
from socket import SHUT_RDWR
from urllib import urlencode
from xbmc import sleep, executebuiltin
2017-05-17 21:55:24 +10:00
from utils import settings, thread_methods
2017-12-10 02:30:52 +11:00
from plexbmchelper import listener, plexgdm, subscribers, httppersist
2017-12-21 19:28:06 +11:00
from plexbmchelper.subscribers import LOCKER
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml
import json_rpc as js
2016-08-07 23:33:36 +10:00
import player
2017-03-06 02:51:13 +11:00
import variables as v
2017-05-18 04:22:16 +10:00
import state
2017-03-05 03:54:24 +11:00
2016-09-03 01:20:19 +10:00
###############################################################################
2017-12-14 18:29:38 +11:00
LOG = getLogger("PLEX." + __name__)
2016-09-03 01:20:19 +10:00
###############################################################################
2017-05-17 21:55:24 +10:00
@thread_methods(add_suspends=['PMS_STATUS'])
2016-12-21 02:38:04 +11:00
class PlexCompanion(Thread):
2016-07-21 02:36:31 +10:00
"""
2017-12-14 18:29:38 +11:00
Plex Companion monitoring class. Invoke only once
2016-07-21 02:36:31 +10:00
"""
2016-12-28 03:33:52 +11:00
def __init__(self, callback=None):
2017-12-14 18:29:38 +11:00
LOG.info("----===## Starting PlexCompanion ##===----")
2016-12-28 03:33:52 +11:00
if callback is not None:
self.mgr = callback
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
2017-12-10 02:30:52 +11:00
self.client.clientDetails()
2017-12-14 18:29:38 +11:00
LOG.debug("Registration string is:\n%s",
self.client.getClientDetails())
2016-07-24 02:06:47 +10:00
# kodi player instance
2017-12-10 00:35:08 +11:00
self.player = player.PKC_Player()
2017-12-14 18:29:38 +11:00
self.httpd = False
self.queue = None
2017-12-21 19:28:06 +11:00
self.subscription_manager = None
2016-12-21 02:38:04 +11:00
Thread.__init__(self)
2017-12-21 19:28:06 +11:00
@LOCKER.lockthis
def _process_alexa(self, data):
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
api = API(xml[0])
if api.getType() == v.PLEX_TYPE_ALBUM:
LOG.debug('Plex music album detected')
queue = self.mgr.playqueue.init_playqueue_from_plex_children(
api.getRatingKey())
queue.plex_transient_token = data.get('token')
else:
state.PLEX_TRANSIENT_TOKEN = data.get('token')
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true',
'node': 'false'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
@staticmethod
def _process_node(data):
"""
E.g. watch later initiated by Companion. Basically navigating Plex
"""
state.PLEX_TRANSIENT_TOKEN = data.get('key')
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
@LOCKER.lockthis
def _process_playlist(self, data):
# Get the playqueue ID
try:
2017-12-29 07:31:05 +11:00
_, container_key, query = ParseContainerKey(data['containerKey'])
2017-12-21 19:28:06 +11:00
except:
LOG.error('Exception while processing')
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
return
try:
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
except KeyError:
# E.g. Plex web does not supply the media type
# Still need to figure out the type (video vs. music vs. pix)
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
api = API(xml[0])
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
2017-12-29 07:31:05 +11:00
if playqueue.id == container_key:
# OK, really weird, this happens at least with Plex for Android
LOG.debug('Already know this Plex playQueue, ignoring this command')
else:
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=data.get('offset'),
2017-12-29 07:32:12 +11:00
transient_token=data.get('token'))
2017-12-21 19:28:06 +11:00
@LOCKER.lockthis
def _process_streams(self, data):
"""
Plex Companion client adjusted audio or subtitle stream
"""
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
self.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
self.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
self.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
@LOCKER.lockthis
def _process_refresh(self, data):
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
xml = get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear()
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
data['playQueueID'])
2017-12-14 18:29:38 +11:00
def _process_tasks(self, task):
"""
2016-12-28 23:14:21 +11:00
Processes tasks picked up e.g. by Companion listener, e.g.
{'action': 'playlist',
'data': {'address': 'xyz.plex.direct',
'commandID': '7',
'containerKey': '/playQueues/6669?own=1&repeat=0&window=200',
'key': '/library/metadata/220493',
'machineIdentifier': 'xyz',
'offset': '0',
'port': '32400',
'protocol': 'https',
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
'type': 'video'}}
"""
2017-12-14 18:29:38 +11:00
LOG.debug('Processing: %s', task)
data = task['data']
2017-03-06 03:51:58 +11:00
if task['action'] == 'alexa':
2017-12-21 19:28:06 +11:00
self._process_alexa(data)
2017-03-06 03:51:58 +11:00
elif (task['action'] == 'playlist' and
2017-01-03 01:41:38 +11:00
data.get('address') == 'node.plexapp.com'):
2017-12-21 19:28:06 +11:00
self._process_node(data)
2017-01-03 01:41:38 +11:00
elif task['action'] == 'playlist':
2017-12-21 19:28:06 +11:00
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
2017-12-21 19:28:06 +11:00
self._process_refresh(data)
elif task['action'] == 'setStreams':
2017-12-21 19:28:06 +11:00
self._process_streams(data)
def run(self):
2017-12-14 18:29:38 +11:00
"""
2017-12-21 19:28:06 +11:00
Ensure that sockets will be closed no matter what
2017-12-14 18:29:38 +11:00
"""
try:
2017-12-14 18:29:38 +11:00
self._run()
finally:
try:
self.httpd.socket.shutdown(SHUT_RDWR)
except AttributeError:
pass
finally:
try:
self.httpd.socket.close()
except AttributeError:
pass
2017-12-14 18:29:38 +11:00
LOG.info("----===## Plex Companion stopped ##===----")
2017-12-14 18:29:38 +11:00
def _run(self):
httpd = self.httpd
# Cache for quicker while loops
client = self.client
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
# Start up instances
2017-12-14 18:29:38 +11:00
request_mgr = httppersist.RequestMgr()
subscription_manager = subscribers.SubscriptionMgr(
request_mgr, self.player, self.mgr)
self.subscription_manager = subscription_manager
2017-12-10 00:35:08 +11:00
queue = Queue(maxsize=100)
2017-03-05 03:54:24 +11:00
self.queue = queue
2016-09-03 01:20:19 +10:00
if settings('plexCompanion') == 'true':
# Start up httpd
start_count = 0
while True:
try:
httpd = listener.ThreadedHTTPServer(
client,
2017-12-14 18:29:38 +11:00
subscription_manager,
queue,
2017-12-10 02:30:52 +11:00
('', v.COMPANION_PORT),
listener.MyHandler)
httpd.timeout = 0.95
break
except:
2017-12-14 18:29:38 +11:00
LOG.error("Unable to start PlexCompanion. Traceback:")
2016-12-21 02:38:04 +11:00
import traceback
2017-12-14 18:29:38 +11:00
LOG.error(traceback.print_exc())
2016-12-21 02:38:04 +11:00
sleep(3000)
if start_count == 3:
2017-12-14 18:29:38 +11:00
LOG.error("Error: Unable to start web helper.")
httpd = False
break
start_count += 1
else:
2017-12-14 18:29:38 +11:00
LOG.info('User deactivated Plex Companion')
client.start_all()
message_count = 0
if httpd:
2017-12-14 18:29:38 +11:00
thread = Thread(target=httpd.handle_request)
while not thread_stopped():
2016-03-11 02:02:46 +11:00
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
while thread_suspended():
if thread_stopped():
break
2016-12-21 02:38:04 +11:00
sleep(1000)
try:
message_count += 1
if httpd:
2017-12-14 18:29:38 +11:00
if not thread.isAlive():
2016-08-11 03:03:37 +10:00
# Use threads cause the method will stall
2017-12-14 18:29:38 +11:00
thread = Thread(target=httpd.handle_request)
thread.start()
if message_count == 3000:
message_count = 0
if client.check_client_registration():
2017-12-14 18:29:38 +11:00
LOG.debug('Client is still registered')
else:
2017-12-14 18:29:38 +11:00
LOG.debug('Client is no longer registered. Plex '
'Companion still running on port %s',
v.COMPANION_PORT)
2017-02-20 03:07:42 +11:00
client.register_as_client()
# Get and set servers
if message_count % 30 == 0:
2017-12-14 18:29:38 +11:00
subscription_manager.serverlist = client.getServerList()
subscription_manager.notify()
if not httpd:
message_count = 0
except:
2017-12-14 18:29:38 +11:00
LOG.warn("Error in loop, continuing anyway. Traceback:")
2016-12-21 02:38:04 +11:00
import traceback
2017-12-14 18:29:38 +11:00
LOG.warn(traceback.format_exc())
# See if there's anything we need to process
try:
task = queue.get(block=False)
2017-12-10 00:35:08 +11:00
except Empty:
pass
else:
# Got instructions, process them
2017-12-14 18:29:38 +11:00
self._process_tasks(task)
queue.task_done()
# Don't sleep
continue
2017-03-06 02:43:06 +11:00
sleep(50)
2017-12-21 19:28:06 +11:00
self.subscription_manager.signal_stop()
client.stop_all()