This release fixes websocket forwarding and casting in the desktop client. Changes:
+This release changes how the web client works to improve reliability. Changes:
diff --git a/jellyfin_mpv_shim/player.py b/jellyfin_mpv_shim/player.py index c9dee4d316..6525edb311 100644 --- a/jellyfin_mpv_shim/player.py +++ b/jellyfin_mpv_shim/player.py @@ -126,8 +126,12 @@ def __init__(self): mpv_options = OrderedDict() mpv_location = settings.mpv_ext_path # Use bundled path for MPV if not specified by user, on Mac OS, and frozen - if mpv_location is None and platform.system() == "Darwin" and getattr(sys, 'frozen', False): - mpv_location = get_resource('mpv') + if ( + mpv_location is None + and platform.system() == "Darwin" + and getattr(sys, "frozen", False) + ): + mpv_location = get_resource("mpv") self.timeline_trigger = None self.action_trigger = None self.external_subtitles = {} diff --git a/jellyfin_mpv_shim/utils.py b/jellyfin_mpv_shim/utils.py index c3a27ee3a1..f2c8506738 100644 --- a/jellyfin_mpv_shim/utils.py +++ b/jellyfin_mpv_shim/utils.py @@ -236,7 +236,7 @@ def get_resource(*path): application_path = os.path.dirname(os.path.abspath(__file__)) # ! Test code for Mac - if getattr(sys, 'frozen', False) and platform.system() == 'Darwin': + if getattr(sys, "frozen", False) and platform.system() == "Darwin": application_path = os.path.join(os.path.dirname(sys.executable), "../Resources") return os.path.join(application_path, *path) diff --git a/jellyfin_mpv_shim/webclient_view/__init__.py b/jellyfin_mpv_shim/webclient_view/__init__.py index d5714d5d5f..2ce7e9039d 100644 --- a/jellyfin_mpv_shim/webclient_view/__init__.py +++ b/jellyfin_mpv_shim/webclient_view/__init__.py @@ -64,7 +64,7 @@ def run(self): app = Flask( __name__, static_url_path="", - static_folder=get_resource("webclient_view", "webclient") + static_folder=get_resource("webclient_view", "webclient"), ) pl_event_queue = Queue() @@ -78,7 +78,7 @@ def wrap_playstate(active, playstate=None, item=None): "CanSeek": False, "IsPaused": False, "IsMuted": False, - "RepeatMode": "RepeatNone" + "RepeatMode": "RepeatNone", } res = { "PlayState": playstate, @@ -89,7 +89,7 @@ def wrap_playstate(active, playstate=None, item=None): "SupportsMediaControl": True, "SupportsContentUploading": False, "SupportsPersistentIdentifier": False, - "SupportsSync": False + "SupportsSync": False, }, "RemoteEndPoint": "0.0.0.0", "PlayableMediaTypes": CAPABILITIES["PlayableMediaTypes"].split(","), @@ -108,7 +108,7 @@ def wrap_playstate(active, playstate=None, item=None): "HasCustomDeviceName": False, "ServerId": last_server_id, "SupportedCommands": CAPABILITIES["SupportedCommands"].split(","), - "dest": "player" + "dest": "player", } if "NowPlayingQueue" in playstate: res["NowPlayingQueue"] = playstate["NowPlayingQueue"] @@ -120,42 +120,70 @@ def wrap_playstate(active, playstate=None, item=None): def on_playstate(state, payload=None, item=None): pl_event_queue.put(wrap_playstate(True, payload, item)) - if (state == "stopped"): + if state == "stopped": pl_event_queue.put(wrap_playstate(False)) def it_on_event(name, event): + server_id = event["ServerId"] + if type(event) is dict and "value" in event and len(event) == 2: + event = event["value"] pl_event_queue.put({ "dest": "ws", "MessageType": name, - "Data": event + "Data": event, + "ServerId": server_id }) playerManager.on_playstate = on_playstate eventHandler.it_on_event = it_on_event - eventHandler.it_event_set = {"UserDataChanged"} + eventHandler.it_event_set = { + "ActivityLogEntry", + "LibraryChanged", + "PackageInstallationCancelled", + "PackageInstallationCompleted", + "PackageInstallationFailed", + "PackageInstalling", + "RefreshProgress", + "RestartRequired", + "ScheduledTasksInfo", + "SeriesTimerCancelled", + "SeriesTimerCancelled", + "SeriesTimerCreated", + "SeriesTimerCreated", + "ServerRestarting", + "ServerShuttingDown", + "Sessions", + "TimerCancelled", + "TimerCreated", + "UserDataChanged", + } @app.after_request def add_header(response): if request.path == "/index.html": do_not_cache(response) - client_data = base64.b64encode(json.dumps({ - "appName": USER_APP_NAME, - "appVersion": CLIENT_VERSION, - "deviceName": settings.player_name, - "deviceId": settings.client_uuid - }).encode('ascii')) + client_data = base64.b64encode( + json.dumps( + { + "appName": USER_APP_NAME, + "appVersion": CLIENT_VERSION, + "deviceName": settings.player_name, + "deviceId": settings.client_uuid, + } + ).encode("ascii") + ) # We need access to this data before we can make an async web call. - replacement = b"""""" % client_data + replacement = ( + b"""""" + % client_data + ) if settings.desktop_scale != 1.0: f_scale = float(settings.desktop_scale) - replacement = replacement + (b"""""" % f_scale) - response.make_sequence() - response.set_data( - response.get_data().replace( - b"", - replacement, + replacement = replacement + ( + b"""""" % f_scale ) - ) + response.make_sequence() + response.set_data(response.get_data().replace(b"", replacement,)) return response if not response.cache_control.no_store: @@ -168,7 +196,11 @@ def mpv_shim_session(): if request.headers["Content-Type"] != "application/json; charset=UTF-8": return "Go Away" req = request.json - log.info("Recieved session for server: {0}, user: {1}".format(req["Name"], req["username"])) + 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.") @@ -208,10 +240,24 @@ def mpv_shim_message(): 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_wsmessage", methods=["POST"]) + def mpv_shim_wsmessage(): + if request.headers["Content-Type"] != "application/json; charset=UTF-8": + return "Go Away" + req = request.json + client = clientManager.clients.get(req["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 + client.wsc.send(req["name"], req.get("payload", "")) + return resp + @app.route("/mpv_shim_teardown", methods=["POST"]) def mpv_shim_teardown(): if request.headers["Content-Type"] != "application/json; charset=UTF-8": diff --git a/setup-mac.py b/setup-mac.py index acc8366710..e782c6f2fc 100644 --- a/setup-mac.py +++ b/setup-mac.py @@ -1,22 +1,20 @@ from setuptools import setup -APP = ['/usr/local/bin/jellyfin-mpv-desktop'] +APP = ["/usr/local/bin/jellyfin-mpv-desktop"] OPTIONS = { - 'argv_emulation' : True, - 'iconfile' : 'jellyfin.icns', - 'resources' : [ - '/usr/local/bin/mpv', - '/usr/local/bin/jellyfin-mpv-shim', - '/usr/local/lib/python3.9/site-packages/jellyfin_mpv_shim/webclient_view' + "argv_emulation": True, + "iconfile": "jellyfin.icns", + "resources": [ + "/usr/local/bin/mpv", + "/usr/local/bin/jellyfin-mpv-shim", + "/usr/local/lib/python3.9/site-packages/jellyfin_mpv_shim/webclient_view", ], - 'packages': [ - 'pkg_resources' - ] + "packages": ["pkg_resources"], } setup( app=APP, - name='Jellyfin MPV Desktop', - options={'py2app': OPTIONS}, - setup_requires=['py2app'], + name="Jellyfin MPV Desktop", + options={"py2app": OPTIONS}, + setup_requires=["py2app"], ) diff --git a/setup.py b/setup.py index b6b6c27079..7f184e4c8b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="jellyfin-mpv-shim", - version="1.10.0", + version="1.10.1", author="Ian Walton", author_email="iwalton3@gmail.com", description="Cast media from Jellyfin Mobile and Web apps to MPV.", @@ -51,7 +51,7 @@ python_requires=">=3.6", install_requires=[ "python-mpv", - "jellyfin-apiclient-python>=1.7.0", + "jellyfin-apiclient-python>=1.7.2", "python-mpv-jsonipc>=1.1.9", "requests", "pydantic",