PlexKodiConnect/resources/lib/plex_companion/playstate.py
2021-12-08 17:17:32 +01:00

267 lines
10 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from logging import getLogger
import requests
from threading import Thread
from .common import communicate, log_error, UUIDStr, Subscriber, timeline, \
stopped_timeline, create_requests_session
from .playqueue import compare_playqueues
from .webserver import ThreadedHTTPServer, CompanionHandlerClassFactory
from .plexgdm import plexgdm
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)
# How many seconds do we wait until we check again whether we are registered
# as a GDM Plex Companion Client?
GDM_COMPANION_CHECK = 120
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
def __init__(self, companion_enabled):
self.companion_enabled = companion_enabled
self.subscribers = dict()
self.s = None
self.httpd = None
self.stopped_timeline = stopped_timeline()
self.gdm = plexgdm()
super().__init__()
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
def _get_requests_session(self):
if self.s is None:
self.s = create_requests_session()
return self.s
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
def close_connections(self):
"""May also be called from another thread"""
self._stop_webserver()
self._close_requests_session()
self.subscribers = dict()
def send_stop(self):
"""
If we're still connected to a PMS, tells the PMS that playback stopped
"""
self.pms_timeline(None, self.stopped_timeline)
self.companion_timeline(self.stopped_timeline)
def check_subscriber(self, cmd):
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:
try:
del self.subscribers[UUIDStr(uuid)]
except KeyError:
pass
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
def companion_timeline(self, message):
state = 'stopped'
for entry in message:
if entry.get('state') != 'stopped':
state = entry.get('state')
for subscriber in self.subscribers.values():
subscriber.send_timeline(message, state)
def pms_timeline_per_player(self, playerid, message):
"""
Sending the "normal", non-Companion playstate to the PMS works a bit
differently
"""
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:
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:
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)
def wait_while_suspended(self):
should_shutdown = super().wait_while_suspended()
if not should_shutdown:
self._start_webserver()
return should_shutdown
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
self.close_connections()
app.APP.deregister_thread(self)
log.info("----===## PlaystateMgr stopped ##===----")
def _run(self):
signaled_playback_stop = True
self._start_webserver()
self.gdm.start()
last_check = timing.unix_timestamp()
while not self.should_cancel():
if self.should_suspend():
self.close_connections()
if self.wait_while_suspended():
break
# 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)
# 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
# Then check for Kodi playback
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()
signaled_playback_stop = True
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)