PlexKodiConnect/resources/lib/plex_companion/webserver.py

201 lines
7.9 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Plex Companion listener
"""
from logging import getLogger
from re import sub
from socketserver import ThreadingMixIn
from http.server import HTTPServer, BaseHTTPRequestHandler
import xml.etree.ElementTree as etree
from . import common
from .processing import process_command
from .. import utils, variables as v
from .. import json_rpc as js
from .. import app
log = getLogger('PLEX.companion.webserver')
def CompanionHandlerClassFactory(playstate_mgr):
"""
This class factory makes playstate_mgr available for CompanionHandler
"""
class CompanionHandler(BaseHTTPRequestHandler):
"""
BaseHTTPRequestHandler implementation of Plex Companion listener
"""
protocol_version = 'HTTP/1.1'
def __init__(self, *args, **kwargs):
self.ok_msg = common.b_ok_message()
self.sending_headers = common.proxy_headers()
super().__init__(*args, **kwargs)
def log_message(self, *args, **kwargs):
"""Mute all requests, don't log them."""
pass
def handle_one_request(self):
try:
super().handle_one_request()
except ConnectionError as error:
# Catches e.g. ConnectionResetError: [WinError 10054]
log.debug('Silencing error: %s: %s', type(error), error)
self.close_connection = True
except Exception as error:
# Catch anything in order to not let our web server crash
log.error('Webserver ignored the following exception: %s, %s',
type(error), error)
self.close_connection = True
def do_HEAD(self):
log.debug("Serving HEAD request...")
self.answer_request()
def do_GET(self):
log.debug("Serving GET request...")
self.answer_request()
def do_OPTIONS(self):
log.debug("Serving OPTIONS request...")
self.send_response(200)
self.send_header('Content-Length', '0')
self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER)
self.send_header('Content-Type', 'text/plain')
self.send_header('Connection', 'close')
self.send_header('Access-Control-Max-Age', '1209600')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods',
'POST, GET, OPTIONS, DELETE, PUT, HEAD')
self.send_header(
'Access-Control-Allow-Headers',
'x-plex-version, x-plex-platform-version, x-plex-username, '
'x-plex-client-identifier, x-plex-target-client-identifier, '
'x-plex-device-name, x-plex-platform, x-plex-product, accept, '
'x-plex-device, x-plex-device-screen-resolution')
self.end_headers()
def response(self, body, code=200):
self.send_response(code)
for key, value in self.sending_headers.items():
self.send_header(key, value)
self.send_header('Content-Length', len(body) if body else 0)
self.end_headers()
if body:
self.wfile.write(body)
def ok_message(self):
self.response(self.ok_msg, code=200)
def nok_message(self, error_message, code):
log.warn('Sending Not OK message: %s', error_message)
self.response(f'Failure: {error_message}'.encode('utf8'),
code=code)
def poll(self, params):
"""
Case for Plex Web contacting us via Plex Companion
Let's NOT register this Companion client - it will poll us
continuously
"""
if params.get('wait') == '1':
# Plex Web asks us to wait until we start playback
i = 20
while not app.APP.is_playing and i > 0:
if app.APP.monitor.waitForAbort(1):
return
i -= 1
message = common.timeline(js.get_players())
self.response(etree.tostring(message, encoding='utf8'),
code=200)
def send_resources_xml(self):
xml = etree.Element('MediaContainer', attrib={'size': '1'})
etree.SubElement(xml, 'Player', attrib=common.player())
self.response(etree.tostring(xml, encoding='utf8'), code=200)
def check_subscription(self, params):
if self.uuid in playstate_mgr.subscribers:
return True
protocol = params.get('protocol')
port = params.get('port')
if protocol is None or port is None:
log.error('Received invalid params for subscription: %s',
params)
return False
url = f"{protocol}://{self.client_address[0]}:{port}"
playstate_mgr.subscribe(self.uuid, self.command_id, url)
return True
def answer_request(self):
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
parseresult = utils.urlparse(self.path)
paramarrays = utils.parse_qs(parseresult.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
log.debug('remote request_path: %s, received from %s. headers: %s',
request_path, self.client_address, self.headers.items())
log.debug('params received from remote: %s', params)
conntype = self.headers.get('Connection', '')
if conntype.lower() == 'keep-alive':
self.sending_headers['Connection'] = 'Keep-Alive'
self.sending_headers['Keep-Alive'] = 'timeout=20'
else:
self.sending_headers['Connection'] = 'Close'
self.command_id = int(params.get('commandID', 0))
uuid = self.headers.get('X-Plex-Client-Identifier')
if uuid is None:
log.error('No X-Plex-Client-Identifier received')
self.nok_message('No X-Plex-Client-Identifier received',
code=400)
return
self.uuid = common.UUIDStr(uuid)
# Here we DO NOT track subscribers
if request_path == 'player/timeline/poll':
# This seems to be only done by Plex Web, polling us
# continuously
self.poll(params)
return
elif request_path == 'resources':
self.send_resources_xml()
return
# Here we TRACK subscribers
if request_path == 'player/timeline/subscribe':
if self.check_subscription(params):
self.ok_message()
else:
self.nok_message(f'Received invalid parameters: {params}',
code=400)
return
elif request_path == 'player/timeline/unsubscribe':
playstate_mgr.unsubscribe(self.uuid)
self.ok_message()
else:
if not playstate_mgr.update_command_id(self.uuid,
self.command_id):
self.nok_message(f'Plex Companion Client not yet registered',
code=500)
return
if process_command(cmd=None, path=request_path, params=params):
self.ok_message()
else:
self.nok_message(f'Unknown request path: {request_path}',
code=500)
return CompanionHandler
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""
Using ThreadingMixIn Thread magic
"""
daemon_threads = True