Compare commits

..

No commits in common. "df242e833f6821f12c6af4b505c1e103c79867a3" and "2b47b002e68f06e02c70d2dc99c514b673b14f1d" have entirely different histories.

7 changed files with 32 additions and 75 deletions

1
.gitignore vendored
View file

@ -3,4 +3,3 @@ __pycache__/
.ruff_cache/ .ruff_cache/
music/ music/
cookies/

View file

@ -1,7 +1,7 @@
.PHONY: run test build frontend backend .PHONY: run test build frontend backend
run: run:
HOST=127.0.0.1 PORT=4009 COOKIES_DIR=cookies python3 ./backend/main.py @echo 'Not implemented'
test: test:
@python3 -m unittest discover -vcs ./backend @python3 -m unittest discover -vcs ./backend

View file

@ -1,22 +0,0 @@
import os
from pathlib import Path
class Config:
def __init__(self) -> None:
self.host = os.getenv('HOST') or '0.0.0.0'
self.port = int(os.getenv('PORT') or 4009)
self.path_length = int(os.getenv('PATH_LENGTH') or 255)
self.cookies_dir = Path(os.getenv('COOKIES_DIR') or 'cookies')
_config: Config | None = None
def get() -> Config:
global _config
if _config is None:
_config = Config()
return _config

View file

@ -1,19 +1,27 @@
import asyncio import asyncio
import json import json
import secrets
import time
import websockets import websockets
import config
import response import response
import ydl_pool import ydl_pool
import ydl_wrap import ydl_wrap
type SocketT = websockets.WebSocketServerProtocol # TODO: config
HOST = '127.0.0.1'
PORT = 5678
def generate_key() -> str:
async def handler(socket: SocketT) -> None: return hex(time.time_ns()) + secrets.token_hex(2)
async def handler(socket: websockets.WebSocketServerProtocol) -> None:
ydls = ydl_pool.Downloaders() ydls = ydl_pool.Downloaders()
@ -33,12 +41,14 @@ async def handler(socket: SocketT) -> None:
)) ))
case 'download': # download by URL case 'download': # download by URL
ret = await ydl_wrap.download( # TODO: send all yt-dlp's output to client
ydls.get_ydl(data['site']), await socket.send(response.ok_download(
data['url'], await ydl_wrap.download(
data.get('items'), ydls.get_ydl(data['site']),
) data['url'],
await socket.send(response.ok_downloaded(ret)) data.get('items'),
)
))
# TODO: cancellation # TODO: cancellation
@ -56,10 +66,9 @@ async def handler(socket: SocketT) -> None:
async def main() -> None: async def main() -> None:
cfg = config.get() async with websockets.serve(handler, HOST, PORT):
async with websockets.serve(handler, cfg.host, cfg.port):
await asyncio.Future() await asyncio.Future()
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main(), debug=True) asyncio.run(main())

View file

@ -9,9 +9,9 @@ def ok_playlist(items: list[str]) -> str:
"data": items, "data": items,
}) })
def ok_downloaded(ret: int) -> str: def ok_download(ret: int) -> str:
return json.dumps({ return json.dumps({
"type": "downloaded", "ok": True,
"data": ret, "data": ret,
}) })

View file

@ -1,7 +1,6 @@
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from yt_dlp.postprocessor import FFmpegExtractAudioPP from yt_dlp.postprocessor import FFmpegExtractAudioPP
import config
import id3pp import id3pp
@ -22,7 +21,7 @@ class _CreateYDL:
@staticmethod @staticmethod
def yandex() -> YoutubeDL: def yandex() -> YoutubeDL:
return YoutubeDL() return YoutubeDL() # TODO: cookies
create_ydl_fn = { create_ydl_fn = {
@ -46,21 +45,13 @@ class Downloaders:
def get_ydl(self, site: str) -> YoutubeDL: def get_ydl(self, site: str) -> YoutubeDL:
cfg = config.get()
ydl = self.ydls[site] ydl = self.ydls[site]
if ydl is None: if ydl is None:
ydl = create_ydl_fn[site]() ydl = create_ydl_fn[site]()
ydl.params['trim_file_name'] = 40 # TODO: config
ydl.params['trim_file_name'] = cfg.path_length # NOTE: includes path, not only filename
# artists.0 instead of artist, because it can contain "feat. ..." # artists.0 instead of artist, because it can contain "feat. ..."
ydl.params['outtmpl']['default'] = 'music/%(artists.0)s/%(album)s/%(track)s.%(ext)s' ydl.params['outtmpl']['default'] = 'music/%(artists.0)s/%(album)s/%(track)s.%(ext)s'
ydl.add_post_processor(id3pp.ID3TagsPP(), when='post_process') ydl.add_post_processor(id3pp.ID3TagsPP(), when='post_process')
cookies = cfg.cookies_dir / (site + '.txt')
if cookies.exists():
ydl.params['cookiefile'] = str(cookies)
return ydl return ydl
def cleanup(self) -> None: def cleanup(self) -> None:

View file

@ -6,43 +6,23 @@ from yt_dlp import YoutubeDL
async def get_playlist_items(ydl: YoutubeDL, url: str) -> list[str]: async def get_playlist_items(ydl: YoutubeDL, url: str) -> list[str]:
return await asyncio.get_event_loop().run_in_executor( return await asyncio.get_event_loop().run_in_executor(None, _target_get_playlist_items, ydl, url)
None,
_target_get_playlist_items,
ydl,
url,
)
def _target_get_playlist_items(ydl: YoutubeDL, url: str) -> list[str]: def _target_get_playlist_items(ydl: YoutubeDL, url: str) -> list[str]:
info = ydl.extract_info(url, download=False, process=True) info = ydl.extract_info(url, download=False, process=False)
if info is None: if info is None:
raise RuntimeError('ydl.extract_info returned None') raise RuntimeError('ydl.extract_info returned None')
return [ return [entry['title'] for entry in info['entries']]
entry['track'] if 'track' in entry else entry['title']
for entry in info['entries']
]
async def download( async def download(ydl: YoutubeDL, url: str, playlist_items: Iterable[int] | None = None) -> int:
ydl: YoutubeDL,
url: str,
playlist_items: Iterable[int] | None = None) -> int:
return await asyncio.get_event_loop().run_in_executor( return await asyncio.get_event_loop().run_in_executor(None, _target_download, ydl, url, playlist_items)
None,
_target_download,
ydl,
url,
playlist_items,
)
def _target_download( def _target_download(ydl: YoutubeDL, url: str, playlist_items: Iterable[int] | None = None) -> int:
ydl: YoutubeDL,
url: str,
playlist_items: Iterable[int] | None = None) -> int:
if playlist_items: if playlist_items:
ydl.params['playlist_items'] = ','.join(str(i) for i in playlist_items) ydl.params['playlist_items'] = ','.join(str(i) for i in playlist_items)