diff --git a/syng/client.py b/syng/client.py index cff1084..f53dd47 100644 --- a/syng/client.py +++ b/syng/client.py @@ -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( diff --git a/syng/gui.py b/syng/gui.py index c1c78ae..d5e7adb 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -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() diff --git a/syng/player_libmpv.py b/syng/player_libmpv.py index ce29c2a..0c94cc4 100644 --- a/syng/player_libmpv.py +++ b/syng/player_libmpv.py @@ -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()