diff --git a/syng/client.py b/syng/client.py index 4281665..d7b0b48 100644 --- a/syng/client.py +++ b/syng/client.py @@ -123,6 +123,7 @@ class Client: self.sources = configure_sources(config["sources"]) self.state = State() self.currentLock = asyncio.Semaphore(0) + print("blubb") self.player = Player() self.register_handlers() @@ -270,17 +271,20 @@ class Client: :rtype: None """ entry: Entry = Entry(**data) + source = self.sources[entry.source] print( f"Playing: {entry.artist} - {entry.title} [{entry.album}] " f"({entry.source}) for {entry.performer}" ) try: - self.state.current_source = self.sources[entry.source] + # self.state.current_source = self.sources[entry.source] if self.state.config["preview_duration"] > 0: await self.preview(entry) - await self.sources[entry.source].play( - entry, self.player, self.state.config["mpv_options"] - ) + video, audio = await source.ensure_playable(entry) + await self.player.play(video, audio, source.extra_mpv_options) + # await self.sources[entry.source].play( + # entry, self.player, self.state.config["mpv_options"] + # ) except Exception: # pylint: disable=broad-except print_exc() self.state.current_source = None @@ -421,9 +425,8 @@ class Client: :rtype: None """ engineio.async_client.async_signal_handler() - if self.state.current_source is not None: - if self.state.current_source.player is not None: # TODO old player - self.state.current_source.player.kill() + if self.player is not None: + self.player.mpv.terminate() async def start_client(self, config: dict[str, Any]) -> None: """ @@ -469,9 +472,8 @@ class Client: except asyncio.CancelledError: pass finally: - if self.state.current_source is not None: - if self.state.current_source.player is not None: # TODO old player - self.state.current_source.player.kill() + if self.player is not None: + self.player.mpv.terminate() def create_async_and_start_client( diff --git a/syng/player_libmpv.py b/syng/player_libmpv.py index 6b3d033..d29063c 100644 --- a/syng/player_libmpv.py +++ b/syng/player_libmpv.py @@ -1,11 +1,10 @@ import asyncio +import locale import sys -import tempfile -from typing import Iterable, Optional +from typing import Iterable, Optional, cast from qrcode.main import QRCode import mpv import os -from PIL.Image import Image from .entry import Entry @@ -14,33 +13,11 @@ __dirname__ = os.path.dirname(__file__) class Player: - def osd_size_handler(self, *args): - if self.qr_overlay: - self.mpv.remove_overlay(self.qr_overlay.overlay_id) - - osd_width: int = self.mpv.osd_width - osd_height: int = self.mpv.osd_height - - x_pos = osd_width - self.qr.width - 10 - y_pos = osd_height - self.qr.height - 10 - - print(osd_width, osd_height) - print(x_pos, y_pos) - - self.qr_overlay = self.mpv.create_image_overlay(self.qr, pos=(x_pos, y_pos)) - - def event_handler(self, event): - devent = event.as_dict() - if devent["event"] == b"file-loaded": - print(self.audio) - if self.audio: - self.mpv.audio_add(self.audio) - - def __init__(self): + def __init__(self) -> None: + locale.setlocale(locale.LC_ALL, "C") self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True) self.mpv.keep_open = "yes" - self.audio = None - self.qr_overlay = None + self.qr_overlay: Optional[mpv.ImageOverlay] = None qr = QRCode(box_size=5, border=1) qr.add_data("https://syng.rocks/") qr.make() @@ -50,15 +27,26 @@ class Player: f"{__dirname__}/static/background.png", ) - self.mpv.register_event_callback(self.event_handler) + self.default_options = { + "scale": "bilinear", + } + self.mpv.observe_property("osd-width", self.osd_size_handler) self.mpv.observe_property("osd-height", self.osd_size_handler) - # def play_entry(self, entry: Entry, video: str, audio: Optional[str] = None): - # self.queue_next(entry) - # self.play(video, audio) + def osd_size_handler(self, attribute: str, value: int) -> None: + if self.qr_overlay: + self.mpv.remove_overlay(self.qr_overlay.overlay_id) - async def queue_next(self, entry: Entry): + osd_width: int = cast(int, self.mpv.osd_width) + osd_height: int = cast(int, self.mpv.osd_height) + + x_pos = osd_width - self.qr.width - 10 + y_pos = osd_height - self.qr.height - 10 + + self.qr_overlay = self.mpv.create_image_overlay(self.qr, pos=(x_pos, y_pos)) + + async def queue_next(self, entry: Entry) -> None: loop = asyncio.get_running_loop() self.play_image(f"{__dirname__}/static/background20perc.png", 3) @@ -79,21 +67,39 @@ class Player: await loop.run_in_executor(None, self.mpv.wait_for_property, "eof-reached") - def play_image(self, image: str, duration: int): + def play_image(self, image: str, duration: int) -> None: + for property, value in self.default_options.items(): + self.mpv[property] = value self.mpv.image_display_duration = duration self.mpv.keep_open = "yes" self.mpv.play(image) self.mpv.pause = False - async def play(self, video: str, audio: Optional[str] = None): + async def play( + self, + video: str, + audio: Optional[str] = None, + override_options: Optional[dict[str, str]] = None, + ) -> None: + if override_options is None: + override_options = {} + for property, value in self.default_options.items(): + self.mpv[property] = value + + for property, value in override_options.items(): + self.mpv[property] = value + loop = asyncio.get_running_loop() - self.audio = audio self.mpv.pause = True self.mpv.play(video) + await loop.run_in_executor(None, self.mpv.wait_for_event, "file-loaded") + if audio: + self.mpv.audio_add(audio) self.mpv.pause = False await loop.run_in_executor(None, self.mpv.wait_for_property, "eof-reached") + self.mpv.play(f"{__dirname__}/static/background.png") - def skip_current(self): + def skip_current(self) -> None: self.mpv.playlist_append( f"{__dirname__}/static/background.png", ) diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py index 2f93c8c..b31ef22 100644 --- a/syng/sources/filebased.py +++ b/syng/sources/filebased.py @@ -40,6 +40,7 @@ class FileBasedSource(Source): self.extensions: list[str] = config["extensions"] if "extensions" in config else ["mp3+cdg"] self.extra_mpv_arguments = ["--scale=oversample"] + self.extra_mpv_options = {"scale": "oversample"} def has_correct_extension(self, path: Optional[str]) -> bool: """ diff --git a/syng/sources/source.py b/syng/sources/source.py index 878367f..8a4c039 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -130,6 +130,7 @@ class Source(ABC): # self.player: Optional[asyncio.subprocess.Process] = None self._index: list[str] = config["index"] if "index" in config else [] self.extra_mpv_arguments: list[str] = [] + self.extra_mpv_options: dict[str, str] = {} self._skip_next = False @staticmethod @@ -341,7 +342,7 @@ class Source(ABC): # if self.player is not None: # self.player.kill() - async def ensure_playable(self, entry: Entry) -> None: + async def ensure_playable(self, entry: Entry) -> tuple[str, Optional[str]]: """ Guaranties that the given entry can be played. @@ -352,7 +353,9 @@ class Source(ABC): :rtype: None """ await self.buffer(entry) - await self.downloaded_files[entry.ident].ready.wait() + dlfilesentry = self.downloaded_files[entry.ident] + await dlfilesentry.ready.wait() + return dlfilesentry.video, dlfilesentry.audio async def get_missing_metadata(self, _entry: Entry) -> dict[str, Any]: """ diff --git a/syng/sources/youtube.py b/syng/sources/youtube.py index 9a72a0c..5405a37 100644 --- a/syng/sources/youtube.py +++ b/syng/sources/youtube.py @@ -221,6 +221,7 @@ class YoutubeSource(Source): self.formatstring = ( f"bestvideo[height<={self.max_res}]+" f"bestaudio/best[height<={self.max_res}]" ) + self.extra_mpv_options = {"ytdl-format": self.formatstring} self._yt_dlp = YoutubeDL( params={ "paths": {"home": self.tmp_dir}, diff --git a/typings/mpv.pyi b/typings/mpv.pyi new file mode 100644 index 0000000..e0ea1cf --- /dev/null +++ b/typings/mpv.pyi @@ -0,0 +1,38 @@ +from typing import Any, Callable, Iterable, Protocol + +from PIL.Image import Image + +class Unregisterable(Protocol): + def unregister(self) -> None: ... + +class ImageOverlay: + overlay_id: int + def remove(self) -> None: ... + +class MPV: + pause: bool + keep_open: str + image_display_duration: int + sub_pos: int + osd_width: str + osd_height: str + + def __init__( + self, ytdl: bool, input_default_bindings: bool, input_vo_keyboard: bool, osc: bool + ) -> None: ... + def terminate(self) -> None: ... + def play(self, file: str) -> None: ... + def playlist_append(self, file: str) -> None: ... + def wait_for_property(self, property: str) -> None: ... + def playlist_next(self) -> None: ... + def audio_add(self, file: str) -> None: ... + def wait_for_event(self, event: str) -> None: ... + def python_stream( + self, stream_name: str + ) -> Callable[[Callable[[], Iterable[bytes]]], Unregisterable]: ... + def sub_add(self, file: str) -> None: ... + def create_image_overlay(self, image: Image, pos: tuple[int, int]) -> ImageOverlay: ... + def remove_overlay(self, overlay_id: int) -> None: ... + def observe_property(self, property: str, callback: Callable[[str, Any], None]) -> None: ... + def __setitem__(self, key: str, value: str) -> None: ... + def __getitem__(self, key: str) -> str: ...