PlexKodiConnect/resources/lib/plex_companion.py

363 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# -*- coding: utf-8 -*-
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
from queue import Empty
2016-12-21 02:38:04 +11:00
from socket import SHUT_RDWR
from xbmc import executebuiltin
2018-06-22 03:24:37 +10:00
from .plexbmchelper import listener, plexgdm, subscribers, httppersist
from .plex_api import API
from . import utils
from . import plex_functions as PF
from . import playlist_func as PL
from . import playback
from . import json_rpc as js
from . import playqueue as PQ
from . import variables as v
2018-11-19 00:59:17 +11:00
from . import backgroundthread
from . import app
from . import exceptions
2017-03-05 03:54:24 +11:00
2016-09-03 01:20:19 +10:00
###############################################################################
2018-06-22 03:24:37 +10:00
LOG = getLogger('PLEX.plex_companion')
2016-09-03 01:20:19 +10:00
###############################################################################
2018-06-22 03:24:37 +10:00
def update_playqueue_from_PMS(playqueue,
playqueue_id=None,
repeat=None,
offset=None,
transient_token=None,
start_plex_id=None):
2018-06-22 03:24:37 +10:00
"""
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue
repeat = 0, 1, 2
offset = time offset in Plextime (milliseconds)
"""
LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s, start_plex_id %s',
playqueue_id, offset, repeat, start_plex_id)
2018-06-22 03:24:37 +10:00
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
2018-11-19 00:59:17 +11:00
with app.APP.lock_playqueues:
2018-08-04 04:45:10 +10:00
try:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
except exceptions.PlaylistError:
2018-08-04 04:45:10 +10:00
LOG.error('Could now download playqueue %s', playqueue_id)
return
if playqueue.id == playqueue_id:
# This seems to be happening ONLY if a Plex Companion device
# reconnects and Kodi is already playing something - silly, really
# For all other cases, a new playqueue is generated by Plex
LOG.debug('Update for existing playqueue detected')
return
playqueue.clear()
# Get new metadata for the playqueue first
2018-06-22 03:24:37 +10:00
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except exceptions.PlaylistError:
2018-06-22 03:24:37 +10:00
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token
playback.play_xml(playqueue,
xml,
offset=offset,
start_plex_id=start_plex_id)
2018-06-22 03:24:37 +10:00
2018-11-19 00:59:17 +11:00
class PlexCompanion(backgroundthread.KillableThread):
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
"""
def __init__(self):
2017-12-14 18:29:38 +11:00
LOG.info("----===## Starting PlexCompanion ##===----")
# Init Plex Companion queue
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
2017-12-10 02:30:52 +11:00
self.client.clientDetails()
2018-01-01 23:28:39 +11:00
LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
2017-12-14 18:29:38 +11:00
self.httpd = False
2017-12-21 19:28:06 +11:00
self.subscription_manager = None
2018-11-19 00:59:17 +11:00
super(PlexCompanion, self).__init__()
2019-02-08 23:52:33 +11:00
@staticmethod
def _process_alexa(data):
if 'key' not in data or 'containerKey' not in data:
LOG.error('Received malformed Alexa data: %s', data)
return
2018-06-22 03:24:37 +10:00
xml = PF.GetPlexMetadata(data['key'])
2017-12-21 19:28:06 +11:00
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
2018-01-01 23:28:39 +11:00
LOG.error('Could not download Plex metadata for: %s', data)
2017-12-21 19:28:06 +11:00
return
api = API(xml[0])
2019-06-11 05:29:42 +10:00
if api.plex_type == v.PLEX_TYPE_ALBUM:
2017-12-21 19:28:06 +11:00
LOG.debug('Plex music album detected')
PQ.init_playqueue_from_plex_children(
2019-06-11 05:29:42 +10:00
api.plex_id,
transient_token=data.get('token'))
2018-02-08 21:16:39 +11:00
elif data['containerKey'].startswith('/playQueues/'):
2018-06-22 03:24:37 +10:00
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
2019-03-30 20:32:56 +11:00
xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
2018-02-08 21:16:39 +11:00
if xml is None:
# "Play error"
2018-06-22 03:24:37 +10:00
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
2018-02-08 21:16:39 +11:00
return
playqueue = PQ.get_playqueue_from_type(
2019-06-11 05:29:42 +10:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
2018-02-08 21:16:39 +11:00
playqueue.clear()
2018-06-22 03:24:37 +10:00
PL.get_playlist_details_from_xml(playqueue, xml)
2018-02-08 21:22:26 +11:00
playqueue.plex_transient_token = data.get('token')
2018-02-08 21:16:39 +11:00
if data.get('offset') != '0':
offset = float(data['offset']) / 1000.0
else:
offset = None
2018-06-22 03:24:37 +10:00
playback.play_xml(playqueue, xml, offset)
2017-12-21 19:28:06 +11:00
else:
2018-11-19 00:59:17 +11:00
app.CONN.plex_transient_token = data.get('token')
2019-06-11 05:29:42 +10:00
playback.playback_triage(api.plex_id,
api.plex_type,
2019-10-26 02:06:50 +11:00
resolve=False,
resume=data.get('offset') not in ('0', None))
2017-12-21 19:28:06 +11:00
@staticmethod
def _process_node(data):
"""
E.g. watch later initiated by Companion. Basically navigating Plex
"""
2018-11-19 00:59:17 +11:00
app.CONN.plex_transient_token = data.get('key')
2017-12-21 19:28:06 +11:00
params = {
'mode': 'plex_node',
2020-12-20 06:43:08 +11:00
'key': f"{{server}}{data.get('key')}",
2018-04-16 02:13:48 +10:00
'offset': data.get('offset')
2017-12-21 19:28:06 +11:00
}
2020-12-20 06:43:08 +11:00
handle = f'RunPlugin(plugin://{utils.extend_url(v.ADDON_ID, params)})'
executebuiltin(handle)
2017-12-21 19:28:06 +11:00
2019-02-08 23:52:33 +11:00
@staticmethod
def _process_playlist(data):
if 'containerKey' not in data:
LOG.error('Received malformed playlist data: %s', data)
return
2017-12-21 19:28:06 +11:00
# Get the playqueue ID
2018-06-22 03:24:37 +10:00
_, container_key, query = PF.ParseContainerKey(data['containerKey'])
2017-12-21 19:28:06 +11:00
try:
playqueue = PQ.get_playqueue_from_type(
2017-12-21 19:28:06 +11:00
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)
2018-06-22 03:24:37 +10:00
xml = PF.GetPlexMetadata(data['key'])
2017-12-21 19:28:06 +11:00
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
api = API(xml[0])
playqueue = PQ.get_playqueue_from_type(
2019-06-11 05:29:42 +10:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
key = data.get('key')
if key:
_, key, _ = PF.ParseContainerKey(key)
2018-06-22 03:24:37 +10:00
update_playqueue_from_PMS(playqueue,
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=utils.cast(int, data.get('offset')),
transient_token=data.get('token'),
start_plex_id=key)
2018-06-22 03:24:37 +10:00
2019-02-08 23:52:33 +11:00
@staticmethod
def _process_streams(data):
2017-12-21 19:28:06 +11:00
"""
Plex Companion client adjusted audio or subtitle stream
"""
if 'type' not in data:
LOG.error('Received malformed stream data: %s', data)
return
playqueue = PQ.get_playqueue_from_type(
2017-12-21 19:28:06 +11:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
playqueue.items[pos].on_plex_stream_change(data)
2017-12-21 19:28:06 +11:00
2019-02-08 23:52:33 +11:00
@staticmethod
def _process_refresh(data):
2017-12-21 19:28:06 +11:00
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
if 'playQueueID' not in data:
LOG.error('Received malformed refresh data: %s', data)
return
2018-06-22 03:24:37 +10:00
xml = PL.get_pms_playqueue(data['playQueueID'])
2017-12-21 19:28:06 +11:00
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
2018-06-22 03:24:37 +10:00
plex_type = PL.get_plextype_from_xml(xml)
2017-12-21 19:28:06 +11:00
if plex_type is None:
return
playqueue = PQ.get_playqueue_from_type(
2017-12-21 19:28:06 +11:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear()
return
playqueue = PQ.get_playqueue_from_type(
2017-12-21 19:28:06 +11:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
2018-06-22 03:24:37 +10:00
update_playqueue_from_PMS(playqueue, data['playQueueID'])
2017-12-21 19:28:06 +11:00
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':
2018-11-19 00:59:17 +11:00
with app.APP.lock_playqueues:
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':
2018-11-19 00:59:17 +11:00
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
2018-11-19 00:59:17 +11:00
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
try:
self._process_streams(data)
except KeyError:
pass
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
"""
app.APP.register_thread(self)
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
app.APP.deregister_thread(self)
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
# Start up instances
2017-12-14 18:29:38 +11:00
request_mgr = httppersist.RequestMgr()
subscription_manager = subscribers.SubscriptionMgr(request_mgr,
2018-11-23 18:41:05 +11:00
app.APP.player)
self.subscription_manager = subscription_manager
2018-06-22 03:24:37 +10:00
if utils.settings('plexCompanion') == 'true':
# Start up httpd
start_count = 0
while True:
try:
httpd = listener.PKCHTTPServer(
client,
2017-12-14 18:29:38 +11:00
subscription_manager,
2017-12-10 02:30:52 +11:00
('', v.COMPANION_PORT),
listener.MyHandler)
2019-12-15 17:36:50 +11:00
httpd.timeout = 10.0
break
2019-02-03 06:22:06 +11:00
except Exception:
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())
app.APP.monitor.waitForAbort(3)
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 self.should_cancel():
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
if self.should_suspend():
if self.wait_while_suspended():
break
try:
message_count += 1
if httpd:
if not thread.is_alive():
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
2019-02-03 06:22:06 +11:00
except Exception:
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:
2018-11-19 00:59:17 +11:00
task = app.APP.companion_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)
2018-11-19 00:59:17 +11:00
app.APP.companion_queue.task_done()
# Don't sleep
continue
self.sleep(0.05)
2018-01-01 23:28:39 +11:00
subscription_manager.signal_stop()
client.stop_all()