diff --git a/.gitignore b/.gitignore index fa3e14e..1e1cf5d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,8 @@ yalc.lock .vscode/settings.json # Ignore output folder -backend/out \ No newline at end of file +backend/out + +.pnpm-store/ +cli/ +out/ diff --git a/main.py b/main.py index 1d88531..4497db1 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,171 @@ +import asyncio +import base64 +import datetime +import glob +import json import os -import decky_plugin +import ssl +from asyncio import Lock + +import certifi +import decky +from aiohttp import ClientSession from settings import SettingsManager + class Plugin: + yt_process: asyncio.subprocess.Process | None = None + # We need this lock to make sure the process output isn't read by two concurrent readers at once. + yt_process_lock = Lock() + music_path = f"{decky.DECKY_PLUGIN_RUNTIME_DIR}/music" + cache_path = f"{decky.DECKY_PLUGIN_RUNTIME_DIR}/cache" + ssl_context = ssl.create_default_context(cafile=certifi.where()) + async def _main(self): - self.settings = SettingsManager(name="config", settings_directory=decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) + self.settings = SettingsManager( + name="config", settings_directory=decky.DECKY_PLUGIN_SETTINGS_DIR + ) async def _unload(self): - pass + if self.yt_process is not None: + self.yt_process.terminate() + # Wait for process to terminate. + async with self.yt_process_lock: + try: + # Allow up to 5 seconds for termination. + await asyncio.wait_for(self.yt_process.communicate(), timeout=5) + except TimeoutError: + # Otherwise, send SIGKILL. + self.yt_process.kill() async def set_setting(self, key, value): self.settings.setSetting(key, value) async def get_setting(self, key, default): - return self.settings.getSetting(key, default) \ No newline at end of file + return self.settings.getSetting(key, default) + + async def search_yt(self, term: str): + if self.yt_process is not None: + self.yt_process.terminate() + # Wait for process to terminate. + async with self.yt_process_lock: + await self.yt_process.communicate() + self.yt_process = await asyncio.create_subprocess_exec( + f"{decky.DECKY_PLUGIN_DIR}/bin/yt-dlp", + f"ytsearch10:{term}", + "-j", + "-f", + "bestaudio", + "--match-filters", + f"duration str | None: + local_matches = [ + x for x in glob.glob(f"{self.music_path}/{id}.*") if os.path.isfile(x) + ] + if len(local_matches) == 0: + return None + + assert ( + len(local_matches) == 1 + ), "More than one downloaded audio with same ID found." + return local_matches[0] + + async def single_yt_url(self, id: str): + local_match = self.local_match(id) + if local_match is not None: + # The audio has already been downloaded, so we can just use that one. + # However, we cannot use local paths in the