From b9bfea02573ea88ad620f4c7325d249ae9e39f99 Mon Sep 17 00:00:00 2001 From: timotheyca Date: Sun, 19 Jun 2022 20:41:11 +0300 Subject: [PATCH] refactor --- v6d3music/app.py | 22 +- v6d3music/create_ytaudio.py | 32 ++ v6d3music/create_ytaudios.py | 21 ++ v6d3music/html/home.html | 7 +- v6d3music/html/login.html | 6 +- v6d3music/html/main.css | 1 + v6d3music/html/main.js | 47 ++- v6d3music/mainaudio.py | 28 ++ v6d3music/queueaudio.py | 107 +++++++ v6d3music/run-bot.py | 311 +------------------- v6d3music/run-extract.py | 4 +- v6d3music/utils/aextract.py | 18 ++ v6d3music/utils/catch.py | 10 + v6d3music/utils/effects_for_preset.py | 10 + v6d3music/utils/entries_effects_for_args.py | 39 +++ v6d3music/utils/entries_for_url.py | 30 ++ v6d3music/{ => utils}/extract.py | 4 - v6d3music/utils/info_tuple.py | 3 + v6d3music/utils/presets.py | 12 + v6d3music/utils/tor_extract.py | 22 ++ v6d3music/yt_audios.py | 20 ++ 21 files changed, 426 insertions(+), 328 deletions(-) create mode 100644 v6d3music/create_ytaudio.py create mode 100644 v6d3music/create_ytaudios.py create mode 100644 v6d3music/mainaudio.py create mode 100644 v6d3music/queueaudio.py create mode 100644 v6d3music/utils/aextract.py create mode 100644 v6d3music/utils/catch.py create mode 100644 v6d3music/utils/effects_for_preset.py create mode 100644 v6d3music/utils/entries_effects_for_args.py create mode 100644 v6d3music/utils/entries_for_url.py rename v6d3music/{ => utils}/extract.py (89%) create mode 100644 v6d3music/utils/info_tuple.py create mode 100644 v6d3music/utils/presets.py create mode 100644 v6d3music/utils/tor_extract.py create mode 100644 v6d3music/yt_audios.py diff --git a/v6d3music/app.py b/v6d3music/app.py index 74f1322..553e0db 100644 --- a/v6d3music/app.py +++ b/v6d3music/app.py @@ -5,13 +5,16 @@ from pathlib import Path import aiohttp import discord from aiohttp import web -from ptvp35 import Db +from ptvp35 import Db, KVJson from v6d0auth.appfactory import AppFactory from v6d0auth.run_app import start_app from v6d1tokens.client import request_token +from v6d3music.config import myroot from v6d3music.utils.bytes_hash import bytes_hash +session_db = Db(myroot / 'session.db', kvrequest_type=KVJson) + class MusicAppFactory(AppFactory): htmlroot = Path(__file__).parent / 'html' @@ -19,7 +22,6 @@ class MusicAppFactory(AppFactory): def __init__( self, secret: str, - db: Db, client: discord.Client ): self.secret = secret @@ -27,7 +29,6 @@ class MusicAppFactory(AppFactory): self.discord_auth = 'https://discord.com/api/oauth2/authorize?client_id=914432576926646322' \ f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify' self.loop = asyncio.get_running_loop() - self.db = db self.client = client def _file(self, file: str): @@ -106,7 +107,7 @@ class MusicAppFactory(AppFactory): discriminator = cls.user_discriminator(user) if discriminator is None: return None - return username + discriminator + return f'{username}#{discriminator}' @classmethod def user_username(cls, user: dict): @@ -147,8 +148,9 @@ class MusicAppFactory(AppFactory): 'client': (None if sclient is None else self.client_status(sclient)) } - def session_data(self, session: str) -> dict: - data = self.db.get(session, {}) + @classmethod + def session_data(cls, session: str) -> dict: + data = session_db.get(session, {}) if not isinstance(data, dict): data = {} return data @@ -177,7 +179,7 @@ class MusicAppFactory(AppFactory): data = self.session_data(session) data['code'] = code data['token'] = await self.code_token(code) - await self.db.set(session, data) + await session_db.set(session, data) return response else: return await self.html_resp('auth') @@ -186,7 +188,7 @@ class MusicAppFactory(AppFactory): async def state(request: web.Request) -> web.Response: session = str(request.query.get('session')) return web.json_response( - data=f"{bytes_hash(session.encode())}" + data=f'{bytes_hash(session.encode())}' ) @routes.get('/status/') @@ -209,6 +211,6 @@ class MusicAppFactory(AppFactory): ) @classmethod - async def start(cls, db: Db, client: discord.Client): - factory = cls(await request_token('music-client', 'token'), db, client) + async def start(cls, client: discord.Client): + factory = cls(await request_token('music-client', 'token'), client) await start_app(factory.app()) diff --git a/v6d3music/create_ytaudio.py b/v6d3music/create_ytaudio.py new file mode 100644 index 0000000..9086769 --- /dev/null +++ b/v6d3music/create_ytaudio.py @@ -0,0 +1,32 @@ +import string +from typing import Any, Optional + +from v6d2ctx.context import Context, Explicit, escape + +from v6d3music.real_url import real_url +from v6d3music.utils.assert_admin import assert_admin +from v6d3music.utils.options_for_effects import options_for_effects +from v6d3music.utils.presets import allowed_effects +from v6d3music.ytaudio import YTAudio + + +async def create_ytaudio( + ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int, tor: bool +) -> YTAudio: + if effects: + if effects not in allowed_effects: + assert_admin(ctx.member) + if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'): + raise Explicit('malformed effects') + options = options_for_effects(effects) + else: + options = None + return YTAudio( + await real_url(info['url'], False, tor), + info['url'], + f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}', + options, + ctx.member, + already_read, + tor + ) diff --git a/v6d3music/create_ytaudios.py b/v6d3music/create_ytaudios.py new file mode 100644 index 0000000..da52fb6 --- /dev/null +++ b/v6d3music/create_ytaudios.py @@ -0,0 +1,21 @@ +import asyncio +from typing import AsyncIterable + +from v6d2ctx.context import Context + +from v6d3music.create_ytaudio import create_ytaudio +from v6d3music.utils.info_tuple import info_tuple +from v6d3music.ytaudio import YTAudio + + +async def create_ytaudios(ctx: Context, infos: list[info_tuple]) -> AsyncIterable[YTAudio]: + for audio in await asyncio.gather( + *[ + create_ytaudio(ctx, info, effects, already_read, tor) + for + info, effects, already_read, tor + in + infos + ] + ): + yield audio diff --git a/v6d3music/html/home.html b/v6d3music/html/home.html index 64b048c..51ab999 100644 --- a/v6d3music/html/home.html +++ b/v6d3music/html/home.html @@ -3,11 +3,6 @@ diff --git a/v6d3music/html/login.html b/v6d3music/html/login.html index 877a2cd..c1f54fb 100644 --- a/v6d3music/html/login.html +++ b/v6d3music/html/login.html @@ -2,12 +2,12 @@
diff --git a/v6d3music/html/main.css b/v6d3music/html/main.css index 8411934..4b88557 100644 --- a/v6d3music/html/main.css +++ b/v6d3music/html/main.css @@ -1,4 +1,5 @@ html, body { color: white; background: black; + margin: 0; } diff --git a/v6d3music/html/main.js b/v6d3music/html/main.js index eb2c44a..6e1c013 100644 --- a/v6d3music/html/main.js +++ b/v6d3music/html/main.js @@ -49,10 +49,45 @@ const userUsername = async () => { return user && user['username']; }; const userAvatarImg = async () => { - const img = document.createElement('img'); - img.src = await userAvatarUrl(); - img.width = 128; - img.height = 128; - img.alt = await userUsername(); - return img; + const avatar = await userAvatarUrl(); + if (avatar) { + const img = document.createElement('img'); + img.src = avatar; + img.width = 128; + img.height = 128; + img.alt = await userUsername(); + return img; + } else { + return baseEl('span'); + } +}; +const userId = async () => { + const user = await sessionUser(); + return user && user['id']; +}; +const baseEl = (tag, ...appended) => { + const element = document.createElement(tag); + element.append(...appended); + return element; +}; +const aLogin = () => { + const a = document.createElement('a'); + a.href = '/login/'; + a.innerText = 'login'; + return a; +}; +const pageHome = async () => { + return baseEl( + 'div', + baseEl('div', aLogin()), + baseEl('div', await userAvatarImg()), + await userId() + ) +}; +let authbase; +const aAuth = async () => { + const a = document.createElement('a'); + a.href = authbase + '&state=' + await sessionState(); + a.innerText = 'auth'; + return a; }; diff --git a/v6d3music/mainaudio.py b/v6d3music/mainaudio.py new file mode 100644 index 0000000..b9fa4a6 --- /dev/null +++ b/v6d3music/mainaudio.py @@ -0,0 +1,28 @@ +import discord +from ptvp35 import Db, KVJson +from v6d2ctx.context import Explicit + +from v6d3music.config import myroot +from v6d3music.queueaudio import QueueAudio +from v6d3music.utils.assert_admin import assert_admin + +volume_db = Db(myroot / 'volume.db', kvrequest_type=KVJson) + + +class MainAudio(discord.PCMVolumeTransformer): + def __init__(self, queue: QueueAudio, volume: float): + self.queue = queue + super().__init__(self.queue, volume=volume) + + async def set(self, volume: float, member: discord.Member): + assert_admin(member) + if volume < 0.01: + raise Explicit('volume too small') + if volume > 1: + raise Explicit('volume too big') + self.volume = volume + await volume_db.set(member.guild.id, volume) + + @classmethod + async def create(cls, guild: discord.Guild) -> 'MainAudio': + return cls(await QueueAudio.create(guild), volume=volume_db.get(guild.id, 0.2)) diff --git a/v6d3music/queueaudio.py b/v6d3music/queueaudio.py new file mode 100644 index 0000000..f68af30 --- /dev/null +++ b/v6d3music/queueaudio.py @@ -0,0 +1,107 @@ +import asyncio +from collections import deque +from io import StringIO + +import discord +from ptvp35 import Db, KVJson + +from v6d3music.config import myroot +from v6d3music.utils.assert_admin import assert_admin +from v6d3music.utils.fill import FILL +from v6d3music.ytaudio import YTAudio + +queue_db = Db(myroot / 'queue.db', kvrequest_type=KVJson) + + +class QueueAudio(discord.AudioSource): + def __init__(self, guild: discord.Guild, respawned: list[YTAudio]): + self.queue: deque[YTAudio] = deque() + self.queue.extend(respawned) + self.guild = guild + + @staticmethod + async def respawned(guild: discord.Guild) -> list[YTAudio]: + respawned = [] + try: + for audio_respawn in queue_db.get(guild.id, []): + try: + respawned.append(await YTAudio.respawn(guild, audio_respawn)) + except Exception as e: + print('audio respawn failed', e) + raise + except Exception as e: + print('queue respawn failed', e) + return respawned + + @classmethod + async def create(cls, guild: discord.Guild): + return cls(guild, await QueueAudio.respawned(guild)) + + async def save(self): + hybernated = [] + for audio in list(self.queue): + await asyncio.sleep(0.01) + hybernated.append(audio.hybernate()) + queue_db.set_nowait(self.guild.id, hybernated) + + def append(self, audio: YTAudio): + self.queue.append(audio) + + def read(self) -> bytes: + if not self.queue: + return FILL + audio = self.queue[0] + frame = audio.read() + if len(frame) != discord.opus.Encoder.FRAME_SIZE: + self.queue.popleft().cleanup() + frame = FILL + return frame + + def skip_at(self, pos: int, member: discord.Member) -> bool: + if pos < len(self.queue): + audio = self.queue[pos] + if audio.can_be_skipped_by(member): + self.queue.remove(audio) + audio.cleanup() + return True + return False + + def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool: + if audio in self.queue: + if audio.can_be_skipped_by(member): + self.queue.remove(audio) + audio.cleanup() + return True + return False + + def clear(self, member: discord.Member) -> None: + assert_admin(member) + self.cleanup() + + def swap(self, member: discord.Member, a: int, b: int) -> None: + assert_admin(member) + if max(a, b) >= len(self.queue): + return + self.queue[a], self.queue[b] = self.queue[b], self.queue[a] + + def move(self, member: discord.Member, a: int, b: int) -> None: + assert_admin(member) + if max(a, b) >= len(self.queue): + return + audio = self.queue[a] + self.queue.remove(audio) + self.queue.insert(b, audio) + + async def format(self) -> str: + stream = StringIO() + for i, audio in enumerate(list(self.queue)): + stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n') + return stream.getvalue() + + def cleanup(self): + self.queue.clear() + for audio in self.queue: + try: + audio.cleanup() + except ValueError: + pass diff --git a/v6d3music/run-bot.py b/v6d3music/run-bot.py index 3e8e893..9680f23 100644 --- a/v6d3music/run-bot.py +++ b/v6d3music/run-bot.py @@ -1,34 +1,27 @@ import asyncio -import concurrent.futures -import json import os import shlex -import string import subprocess import time -from collections import deque -from io import StringIO -from typing import Any, AsyncIterable, Iterable, Optional, TypeAlias import discord -from ptvp35 import Db, KVJson from v6d1tokens.client import request_token -from v6d2ctx.context import Benchmark, Context, Explicit, Implicit, at, escape, monitor +from v6d2ctx.context import Benchmark, Context, Explicit, at, monitor from v6d2ctx.handle_content import handle_content from v6d2ctx.lock_for import lock_for from v6d2ctx.serve import serve -import v6d3music.extract -import v6d3music.ffmpegnormalaudio -from v6d3music.app import MusicAppFactory +from v6d3music.app import MusicAppFactory, session_db from v6d3music.cache_url import cache_db -from v6d3music.config import myroot, prefix -from v6d3music.real_url import real_url +from v6d3music.config import prefix +from v6d3music.mainaudio import MainAudio, volume_db +from v6d3music.queueaudio import QueueAudio, queue_db from v6d3music.utils.assert_admin import assert_admin -from v6d3music.utils.fill import FILL +from v6d3music.utils.catch import catch +from v6d3music.utils.effects_for_preset import effects_for_preset from v6d3music.utils.options_for_effects import options_for_effects -from v6d3music.utils.sparq import sparq -from v6d3music.ytaudio import YTAudio +from v6d3music.utils.presets import allowed_presets +from v6d3music.yt_audios import yt_audios loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -45,9 +38,6 @@ client = discord.Client( reactions=True ), ) -volume_db = Db(myroot / 'volume.db', kvrequest_type=KVJson) -queue_db = Db(myroot / 'queue.db', kvrequest_type=KVJson) -session_db = Db(myroot / 'session.db', kvrequest_type=KVJson) vcs_restored = False @@ -85,8 +75,9 @@ async def on_ready(): name='феноменально', ) ) - if not vcs_restored: - await restore_vcs() + async with lock_for('vcs_restored', '...'): + if not vcs_restored: + await restore_vcs() @at('commands', 'help') @@ -98,283 +89,9 @@ async def help_(ctx: Context, args: list[str]) -> None: await ctx.reply(f'help for {name}: `{name} help`') -class QueueAudio(discord.AudioSource): - def __init__(self, guild: discord.Guild, respawned: list[YTAudio]): - self.queue: deque[YTAudio] = deque() - self.queue.extend(respawned) - self.guild = guild - - @staticmethod - async def respawned(guild: discord.Guild) -> list[YTAudio]: - respawned = [] - try: - for audio_respawn in queue_db.get(guild.id, []): - try: - respawned.append(await YTAudio.respawn(guild, audio_respawn)) - except Exception as e: - print('audio respawn failed', e) - raise - except Exception as e: - print('queue respawn failed', e) - return respawned - - @classmethod - async def create(cls, guild: discord.Guild): - return cls(guild, await QueueAudio.respawned(guild)) - - async def save(self): - hybernated = [] - for audio in list(self.queue): - await asyncio.sleep(0.01) - hybernated.append(audio.hybernate()) - queue_db.set_nowait(self.guild.id, hybernated) - - def append(self, audio: YTAudio): - self.queue.append(audio) - - def read(self) -> bytes: - if not self.queue: - return FILL - audio = self.queue[0] - frame = audio.read() - if len(frame) != discord.opus.Encoder.FRAME_SIZE: - self.queue.popleft().cleanup() - frame = FILL - return frame - - def skip_at(self, pos: int, member: discord.Member) -> bool: - if pos < len(self.queue): - audio = self.queue[pos] - if audio.can_be_skipped_by(member): - self.queue.remove(audio) - audio.cleanup() - return True - return False - - def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool: - if audio in self.queue: - if audio.can_be_skipped_by(member): - self.queue.remove(audio) - audio.cleanup() - return True - return False - - def clear(self, member: discord.Member) -> None: - assert_admin(member) - self.cleanup() - - def swap(self, member: discord.Member, a: int, b: int) -> None: - assert_admin(member) - if max(a, b) >= len(self.queue): - return - self.queue[a], self.queue[b] = self.queue[b], self.queue[a] - - def move(self, member: discord.Member, a: int, b: int) -> None: - assert_admin(member) - if max(a, b) >= len(self.queue): - return - audio = self.queue[a] - self.queue.remove(audio) - self.queue.insert(b, audio) - - async def format(self) -> str: - stream = StringIO() - for i, audio in enumerate(list(self.queue)): - stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n') - return stream.getvalue() - - def cleanup(self): - self.queue.clear() - for audio in self.queue: - try: - audio.cleanup() - except ValueError: - pass - - -class MainAudio(discord.PCMVolumeTransformer): - def __init__(self, queue: QueueAudio, volume: float): - self.queue = queue - super().__init__(self.queue, volume=volume) - - async def set(self, volume: float, member: discord.Member): - assert_admin(member) - if volume < 0.01: - raise Explicit('volume too small') - if volume > 1: - raise Explicit('volume too big') - self.volume = volume - await volume_db.set(member.guild.id, volume) - - -async def aextract(params: dict, url: str, **kwargs): - with Benchmark('AEX'): - with concurrent.futures.ProcessPoolExecutor() as pool: - return await loop.run_in_executor( - pool, - v6d3music.extract.extract, - params, - url, - kwargs - ) - - -async def tor_extract(params: dict, url: str, **kwargs): - print(f'tor extracting {url}') - p = subprocess.Popen( - ['torify', 'python', '-m', 'v6d3music.run-extract'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - text=True - ) - p.stdin.write(f'{json.dumps(params)}\n') - p.stdin.write(f'{json.dumps(url)}\n') - p.stdin.write(f'{json.dumps(kwargs)}\n') - p.stdin.flush() - p.stdin.close() - code = await loop.run_in_executor(None, p.wait) - if code: - raise RuntimeError(code) - return json.loads(p.stdout.read()) - - -async def create_ytaudio( - ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int, tor: bool -) -> YTAudio: - if effects: - if effects not in allowed_effects: - assert_admin(ctx.member) - if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'): - raise Explicit('malformed effects') - options = options_for_effects(effects) - else: - options = None - return YTAudio( - await real_url(info['url'], False, tor), - info['url'], - f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}', - options, - ctx.member, - already_read, - tor - ) - - -async def entries_for_url(url: str, tor: bool) -> AsyncIterable[ - dict[str, Any] -]: - ef = aextract - if tor: - ef = tor_extract - info = await ef( - { - 'playlistend': 128, - 'logtostderr': True - }, - url, - download=False, - process=False - ) - if '__error__' in info: - raise Explicit('extraction error\n' + info.get('__error_str__')) - if 'entries' in info: - for entry in info['entries']: - yield entry - else: - yield info | {'url': url} - - -info_tuple: TypeAlias = tuple[dict[str, Any], str, int, bool] - - -async def create_ytaudios(ctx: Context, infos: list[info_tuple]) -> AsyncIterable[YTAudio]: - for audio in await asyncio.gather( - *[ - create_ytaudio(ctx, info, effects, already_read, tor) - for - info, effects, already_read, tor - in - infos - ] - ): - yield audio - - -presets: dict[str, str] = { - 'cursed': 'aeval=val(0)*2*sin(440*t)+val(1)*2*cos(622*t)|val(1)*2*sin(622*t)+val(0)*2*cos(440*t)', - 'bassboost': 'bass=g=10', - 'bassbooboost': 'bass=g=30', - 'nightcore': 'asetrate=67882', - 'daycore': 'atempo=.9,aecho=1.0:0.5:20:0.5', - 'пришествие анимешне': 'bass=g=15,asetrate=67882,bass=g=15', - 'difference': 'aeval=val(0)-val(1)|val(1)-val(0)', - 'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)', -} -allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'daycore', 'mono'] -allowed_effects = {'', *(presets[key] for key in allowed_presets)} - - -def effects_for_preset(preset: str) -> str: - if preset in presets: - return presets[preset] - else: - raise Explicit('unknown preset') - - -async def entries_effects_for_args(args: list[str]) -> AsyncIterable[info_tuple]: - while args: - match args: - case [url, '-', effects, *args]: - pass - case [url, '+', preset, *args]: - effects = effects_for_preset(preset) - case [url, *args]: - effects = None - case _: - raise RuntimeError - seconds = 0 - match args: - case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal(): - seconds = 3600 * int(h) + 60 * int(m) + int(s) - case [m, s, *args] if m.isdecimal() and s.isdecimal(): - seconds = 60 * int(m) + int(s) - case [s, *args] if s.isdecimal(): - seconds = int(s) - case [*args]: - pass - already_read = round(seconds / sparq(options_for_effects(effects))) - tor = False - match args: - case ['tor', *args]: - tor = True - case [*args]: - pass - async for info in entries_for_url(url, tor): - yield info, effects, already_read, tor - - -async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]: - tuples: list[info_tuple] = [] - async for info, effects, already_read, tor in entries_effects_for_args(args): - tuples.append((info, effects, already_read, tor)) - if len(tuples) >= 5: - async for audio in create_ytaudios(ctx, tuples): - yield audio - tuples.clear() - async for audio in create_ytaudios(ctx, tuples): - yield audio - - mainasrcs: dict[discord.Guild, MainAudio] = {} -async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str)): - catched = {(case,) if isinstance(case, str) else tuple(case) for case in catched} - if tuple(args) in catched: - await ctx.reply(reply.strip()) - raise Implicit - - @at('commands', '/') @at('commands', 'play') async def play(ctx: Context, args: list[str]) -> None: @@ -422,7 +139,7 @@ async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio if create: source = mainasrcs.setdefault( vc.guild, - MainAudio(await QueueAudio.create(vc.guild), volume=volume_db.get(vc.guild.id, 0.2)) + await MainAudio.create(vc.guild) ) else: raise Explicit('not playing') @@ -644,7 +361,7 @@ async def save_job(): async def start_app(): - await MusicAppFactory.start(session_db, client) + await MusicAppFactory.start(client) async def setup_tasks(): diff --git a/v6d3music/run-extract.py b/v6d3music/run-extract.py index f67bff3..ec925a4 100644 --- a/v6d3music/run-extract.py +++ b/v6d3music/run-extract.py @@ -1,8 +1,8 @@ import json -import v6d3music.extract +import v6d3music.utils.extract params = json.loads(input()) url = json.loads(input()) kwargs = json.loads(input()) -print(json.dumps(v6d3music.extract.extract(params, url, kwargs))) +print(json.dumps(v6d3music.utils.extract.extract(params, url, kwargs))) diff --git a/v6d3music/utils/aextract.py b/v6d3music/utils/aextract.py new file mode 100644 index 0000000..a6fa580 --- /dev/null +++ b/v6d3music/utils/aextract.py @@ -0,0 +1,18 @@ +import asyncio +from concurrent.futures import ProcessPoolExecutor + +from v6d2ctx.context import Benchmark + +from v6d3music.utils.extract import extract + + +async def aextract(params: dict, url: str, **kwargs): + with Benchmark('AEX'): + with ProcessPoolExecutor() as pool: + return await asyncio.get_running_loop().run_in_executor( + pool, + extract, + params, + url, + kwargs + ) diff --git a/v6d3music/utils/catch.py b/v6d3music/utils/catch.py new file mode 100644 index 0000000..7589047 --- /dev/null +++ b/v6d3music/utils/catch.py @@ -0,0 +1,10 @@ +from typing import Iterable + +from v6d2ctx.context import Context, Implicit + + +async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str)): + catched = {(case,) if isinstance(case, str) else tuple(case) for case in catched} + if tuple(args) in catched: + await ctx.reply(reply.strip()) + raise Implicit diff --git a/v6d3music/utils/effects_for_preset.py b/v6d3music/utils/effects_for_preset.py new file mode 100644 index 0000000..9eaea8f --- /dev/null +++ b/v6d3music/utils/effects_for_preset.py @@ -0,0 +1,10 @@ +from v6d2ctx.context import Explicit + +from v6d3music.utils.presets import presets + + +def effects_for_preset(preset: str) -> str: + if preset in presets: + return presets[preset] + else: + raise Explicit('unknown preset') diff --git a/v6d3music/utils/entries_effects_for_args.py b/v6d3music/utils/entries_effects_for_args.py new file mode 100644 index 0000000..f943ed1 --- /dev/null +++ b/v6d3music/utils/entries_effects_for_args.py @@ -0,0 +1,39 @@ +from typing import AsyncIterable + +from v6d3music.utils.effects_for_preset import effects_for_preset +from v6d3music.utils.entries_for_url import entries_for_url +from v6d3music.utils.info_tuple import info_tuple +from v6d3music.utils.options_for_effects import options_for_effects +from v6d3music.utils.sparq import sparq + + +async def entries_effects_for_args(args: list[str]) -> AsyncIterable[info_tuple]: + while args: + match args: + case [url, '-', effects, *args]: + pass + case [url, '+', preset, *args]: + effects = effects_for_preset(preset) + case [url, *args]: + effects = None + case _: + raise RuntimeError + seconds = 0 + match args: + case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal(): + seconds = 3600 * int(h) + 60 * int(m) + int(s) + case [m, s, *args] if m.isdecimal() and s.isdecimal(): + seconds = 60 * int(m) + int(s) + case [s, *args] if s.isdecimal(): + seconds = int(s) + case [*args]: + pass + already_read = round(seconds / sparq(options_for_effects(effects))) + tor = False + match args: + case ['tor', *args]: + tor = True + case [*args]: + pass + async for info in entries_for_url(url, tor): + yield info, effects, already_read, tor diff --git a/v6d3music/utils/entries_for_url.py b/v6d3music/utils/entries_for_url.py new file mode 100644 index 0000000..10770a1 --- /dev/null +++ b/v6d3music/utils/entries_for_url.py @@ -0,0 +1,30 @@ +from typing import Any, AsyncIterable + +from v6d2ctx.context import Explicit + +from v6d3music.utils.aextract import aextract +from v6d3music.utils.tor_extract import tor_extract + + +async def entries_for_url(url: str, tor: bool) -> AsyncIterable[ + dict[str, Any] +]: + ef = aextract + if tor: + ef = tor_extract + info = await ef( + { + 'playlistend': 128, + 'logtostderr': True + }, + url, + download=False, + process=False + ) + if '__error__' in info: + raise Explicit('extraction error\n' + info.get('__error_str__')) + if 'entries' in info: + for entry in info['entries']: + yield entry + else: + yield info | {'url': url} diff --git a/v6d3music/extract.py b/v6d3music/utils/extract.py similarity index 89% rename from v6d3music/extract.py rename to v6d3music/utils/extract.py index 0c0a4ef..c76ab08 100644 --- a/v6d3music/extract.py +++ b/v6d3music/utils/extract.py @@ -1,10 +1,6 @@ -from collections import namedtuple - import discord.utils import youtube_dl -eerror = namedtuple('eerror', ['content']) - def extract(params: dict, url: str, kwargs: dict): try: diff --git a/v6d3music/utils/info_tuple.py b/v6d3music/utils/info_tuple.py new file mode 100644 index 0000000..07db908 --- /dev/null +++ b/v6d3music/utils/info_tuple.py @@ -0,0 +1,3 @@ +from typing import Any, TypeAlias + +info_tuple: TypeAlias = tuple[dict[str, Any], str, int, bool] diff --git a/v6d3music/utils/presets.py b/v6d3music/utils/presets.py new file mode 100644 index 0000000..711c304 --- /dev/null +++ b/v6d3music/utils/presets.py @@ -0,0 +1,12 @@ +presets: dict[str, str] = { + 'cursed': 'aeval=val(0)*2*sin(440*t)+val(1)*2*cos(622*t)|val(1)*2*sin(622*t)+val(0)*2*cos(440*t)', + 'bassboost': 'bass=g=10', + 'bassbooboost': 'bass=g=30', + 'nightcore': 'asetrate=67882', + 'daycore': 'atempo=.9,aecho=1.0:0.5:20:0.5', + 'пришествие анимешне': 'bass=g=15,asetrate=67882,bass=g=15', + 'difference': 'aeval=val(0)-val(1)|val(1)-val(0)', + 'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)', +} +allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'daycore', 'mono'] +allowed_effects = {'', *(presets[key] for key in allowed_presets)} diff --git a/v6d3music/utils/tor_extract.py b/v6d3music/utils/tor_extract.py new file mode 100644 index 0000000..7071057 --- /dev/null +++ b/v6d3music/utils/tor_extract.py @@ -0,0 +1,22 @@ +import asyncio +import json +import subprocess + + +async def tor_extract(params: dict, url: str, **kwargs): + print(f'tor extracting {url}') + p = subprocess.Popen( + ['torify', 'python', '-m', 'v6d3music.run-extract'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True + ) + p.stdin.write(f'{json.dumps(params)}\n') + p.stdin.write(f'{json.dumps(url)}\n') + p.stdin.write(f'{json.dumps(kwargs)}\n') + p.stdin.flush() + p.stdin.close() + code = await asyncio.get_running_loop().run_in_executor(None, p.wait) + if code: + raise RuntimeError(code) + return json.loads(p.stdout.read()) diff --git a/v6d3music/yt_audios.py b/v6d3music/yt_audios.py new file mode 100644 index 0000000..0b481bf --- /dev/null +++ b/v6d3music/yt_audios.py @@ -0,0 +1,20 @@ +from typing import AsyncIterable + +from v6d2ctx.context import Context + +from v6d3music.create_ytaudios import create_ytaudios +from v6d3music.utils.entries_effects_for_args import entries_effects_for_args +from v6d3music.utils.info_tuple import info_tuple +from v6d3music.ytaudio import YTAudio + + +async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]: + tuples: list[info_tuple] = [] + async for info, effects, already_read, tor in entries_effects_for_args(args): + tuples.append((info, effects, already_read, tor)) + if len(tuples) >= 5: + async for audio in create_ytaudios(ctx, tuples): + yield audio + tuples.clear() + async for audio in create_ytaudios(ctx, tuples): + yield audio