most things work, only skipping song needs work

This commit is contained in:
Christoph Stahl 2024-10-10 00:25:39 +02:00
parent e51acd075a
commit 63555dde87
6 changed files with 100 additions and 49 deletions

View file

@ -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(

View file

@ -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",
)

View file

@ -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:
"""

View file

@ -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]:
"""

View file

@ -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},

38
typings/mpv.pyi Normal file
View file

@ -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: ...