mirror of
https://github.com/jellyfin/jellyfin-mpv-shim.git
synced 2024-11-23 05:59:43 +00:00
Proxy desktop player through local.
This commit is contained in:
parent
a33bbc3f6e
commit
cad494abe0
@ -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
|
||||
|
@ -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"
|
||||
),
|
||||
}
|
@ -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")
|
||||
|
@ -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 (
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user