diff --git a/syng/client.py b/syng/client.py index c78c1f6..5aae08e 100644 --- a/syng/client.py +++ b/syng/client.py @@ -25,7 +25,7 @@ state = {"current": None, "queue": [], "recent": [], "room": None, "server": "", @sio.on("skip") async def handle_skip(): logger.info("Skipping current") - await state["current"].skip_current() + await state["current"].skip_current(state["queue"][0]) @sio.on("state") @@ -55,6 +55,11 @@ async def handle_buffer(data): await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info}) +async def buffer_and_report(entry): + meta_info = await sources[entry.source].buffer(entry) + await sio.emit("meta-info", {"uuid": entry.uuid, "meta": meta_info}) + + @sio.on("play") async def handle_play(data): entry = Entry(**data) @@ -62,9 +67,8 @@ async def handle_play(data): f"Playing: {entry.artist} - {entry.title} [{entry.album}] ({entry.source}) for {entry.performer}" ) try: - meta_info = await sources[entry.source].buffer(entry) - await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info}) state["current"] = sources[entry.source] + asyncio.create_task(buffer_and_report(entry)) await sources[entry.source].play(entry) except Exception: print_exc() diff --git a/syng/sources/common.py b/syng/sources/common.py index 1df7ed3..8b917dc 100644 --- a/syng/sources/common.py +++ b/syng/sources/common.py @@ -3,9 +3,9 @@ import asyncio async def play_mpv( - video: str, audio: str | None, options + video: str, audio: str | None, options: list[str] = list() ) -> asyncio.subprocess.Process: - args = [*options, video] + ([f"--audio-file={audio}"] if audio else []) + args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else []) mpv_process = asyncio.create_subprocess_exec("mpv", *args) return await mpv_process diff --git a/syng/sources/s3.py b/syng/sources/s3.py index 18de94c..d9d07a1 100644 --- a/syng/sources/s3.py +++ b/syng/sources/s3.py @@ -57,12 +57,12 @@ class S3Source(Source): mp3_file = self.downloaded_files[entry.uuid]["mp3"] self.player = await play_mpv( - cdg_file, mp3_file, ["--scale=oversample", "--fullscreen"] + cdg_file, mp3_file, ["--scale=oversample"] ) await self.player.wait() - async def skip_current(self) -> None: + async def skip_current(self, entry) -> None: await self.player.kill() @async_in_thread diff --git a/syng/sources/source.py b/syng/sources/source.py index 6840e97..e7d8039 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -29,7 +29,7 @@ class Source: async def play(self, entry: Entry) -> None: raise NotImplementedError - async def skip_current(self) -> None: + async def skip_current(self, entry: Entry) -> None: pass async def init_server(self) -> None: diff --git a/syng/sources/youtube.py b/syng/sources/youtube.py index 288880e..22f1195 100644 --- a/syng/sources/youtube.py +++ b/syng/sources/youtube.py @@ -1,6 +1,7 @@ import asyncio import shlex from functools import partial +from threading import Event, Lock from pytube import YouTube, Search, Channel, innertube @@ -15,24 +16,36 @@ class YoutubeSource(Source): super().__init__() self.innertube_client = innertube.InnerTube(client="WEB") self.channels = config["channels"] if "channels" in config else [] + self.tmp_dir = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng" self.player: None | asyncio.subprocess.Process = None + self.downloaded_files = {} + self.masterlock = Lock() async def get_config(self): return {"channels": self.channels} async def play(self, entry: Entry) -> None: - self.player = await play_mpv( - entry.id, - None, - [ - "--script-opts=ytdl_hook-ytdl_path=yt-dlp,ytdl_hook-exclude='%.pls$'", - "--ytdl-format=bestvideo[height<=720]+bestaudio/best[height<=720]", - "--fullscreen", - ], - ) + + if entry.uuid in self.downloaded_files and "video" in self.downloaded_files[entry.uuid]: + print("playing locally") + video_file = self.downloaded_files[entry.uuid]["video"] + audio_file = self.downloaded_files[entry.uuid]["audio"] + self.player = await play_mpv(video_file, audio_file) + else: + print("streaming") + self.player = await play_mpv( + entry.id, + None, + [ + "--script-opts=ytdl_hook-ytdl_path=yt-dlp,ytdl_hook-exclude='%.pls$'", + "--ytdl-format=bestvideo[height<=720]+bestaudio/best[height<=720]", + "--fullscreen", + ], + ) + await self.player.wait() - async def skip_current(self) -> None: + async def skip_current(self, entry) -> None: await self.player.kill() @async_in_thread @@ -121,5 +134,39 @@ class YoutubeSource(Source): pass return list_of_videos + @async_in_thread + def buffer(self, entry: Entry) -> dict: + print(f"Buffering {entry}") + with self.masterlock: + if entry.uuid in self.downloaded_files: + print(f"Already buffering {entry}") + return {} + self.downloaded_files[entry.uuid] = {} + + yt = YouTube(entry.id) + + streams = yt.streams + + video_streams = streams.filter( + type="video", + custom_filter_functions=[lambda s: int(s.resolution[:-1]) <= 1080] + ) + audio_streams = streams.filter(only_audio=True) + + best_720_stream = sorted(video_streams, key=lambda s: int(s.resolution[:-1]) + (1 if s.is_progressive else 0))[-1] + best_audio_stream = sorted(audio_streams, key=lambda s: int(s.abr[:-4]))[-1] + + print(best_720_stream) + print(best_audio_stream) + + if not best_720_stream.is_progressive: + self.downloaded_files[entry.uuid]["audio"] = best_audio_stream.download(output_path=self.tmp_dir, filename_prefix=f"{entry.uuid}-audio") + else: + self.downloaded_files[entry.uuid]["audio"] = None + + self.downloaded_files[entry.uuid]["video"] = best_720_stream.download(output_path=self.tmp_dir, filename_prefix=entry.uuid) + + return {} + available_sources["youtube"] = YoutubeSource