Proxy desktop player through local.

This commit is contained in:
Ian Walton 2021-03-23 00:11:37 -04:00
parent a33bbc3f6e
commit cad494abe0
5 changed files with 168 additions and 54 deletions

View File

@ -3,7 +3,7 @@ from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE
from .conf import settings
from . import conffile
from getpass import getpass
from .constants import CLIENT_VERSION, USER_APP_NAME, USER_AGENT, APP_NAME
from .constants import CAPABILITIES, CLIENT_VERSION, USER_APP_NAME, USER_AGENT, APP_NAME
from .i18n import _
import sys
@ -73,7 +73,7 @@ class ClientManager(object):
def _connect_all(self):
is_logged_in = False
for server in self.credentials:
if self._connect_client(server):
if self.connect_client(server):
is_logged_in = True
return is_logged_in
@ -142,7 +142,7 @@ class ClientManager(object):
server["username"] = username
if force_unique and server["Id"] in self.clients:
return True
self._connect_client(server)
self.connect_client(server)
self.credentials.append(server)
self.save_credentials()
return True
@ -162,7 +162,7 @@ class ClientManager(object):
)
self._disconnect_client(server=server)
time.sleep(timeout)
if self._connect_client(server):
if self.connect_client(server):
break
else:
self.callback(client, event_name, data)
@ -171,21 +171,7 @@ class ClientManager(object):
client.callback_ws = event
client.start(websocket=True)
client.jellyfin.post_capabilities(
{
"PlayableMediaTypes": "Video",
"SupportsMediaControl": True,
"SupportedCommands": (
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleFullscreen,"
"GoHome,GoToSettings,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
"Mute,Unmute,SetVolume,DisplayContent,"
"Play,Playstate,PlayNext,PlayMediaSource"
),
}
)
client.jellyfin.post_capabilities(CAPABILITIES)
def remove_client(self, uuid: str):
self.credentials = [
@ -194,7 +180,7 @@ class ClientManager(object):
self.save_credentials()
self._disconnect_client(uuid=uuid)
def _connect_client(self, server):
def connect_client(self, server):
if self.is_stopping:
return False
@ -224,11 +210,14 @@ class ClientManager(object):
client.stop()
def remove_all_clients(self):
self.stop_all_clients()
self.credentials = []
self.save_credentials()
def stop_all_clients(self):
for key, client in list(self.clients.items()):
del self.clients[key]
client.stop()
self.credentials = []
self.save_credentials()
def stop(self):
self.is_stopping = True

View File

@ -2,3 +2,16 @@ APP_NAME = "jellyfin-mpv-shim"
USER_APP_NAME = "Jellyfin MPV Shim"
CLIENT_VERSION = "1.9.0"
USER_AGENT = "Jellyfin-MPV-Shim/%s" % CLIENT_VERSION
CAPABILITIES = {
"PlayableMediaTypes": "Video",
"SupportsMediaControl": True,
"SupportedCommands": (
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleFullscreen,"
"GoHome,GoToSettings,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
"Mute,Unmute,SetVolume,DisplayContent,"
"Play,Playstate,PlayNext,PlayMediaSource"
),
}

View File

@ -37,6 +37,10 @@ def bind(event_name: str):
class EventHandler(object):
mirror = None
def __init__(self):
self.it_on_event = None
self.it_event_set = set()
def handle_event(
self, client: "JellyfinClient_type", event_name: str, arguments: dict
):
@ -46,6 +50,9 @@ class EventHandler(object):
else:
log.debug("Unhandled Event {0}: {1}".format(event_name, arguments))
if self.it_on_event and event_name in self.it_event_set:
self.it_on_event(event_name, arguments)
@bind("Play")
def play_media(self, client: "JellyfinClient_type", _event_name, arguments: dict):
play_command = arguments.get("PlayCommand")

View File

@ -143,6 +143,7 @@ class PlayerManager(object):
self.warned_about_transcode = False
self.fullscreen_disable = False
self.update_check = UpdateChecker(self)
self.on_playstate = None
if is_using_ext_mpv:
mpv_options.update(
@ -871,7 +872,10 @@ class PlayerManager(object):
and self._video
and not self._player.playback_abort
):
self._video.client.jellyfin.session_progress(self.get_timeline_options())
options = self.get_timeline_options()
if self.on_playstate:
self.on_playstate("initial", options, self._video.item)
self._video.client.jellyfin.session_progress(options)
try:
if self.syncplay.is_enabled():
self.syncplay.sync_playback_time()
@ -880,7 +884,10 @@ class PlayerManager(object):
@synchronous("_tl_lock")
def send_timeline_initial(self):
self._video.client.jellyfin.session_playing(self.get_timeline_options())
options = self.get_timeline_options()
if self.on_playstate:
self.on_playstate("initial", options, self._video.item)
self._video.client.jellyfin.session_playing(options)
@synchronous("_tl_lock")
def send_timeline_stopped(self, finished=False, options=None, client=None):
@ -892,6 +899,8 @@ class PlayerManager(object):
if client is None:
client = self._video.client
if self.on_playstate:
self.on_playstate("stopped", options)
client.jellyfin.session_stop(options)
if self.get_webview() is not None and (

View File

@ -1,4 +1,6 @@
from queue import Empty, Queue
import threading
import time
import urllib.request
from werkzeug.serving import make_server
from flask import Flask, request, jsonify
@ -20,16 +22,20 @@ import json
import webview # Python3-webview in Debian, pywebview in pypi
import sys
from threading import Event
import datetime
from ..clients import clientManager
from ..player import playerManager
from ..conf import settings
from ..constants import USER_APP_NAME, APP_NAME
from ..event_handler import eventHandler
from ..constants import CAPABILITIES, CLIENT_VERSION, USER_APP_NAME, APP_NAME
from ..utils import get_resource
from .. import conffile
import logging
log = logging.getLogger("webclient")
remember_layout = conffile.get(APP_NAME, "layout.json")
loaded = Event()
def do_not_cache(response):
@ -60,6 +66,72 @@ class Server(threading.Thread):
static_folder=get_resource("webclient_view", "webclient"),
)
pl_event_queue = Queue()
last_server_id = ""
last_user_id = ""
last_user_name = ""
def wrap_playstate(active, playstate=None, item=None):
if playstate is None:
playstate = {
"CanSeek": False,
"IsPaused": False,
"IsMuted": False,
"RepeatMode": "RepeatNone"
}
res = {
"PlayState": playstate,
"AdditionalUsers": [],
"Capabilities": {
"PlayableMediaTypes": CAPABILITIES["PlayableMediaTypes"].split(","),
"SupportedCommands": CAPABILITIES["SupportedCommands"].split(","),
"SupportsMediaControl": True,
"SupportsContentUploading": False,
"SupportsPersistentIdentifier": False,
"SupportsSync": False
},
"RemoteEndPoint": "0.0.0.0",
"PlayableMediaTypes": CAPABILITIES["PlayableMediaTypes"].split(","),
"Id": settings.client_uuid,
"UserId": last_user_id,
"UserName": last_user_name,
"Client": USER_APP_NAME,
"LastActivityDate": datetime.datetime.utcnow().isoformat(),
"LastPlaybackCheckIn": "0001-01-01T00:00:00.0000000Z",
"DeviceName": settings.player_name,
"DeviceId": settings.client_uuid,
"ApplicationVersion": CLIENT_VERSION,
"IsActive": active,
"SupportsMediaControl": True,
"SupportsRemoteControl": True,
"HasCustomDeviceName": False,
"ServerId": last_server_id,
"SupportedCommands": CAPABILITIES["SupportedCommands"].split(","),
"dest": "player"
}
if "NowPlayingQueue" in playstate:
res["NowPlayingQueue"] = playstate["NowPlayingQueue"]
if "PlaylistItemId" in playstate:
res["PlaylistItemId"] = playstate["PlaylistItemId"]
if item:
res["NowPlayingItem"] = item
return res
def on_playstate(state, payload=None, item=None):
pl_event_queue.put(wrap_playstate(True, payload, item))
if (state == "stopped"):
pl_event_queue.put(wrap_playstate(False))
def it_on_event(name, event):
event = (event or {}).copy()
event["Name"] = name
event["dest"] = "ws"
pl_event_queue.put(event)
playerManager.on_playstate = on_playstate
self.it_on_event = it_on_event
self.it_event_set = {"UserDataChanged"}
@app.after_request
def add_header(response):
if request.path == "/index.html":
@ -78,29 +150,63 @@ class Server(threading.Thread):
response.cache_control.max_age = 2592000
return response
@app.route("/mpv_shim_password", methods=["POST"])
def mpv_shim_password():
@app.route("/mpv_shim_session", methods=["POST"])
def mpv_shim_session():
nonlocal last_server_id, last_user_id, last_user_name
if request.headers["Content-Type"] != "application/json; charset=UTF-8":
return "Go Away"
login_req = request.json
success = clientManager.login(
login_req["server"], login_req["username"], login_req["password"], True
)
if success:
loaded.set()
resp = jsonify({"success": success})
req = request.json
log.info("Recieved session for server: {0}, user: {1}".format(req["Name"], req["username"]))
if req["Id"] not in clientManager.clients:
is_logged_in = clientManager.connect_client(req)
log.info("Connection was successful.")
else:
is_logged_in = True
log.info("Ignoring as client already exists.")
last_server_id = req["Id"]
last_user_id = req["UserId"]
last_user_name = req["username"]
resp = jsonify({"success": is_logged_in})
resp.status_code = 200 if is_logged_in else 500
do_not_cache(resp)
return resp
@app.route("/mpv_shim_event", methods=["POST"])
def mpv_shim_event():
if request.headers["Content-Type"] != "application/json; charset=UTF-8":
return "Go Away"
try:
queue_item = pl_event_queue.get(timeout=5)
except Empty:
queue_item = {}
resp = jsonify(queue_item)
resp.status_code = 200
do_not_cache(resp)
return resp
@app.route("/mpv_shim_id", methods=["POST"])
def mpv_shim_id():
@app.route("/mpv_shim_message", methods=["POST"])
def mpv_shim_message():
if request.headers["Content-Type"] != "application/json; charset=UTF-8":
return "Go Away"
loaded.wait()
resp = jsonify(
{"appName": USER_APP_NAME, "deviceName": settings.player_name}
)
req = request.json
client = clientManager.clients.get(req["payload"]["ServerId"])
resp = jsonify({})
resp.status_code = 200
do_not_cache(resp)
if client is None:
log.warning("Message recieved but no client available. Ignoring.")
return resp
# Assume only 1 client is connected.
eventHandler.handle_event(client, req["name"], req["payload"])
return resp
@app.route("/mpv_shim_teardown", methods=["POST"])
def mpv_shim_teardown():
if request.headers["Content-Type"] != "application/json; charset=UTF-8":
return "Go Away"
log.info("Client teardown requested.")
clientManager.stop_all_clients()
resp = jsonify({})
resp.status_code = 200
do_not_cache(resp)
return resp
@ -118,16 +224,6 @@ class Server(threading.Thread):
do_not_cache(resp)
return resp
@app.route("/destroy_session", methods=["POST"])
def mpv_shim_destroy_session():
if request.headers["Content-Type"] != "application/json; charset=UTF-8":
return "Go Away"
clientManager.remove_all_clients()
resp = jsonify({"success": True})
resp.status_code = 200
do_not_cache(resp)
return resp
self.srv = make_server("127.0.0.1", 18096, app, threaded=True)
self.ctx = app.app_context()
self.ctx.push()
@ -149,9 +245,7 @@ class WebviewClient(object):
@staticmethod
def login_servers():
success = clientManager.try_connect()
if success:
loaded.set()
pass
def get_webview(self):
return self.webview
@ -270,6 +364,8 @@ class WebviewClient(object):
json.dump(extra_options, fh)
window.closing += handle_close
# REMOVE ME!!!!!!!!!!!!
sleep(3600)
if self.cef:
webview.start(gui="cef")
else: