Compare commits
2 commits
2b47b002e6
...
df242e833f
Author | SHA1 | Date | |
---|---|---|---|
df242e833f | |||
93c5ea3b18 |
7 changed files with 75 additions and 32 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ __pycache__/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
|
||||||
music/
|
music/
|
||||||
|
cookies/
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -1,7 +1,7 @@
|
||||||
.PHONY: run test build frontend backend
|
.PHONY: run test build frontend backend
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@echo 'Not implemented'
|
HOST=127.0.0.1 PORT=4009 COOKIES_DIR=cookies python3 ./backend/main.py
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@python3 -m unittest discover -vcs ./backend
|
@python3 -m unittest discover -vcs ./backend
|
||||||
|
|
22
backend/config.py
Normal file
22
backend/config.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
|
@ -1,27 +1,19 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
# TODO: config
|
type SocketT = websockets.WebSocketServerProtocol
|
||||||
HOST = '127.0.0.1'
|
|
||||||
PORT = 5678
|
|
||||||
|
|
||||||
|
|
||||||
def generate_key() -> str:
|
|
||||||
|
|
||||||
return hex(time.time_ns()) + secrets.token_hex(2)
|
async def handler(socket: SocketT) -> None:
|
||||||
|
|
||||||
|
|
||||||
async def handler(socket: websockets.WebSocketServerProtocol) -> None:
|
|
||||||
|
|
||||||
ydls = ydl_pool.Downloaders()
|
ydls = ydl_pool.Downloaders()
|
||||||
|
|
||||||
|
@ -41,14 +33,12 @@ async def handler(socket: websockets.WebSocketServerProtocol) -> None:
|
||||||
))
|
))
|
||||||
|
|
||||||
case 'download': # download by URL
|
case 'download': # download by URL
|
||||||
# TODO: send all yt-dlp's output to client
|
ret = await ydl_wrap.download(
|
||||||
await socket.send(response.ok_download(
|
|
||||||
await ydl_wrap.download(
|
|
||||||
ydls.get_ydl(data['site']),
|
ydls.get_ydl(data['site']),
|
||||||
data['url'],
|
data['url'],
|
||||||
data.get('items'),
|
data.get('items'),
|
||||||
)
|
)
|
||||||
))
|
await socket.send(response.ok_downloaded(ret))
|
||||||
|
|
||||||
# TODO: cancellation
|
# TODO: cancellation
|
||||||
|
|
||||||
|
@ -66,9 +56,10 @@ async def handler(socket: websockets.WebSocketServerProtocol) -> None:
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with websockets.serve(handler, HOST, PORT):
|
cfg = config.get()
|
||||||
|
async with websockets.serve(handler, cfg.host, cfg.port):
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(main())
|
asyncio.run(main(), debug=True)
|
||||||
|
|
|
@ -9,9 +9,9 @@ def ok_playlist(items: list[str]) -> str:
|
||||||
"data": items,
|
"data": items,
|
||||||
})
|
})
|
||||||
|
|
||||||
def ok_download(ret: int) -> str:
|
def ok_downloaded(ret: int) -> str:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"ok": True,
|
"type": "downloaded",
|
||||||
"data": ret,
|
"data": ret,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ class _CreateYDL:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def yandex() -> YoutubeDL:
|
def yandex() -> YoutubeDL:
|
||||||
return YoutubeDL() # TODO: cookies
|
return YoutubeDL()
|
||||||
|
|
||||||
|
|
||||||
create_ydl_fn = {
|
create_ydl_fn = {
|
||||||
|
@ -45,13 +46,21 @@ 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:
|
||||||
|
|
|
@ -6,23 +6,43 @@ 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(None, _target_get_playlist_items, ydl, url)
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
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=False)
|
info = ydl.extract_info(url, download=False, process=True)
|
||||||
if info is None:
|
if info is None:
|
||||||
raise RuntimeError('ydl.extract_info returned None')
|
raise RuntimeError('ydl.extract_info returned None')
|
||||||
return [entry['title'] for entry in info['entries']]
|
return [
|
||||||
|
entry['track'] if 'track' in entry else entry['title']
|
||||||
|
for entry in info['entries']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def download(ydl: YoutubeDL, url: str, playlist_items: Iterable[int] | None = None) -> int:
|
async def download(
|
||||||
|
ydl: YoutubeDL,
|
||||||
|
url: str,
|
||||||
|
playlist_items: Iterable[int] | None = None) -> int:
|
||||||
|
|
||||||
return await asyncio.get_event_loop().run_in_executor(None, _target_download, ydl, url, playlist_items)
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
_target_download,
|
||||||
|
ydl,
|
||||||
|
url,
|
||||||
|
playlist_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _target_download(ydl: YoutubeDL, url: str, playlist_items: Iterable[int] | None = None) -> int:
|
def _target_download(
|
||||||
|
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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue