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.sources = configure_sources(config["sources"])
self.state = State() self.state = State()
self.currentLock = asyncio.Semaphore(0) self.currentLock = asyncio.Semaphore(0)
print("blubb")
self.player = Player() self.player = Player()
self.register_handlers() self.register_handlers()
@ -270,17 +271,20 @@ class Client:
:rtype: None :rtype: None
""" """
entry: Entry = Entry(**data) entry: Entry = Entry(**data)
source = self.sources[entry.source]
print( print(
f"Playing: {entry.artist} - {entry.title} [{entry.album}] " f"Playing: {entry.artist} - {entry.title} [{entry.album}] "
f"({entry.source}) for {entry.performer}" f"({entry.source}) for {entry.performer}"
) )
try: try:
self.state.current_source = self.sources[entry.source] # self.state.current_source = self.sources[entry.source]
if self.state.config["preview_duration"] > 0: if self.state.config["preview_duration"] > 0:
await self.preview(entry) await self.preview(entry)
await self.sources[entry.source].play( video, audio = await source.ensure_playable(entry)
entry, self.player, self.state.config["mpv_options"] 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 except Exception: # pylint: disable=broad-except
print_exc() print_exc()
self.state.current_source = None self.state.current_source = None
@ -421,9 +425,8 @@ class Client:
:rtype: None :rtype: None
""" """
engineio.async_client.async_signal_handler() engineio.async_client.async_signal_handler()
if self.state.current_source is not None: if self.player is not None:
if self.state.current_source.player is not None: # TODO old player self.player.mpv.terminate()
self.state.current_source.player.kill()
async def start_client(self, config: dict[str, Any]) -> None: async def start_client(self, config: dict[str, Any]) -> None:
""" """
@ -469,9 +472,8 @@ class Client:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
finally: finally:
if self.state.current_source is not None: if self.player is not None:
if self.state.current_source.player is not None: # TODO old player self.player.mpv.terminate()
self.state.current_source.player.kill()
def create_async_and_start_client( def create_async_and_start_client(

View file

@ -1,11 +1,10 @@
import asyncio import asyncio
import locale
import sys import sys
import tempfile from typing import Iterable, Optional, cast
from typing import Iterable, Optional
from qrcode.main import QRCode from qrcode.main import QRCode
import mpv import mpv
import os import os
from PIL.Image import Image
from .entry import Entry from .entry import Entry
@ -14,33 +13,11 @@ __dirname__ = os.path.dirname(__file__)
class Player: class Player:
def osd_size_handler(self, *args): def __init__(self) -> None:
if self.qr_overlay: locale.setlocale(locale.LC_ALL, "C")
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):
self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True) self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True)
self.mpv.keep_open = "yes" self.mpv.keep_open = "yes"
self.audio = None self.qr_overlay: Optional[mpv.ImageOverlay] = None
self.qr_overlay = None
qr = QRCode(box_size=5, border=1) qr = QRCode(box_size=5, border=1)
qr.add_data("https://syng.rocks/") qr.add_data("https://syng.rocks/")
qr.make() qr.make()
@ -50,15 +27,26 @@ class Player:
f"{__dirname__}/static/background.png", 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-width", self.osd_size_handler)
self.mpv.observe_property("osd-height", 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): def osd_size_handler(self, attribute: str, value: int) -> None:
# self.queue_next(entry) if self.qr_overlay:
# self.play(video, audio) 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() loop = asyncio.get_running_loop()
self.play_image(f"{__dirname__}/static/background20perc.png", 3) 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") 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.image_display_duration = duration
self.mpv.keep_open = "yes" self.mpv.keep_open = "yes"
self.mpv.play(image) self.mpv.play(image)
self.mpv.pause = False 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() loop = asyncio.get_running_loop()
self.audio = audio
self.mpv.pause = True self.mpv.pause = True
self.mpv.play(video) 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 self.mpv.pause = False
await loop.run_in_executor(None, self.mpv.wait_for_property, "eof-reached") 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( self.mpv.playlist_append(
f"{__dirname__}/static/background.png", 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.extensions: list[str] = config["extensions"] if "extensions" in config else ["mp3+cdg"]
self.extra_mpv_arguments = ["--scale=oversample"] self.extra_mpv_arguments = ["--scale=oversample"]
self.extra_mpv_options = {"scale": "oversample"}
def has_correct_extension(self, path: Optional[str]) -> bool: 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.player: Optional[asyncio.subprocess.Process] = None
self._index: list[str] = config["index"] if "index" in config else [] self._index: list[str] = config["index"] if "index" in config else []
self.extra_mpv_arguments: list[str] = [] self.extra_mpv_arguments: list[str] = []
self.extra_mpv_options: dict[str, str] = {}
self._skip_next = False self._skip_next = False
@staticmethod @staticmethod
@ -341,7 +342,7 @@ class Source(ABC):
# if self.player is not None: # if self.player is not None:
# self.player.kill() # 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. Guaranties that the given entry can be played.
@ -352,7 +353,9 @@ class Source(ABC):
:rtype: None :rtype: None
""" """
await self.buffer(entry) 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]: async def get_missing_metadata(self, _entry: Entry) -> dict[str, Any]:
""" """

View file

@ -221,6 +221,7 @@ class YoutubeSource(Source):
self.formatstring = ( self.formatstring = (
f"bestvideo[height<={self.max_res}]+" f"bestaudio/best[height<={self.max_res}]" 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( self._yt_dlp = YoutubeDL(
params={ params={
"paths": {"home": self.tmp_dir}, "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: ...