2021-10-22 17:40:25 +11:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from logging import getLogger
|
|
|
|
import requests
|
2021-11-22 00:40:56 +11:00
|
|
|
from threading import Thread
|
2021-10-22 17:40:25 +11:00
|
|
|
|
2021-12-09 03:17:32 +11:00
|
|
|
from .common import communicate, log_error, UUIDStr, Subscriber, timeline, \
|
|
|
|
stopped_timeline, create_requests_session
|
2021-10-31 20:44:27 +11:00
|
|
|
from .playqueue import compare_playqueues
|
2021-11-22 00:40:56 +11:00
|
|
|
from .webserver import ThreadedHTTPServer, CompanionHandlerClassFactory
|
|
|
|
from .plexgdm import plexgdm
|
2021-10-22 17:40:25 +11:00
|
|
|
|
|
|
|
from .. import json_rpc as js
|
|
|
|
from .. import variables as v
|
|
|
|
from .. import backgroundthread
|
|
|
|
from .. import app
|
|
|
|
from .. import timing
|
|
|
|
|
|
|
|
|
|
|
|
# Disable annoying requests warnings
|
|
|
|
import requests.packages.urllib3
|
|
|
|
requests.packages.urllib3.disable_warnings()
|
|
|
|
|
|
|
|
log = getLogger('PLEX.companion.playstate')
|
|
|
|
|
|
|
|
TIMEOUT = (5, 5)
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
# How many seconds do we wait until we check again whether we are registered
|
|
|
|
# as a GDM Plex Companion Client?
|
|
|
|
GDM_COMPANION_CHECK = 120
|
2021-10-22 17:40:25 +11:00
|
|
|
|
|
|
|
|
|
|
|
def update_player_info(players):
|
|
|
|
"""
|
|
|
|
Update the playstate info for other PKC "consumers"
|
|
|
|
"""
|
|
|
|
for player in players.values():
|
|
|
|
playerid = player['playerid']
|
|
|
|
app.PLAYSTATE.player_states[playerid].update(js.get_player_props(playerid))
|
|
|
|
app.PLAYSTATE.player_states[playerid]['volume'] = js.get_volume()
|
|
|
|
app.PLAYSTATE.player_states[playerid]['muted'] = js.get_muted()
|
|
|
|
|
|
|
|
|
|
|
|
class PlaystateMgr(backgroundthread.KillableThread):
|
|
|
|
"""
|
|
|
|
If Kodi plays something, tell the PMS about it and - if a Companion client
|
|
|
|
is connected - tell the PMS Plex Companion piece of the PMS about it.
|
|
|
|
Also checks whether an intro is currently playing, enabling the user to
|
|
|
|
skip it.
|
|
|
|
"""
|
|
|
|
daemon = True
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def __init__(self, companion_enabled):
|
|
|
|
self.companion_enabled = companion_enabled
|
|
|
|
self.subscribers = dict()
|
2021-10-22 17:40:25 +11:00
|
|
|
self.s = None
|
2021-11-22 00:40:56 +11:00
|
|
|
self.httpd = None
|
2021-10-22 17:40:25 +11:00
|
|
|
self.stopped_timeline = stopped_timeline()
|
2021-11-22 00:40:56 +11:00
|
|
|
self.gdm = plexgdm()
|
2021-10-22 17:40:25 +11:00
|
|
|
super().__init__()
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def _start_webserver(self):
|
|
|
|
if self.httpd is None and self.companion_enabled:
|
|
|
|
log.debug('Starting PKC Companion webserver on port %s', v.COMPANION_PORT)
|
|
|
|
server_address = ('', v.COMPANION_PORT)
|
|
|
|
HandlerClass = CompanionHandlerClassFactory(self)
|
|
|
|
self.httpd = ThreadedHTTPServer(server_address, HandlerClass)
|
|
|
|
self.httpd.timeout = 10.0
|
|
|
|
t = Thread(target=self.httpd.serve_forever)
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
def _stop_webserver(self):
|
|
|
|
if self.httpd is not None:
|
|
|
|
log.debug('Shutting down PKC Companion webserver')
|
|
|
|
try:
|
|
|
|
self.httpd.shutdown()
|
|
|
|
except AttributeError:
|
|
|
|
# Ensure thread-safety
|
|
|
|
pass
|
|
|
|
self.httpd = None
|
|
|
|
|
2021-10-22 17:40:25 +11:00
|
|
|
def _get_requests_session(self):
|
|
|
|
if self.s is None:
|
2021-12-09 03:17:32 +11:00
|
|
|
self.s = create_requests_session()
|
2021-10-22 17:40:25 +11:00
|
|
|
return self.s
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def _close_requests_session(self):
|
|
|
|
if self.s is not None:
|
|
|
|
try:
|
|
|
|
self.s.close()
|
|
|
|
except AttributeError:
|
|
|
|
# "thread-safety" - Just in case s was set to None in the
|
|
|
|
# meantime
|
|
|
|
pass
|
|
|
|
self.s = None
|
2021-10-22 17:40:25 +11:00
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def close_connections(self):
|
|
|
|
"""May also be called from another thread"""
|
|
|
|
self._stop_webserver()
|
|
|
|
self._close_requests_session()
|
|
|
|
self.subscribers = dict()
|
2021-10-22 17:40:25 +11:00
|
|
|
|
|
|
|
def send_stop(self):
|
|
|
|
"""
|
|
|
|
If we're still connected to a PMS, tells the PMS that playback stopped
|
|
|
|
"""
|
2021-11-22 00:40:56 +11:00
|
|
|
self.pms_timeline(None, self.stopped_timeline)
|
|
|
|
self.companion_timeline(self.stopped_timeline)
|
2021-10-22 17:40:25 +11:00
|
|
|
|
|
|
|
def check_subscriber(self, cmd):
|
2021-11-22 00:40:56 +11:00
|
|
|
if not cmd.get('clientIdentifier'):
|
|
|
|
return
|
|
|
|
uuid = UUIDStr(cmd.get('clientIdentifier'))
|
|
|
|
with app.APP.lock_subscriber:
|
|
|
|
if cmd.get('path') == '/player/timeline/unsubscribe':
|
|
|
|
if uuid in self.subscribers:
|
|
|
|
log.debug('Stop Plex Companion subscription for %s', uuid)
|
|
|
|
del self.subscribers[uuid]
|
|
|
|
elif uuid not in self.subscribers:
|
|
|
|
log.debug('Start new Plex Companion subscription for %s', uuid)
|
|
|
|
self.subscribers[uuid] = Subscriber(self, cmd=cmd)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self.subscribers[uuid].command_id = int(cmd.get('commandID'))
|
|
|
|
except TypeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def subscribe(self, uuid, command_id, url):
|
|
|
|
log.debug('New Plex Companion subscriber %s: %s', uuid, url)
|
|
|
|
with app.APP.lock_subscriber:
|
|
|
|
self.subscribers[UUIDStr(uuid)] = Subscriber(self,
|
|
|
|
cmd=None,
|
|
|
|
uuid=uuid,
|
|
|
|
command_id=command_id,
|
|
|
|
url=url)
|
|
|
|
|
|
|
|
def unsubscribe(self, uuid):
|
|
|
|
log.debug('Unsubscribing Plex Companion client %s', uuid)
|
|
|
|
with app.APP.lock_subscriber:
|
2021-10-22 17:40:25 +11:00
|
|
|
try:
|
2021-11-22 00:40:56 +11:00
|
|
|
del self.subscribers[UUIDStr(uuid)]
|
|
|
|
except KeyError:
|
2021-10-22 17:40:25 +11:00
|
|
|
pass
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def update_command_id(self, uuid, command_id):
|
|
|
|
with app.APP.lock_subscriber:
|
|
|
|
if uuid not in self.subscribers:
|
|
|
|
return False
|
|
|
|
self.subscribers[uuid].command_id = command_id
|
|
|
|
return True
|
|
|
|
|
2021-10-22 17:40:25 +11:00
|
|
|
def companion_timeline(self, message):
|
|
|
|
state = 'stopped'
|
2021-11-22 00:40:56 +11:00
|
|
|
for entry in message:
|
|
|
|
if entry.get('state') != 'stopped':
|
|
|
|
state = entry.get('state')
|
|
|
|
for subscriber in self.subscribers.values():
|
|
|
|
subscriber.send_timeline(message, state)
|
2021-10-22 17:40:25 +11:00
|
|
|
|
|
|
|
def pms_timeline_per_player(self, playerid, message):
|
|
|
|
"""
|
2021-11-22 00:40:56 +11:00
|
|
|
Sending the "normal", non-Companion playstate to the PMS works a bit
|
|
|
|
differently
|
2021-10-22 17:40:25 +11:00
|
|
|
"""
|
|
|
|
url = f'{app.CONN.server}/:/timeline'
|
|
|
|
self._get_requests_session()
|
|
|
|
self.s.params.update(message[playerid].attrib)
|
|
|
|
# Tell the PMS about our playstate progress
|
|
|
|
try:
|
2021-11-22 00:40:56 +11:00
|
|
|
req = communicate(self.s.get, url, timeout=TIMEOUT)
|
|
|
|
except requests.RequestException as error:
|
|
|
|
log.error('Could not send the PMS timeline: %s', error)
|
|
|
|
return
|
|
|
|
except SystemExit:
|
2021-10-22 17:40:25 +11:00
|
|
|
return
|
|
|
|
if not req.ok:
|
|
|
|
log_error(log.error, 'Failed reporting playback progress', req)
|
|
|
|
|
|
|
|
def pms_timeline(self, players, message):
|
|
|
|
players = players if players else \
|
|
|
|
{0: {'playerid': 0}, 1: {'playerid': 1}, 2: {'playerid': 2}}
|
|
|
|
for player in players.values():
|
|
|
|
self.pms_timeline_per_player(player['playerid'], message)
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def wait_while_suspended(self):
|
|
|
|
should_shutdown = super().wait_while_suspended()
|
|
|
|
if not should_shutdown:
|
|
|
|
self._start_webserver()
|
|
|
|
return should_shutdown
|
|
|
|
|
2021-10-22 17:40:25 +11:00
|
|
|
def run(self):
|
|
|
|
app.APP.register_thread(self)
|
|
|
|
log.info("----===## Starting PlaystateMgr ##===----")
|
|
|
|
try:
|
|
|
|
self._run()
|
|
|
|
finally:
|
|
|
|
# Make sure we're telling the PMS that playback will stop
|
|
|
|
self.send_stop()
|
|
|
|
# Cleanup
|
2021-11-22 00:40:56 +11:00
|
|
|
self.close_connections()
|
2021-10-22 17:40:25 +11:00
|
|
|
app.APP.deregister_thread(self)
|
|
|
|
log.info("----===## PlaystateMgr stopped ##===----")
|
|
|
|
|
|
|
|
def _run(self):
|
|
|
|
signaled_playback_stop = True
|
2021-11-22 00:40:56 +11:00
|
|
|
self._start_webserver()
|
|
|
|
self.gdm.start()
|
|
|
|
last_check = timing.unix_timestamp()
|
2021-10-22 17:40:25 +11:00
|
|
|
while not self.should_cancel():
|
|
|
|
if self.should_suspend():
|
2021-11-22 00:40:56 +11:00
|
|
|
self.close_connections()
|
2021-10-22 17:40:25 +11:00
|
|
|
if self.wait_while_suspended():
|
|
|
|
break
|
2021-10-31 20:44:27 +11:00
|
|
|
# Check for Kodi playlist changes first
|
|
|
|
with app.APP.lock_playqueues:
|
|
|
|
for playqueue in app.PLAYQUEUES:
|
|
|
|
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
|
|
|
if playqueue.old_kodi_pl != kodi_pl:
|
|
|
|
if playqueue.id is None and (not app.SYNC.direct_paths or
|
|
|
|
app.PLAYSTATE.context_menu_play):
|
|
|
|
# Only initialize if directly fired up using direct
|
|
|
|
# paths. Otherwise let default.py do its magic
|
|
|
|
log.debug('Not yet initiating playback')
|
|
|
|
else:
|
|
|
|
# compare old and new playqueue
|
|
|
|
compare_playqueues(playqueue, kodi_pl)
|
|
|
|
playqueue.old_kodi_pl = list(kodi_pl)
|
2021-11-22 00:40:56 +11:00
|
|
|
# Make sure we are registered as a player
|
|
|
|
now = timing.unix_timestamp()
|
|
|
|
if now - last_check > GDM_COMPANION_CHECK:
|
|
|
|
self.gdm.check_client_registration()
|
|
|
|
last_check = now
|
2021-10-31 20:44:27 +11:00
|
|
|
# Then check for Kodi playback
|
2021-10-22 17:40:25 +11:00
|
|
|
players = js.get_players()
|
|
|
|
if not players and signaled_playback_stop:
|
|
|
|
self.sleep(1)
|
|
|
|
continue
|
|
|
|
elif not players:
|
|
|
|
# Playback has just stopped, need to tell Plex
|
|
|
|
self.send_stop()
|
2021-11-22 00:40:56 +11:00
|
|
|
signaled_playback_stop = True
|
2021-10-22 17:40:25 +11:00
|
|
|
self.sleep(1)
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Update the playstate info, such as playback progress
|
|
|
|
update_player_info(players)
|
|
|
|
try:
|
|
|
|
message = timeline(players)
|
|
|
|
except TypeError:
|
|
|
|
# We haven't had a chance to set the kodi_stream_index for
|
|
|
|
# the currently playing item. Just skip for now
|
|
|
|
self.sleep(1)
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Kodi will started with 'stopped' - make sure we're
|
|
|
|
# waiting here until we got something playing or on pause.
|
|
|
|
for entry in message:
|
|
|
|
if entry.get('state') != 'stopped':
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
signaled_playback_stop = False
|
|
|
|
# Send the playback progress info to the PMS
|
|
|
|
self.pms_timeline(players, message)
|
|
|
|
# Send the info to all Companion devices via the PMS
|
|
|
|
self.companion_timeline(message)
|
|
|
|
self.sleep(1)
|