Improved handling of disconnection for mpv
All checks were successful
Check / mypy (push) Successful in 2m49s
Check / ruff (push) Successful in 6s

This commit is contained in:
Christoph Stahl 2025-05-18 22:58:56 +02:00
parent e2acc4f41e
commit 806d26767b
3 changed files with 68 additions and 20 deletions

View file

@ -14,6 +14,7 @@ be one of:
from __future__ import annotations
from collections.abc import Callable
from functools import partial
import logging
import os
import asyncio
@ -46,6 +47,29 @@ from .sources import configure_sources, Source
from .log import logger
class ConnectionState:
__is_connected__ = False
__mpv_running__ = False
def is_connected(self) -> bool:
return self.__is_connected__
def is_mpv_running(self) -> bool:
return self.__mpv_running__
def set_disconnected(self) -> None:
self.__is_connected__ = False
def set_connected(self) -> None:
self.__is_connected__ = True
def set_mpv_running(self) -> None:
self.__mpv_running__ = True
def set_mpv_terminated(self) -> None:
self.__mpv_running__ = False
def default_config() -> dict[str, Optional[int | str]]:
"""
Return a default configuration for the client.
@ -134,8 +158,7 @@ class Client:
def __init__(self, config: dict[str, Any]):
config["config"] = default_config() | config["config"]
self.is_running = False
self.is_quitting = False
self.connection_state = ConnectionState()
self.set_log_level(config["config"]["log_level"])
self.sio = socketio.AsyncClient(json=jsonencoder)
self.loop: Optional[asyncio.AbstractEventLoop] = None
@ -150,6 +173,8 @@ class Client:
QRPosition.from_string(config["config"]["qr_position"]),
self.quit_callback,
)
self.connection_state.set_mpv_running()
logger.info(f"MPV: {self.connection_state.is_mpv_running()} ")
self.register_handlers()
self.queue_callbacks: list[Callable[[list[Entry]], None]] = []
@ -183,7 +208,18 @@ class Client:
self.sio.on("disconnect", self.handle_disconnect)
async def handle_disconnect(self) -> None:
logger.info("Disconnected from server")
self.connection_state.set_disconnected()
await self.ensure_disconnect()
async def ensure_disconnect(self) -> None:
logger.info("Disconnecting from server")
logger.info(f"Connection: {self.connection_state.is_connected()}")
logger.info(f"MPV: {self.connection_state.is_mpv_running()}")
if self.connection_state.is_connected():
await self.sio.disconnect()
if self.connection_state.is_mpv_running():
if self.player.mpv is not None:
self.player.mpv.terminate()
async def handle_msg(self, data: dict[str, Any]) -> None:
"""
@ -262,7 +298,6 @@ class Client:
"""
self.state.queue.clear()
self.state.queue.extend([Entry(**entry) for entry in data["queue"]])
# self.state.queue = [Entry(**entry) for entry in data["queue"]]
self.state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]]
self.state.recent = [Entry(**entry) for entry in data["recent"]]
@ -521,25 +556,40 @@ class Client:
elif updated_config is not None:
await self.sio.emit("config", {"source": data["source"], "config": updated_config})
def signal_handler(self) -> None:
def signal_handler(self, loop: asyncio.AbstractEventLoop) -> None:
"""
Signal handler for the client.
This function is called when the client receives a signal to terminate. It
will disconnect from the server and kill the current player.
:param loop: The asyncio event loop
:type loop: asyncio.AbstractEventLoop
:rtype: None
"""
engineio.async_client.async_signal_handler()
if self.player.mpv is not None:
self.player.mpv.terminate()
asyncio.ensure_future(self.ensure_disconnect(), loop=loop)
def quit_callback(self) -> None:
if self.is_quitting:
return
self.is_quitting = True
"""
Callback function for the player, terminating the player and disconnecting
:rtype: None
"""
self.connection_state.set_mpv_terminated()
if self.loop is not None:
asyncio.run_coroutine_threadsafe(self.sio.disconnect(), self.loop)
asyncio.run_coroutine_threadsafe(self.ensure_disconnect(), self.loop)
asyncio.run_coroutine_threadsafe(self.kill_mpv(), self.loop)
async def kill_mpv(self) -> None:
"""
Kill the mpv process. Needs to be called in a thread, because of mpv...
See https://github.com/jaseg/python-mpv/issues/114#issuecomment-1214305952
:rtype: None
"""
if self.player.mpv is not None:
self.player.mpv.terminate()
async def start_client(self, config: dict[str, Any]) -> None:
"""
@ -576,18 +626,17 @@ class Client:
# this is not supported under windows
if os.name != "nt":
asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self.signal_handler)
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, partial(self.signal_handler, loop))
self.is_running = True
self.connection_state.set_connected()
await self.sio.wait()
except asyncio.CancelledError:
pass
except ConnectionError:
logger.critical("Could not connect to server")
finally:
self.is_running = False
if self.player.mpv is not None:
self.player.mpv.terminate()
await self.ensure_disconnect()
def create_async_and_start_client(

View file

@ -824,7 +824,7 @@ class SyngGui(QMainWindow):
self.timer.stop()
return
if not self.client.is_running:
if not self.client.connection_state.is_connected():
self.client = None
self.set_client_button_start()
else:
@ -837,7 +837,7 @@ class SyngGui(QMainWindow):
self.startbutton.setText("Connect")
def start_syng_client(self) -> None:
if self.client is None or not self.client.is_running:
if self.client is None or not self.client.connection_state.is_connected():
logger.debug("Starting client")
self.save_config()
config = self.gather_config()

View file

@ -61,7 +61,7 @@ class Player:
def start(self) -> None:
self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True)
self.mpv.title = "Syng - Player"
self.mpv.title = "Syng.Rocks! - Player"
self.mpv.keep_open = "yes"
self.mpv.play(
f"{self.base_dir}/background.png",
@ -199,4 +199,3 @@ class Player:
self.mpv.play(
f"{self.base_dir}/background.png",
)
# self.mpv.playlist_next()