This commit is contained in:
AF 2022-06-19 20:41:11 +03:00
parent aad21d7135
commit b9bfea0257
21 changed files with 426 additions and 328 deletions

View File

@ -5,13 +5,16 @@ from pathlib import Path
import aiohttp import aiohttp
import discord import discord
from aiohttp import web from aiohttp import web
from ptvp35 import Db from ptvp35 import Db, KVJson
from v6d0auth.appfactory import AppFactory from v6d0auth.appfactory import AppFactory
from v6d0auth.run_app import start_app from v6d0auth.run_app import start_app
from v6d1tokens.client import request_token from v6d1tokens.client import request_token
from v6d3music.config import myroot
from v6d3music.utils.bytes_hash import bytes_hash from v6d3music.utils.bytes_hash import bytes_hash
session_db = Db(myroot / 'session.db', kvrequest_type=KVJson)
class MusicAppFactory(AppFactory): class MusicAppFactory(AppFactory):
htmlroot = Path(__file__).parent / 'html' htmlroot = Path(__file__).parent / 'html'
@ -19,7 +22,6 @@ class MusicAppFactory(AppFactory):
def __init__( def __init__(
self, self,
secret: str, secret: str,
db: Db,
client: discord.Client client: discord.Client
): ):
self.secret = secret self.secret = secret
@ -27,7 +29,6 @@ class MusicAppFactory(AppFactory):
self.discord_auth = 'https://discord.com/api/oauth2/authorize?client_id=914432576926646322' \ 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' f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.db = db
self.client = client self.client = client
def _file(self, file: str): def _file(self, file: str):
@ -106,7 +107,7 @@ class MusicAppFactory(AppFactory):
discriminator = cls.user_discriminator(user) discriminator = cls.user_discriminator(user)
if discriminator is None: if discriminator is None:
return None return None
return username + discriminator return f'{username}#{discriminator}'
@classmethod @classmethod
def user_username(cls, user: dict): def user_username(cls, user: dict):
@ -147,8 +148,9 @@ class MusicAppFactory(AppFactory):
'client': (None if sclient is None else self.client_status(sclient)) 'client': (None if sclient is None else self.client_status(sclient))
} }
def session_data(self, session: str) -> dict: @classmethod
data = self.db.get(session, {}) def session_data(cls, session: str) -> dict:
data = session_db.get(session, {})
if not isinstance(data, dict): if not isinstance(data, dict):
data = {} data = {}
return data return data
@ -177,7 +179,7 @@ class MusicAppFactory(AppFactory):
data = self.session_data(session) data = self.session_data(session)
data['code'] = code data['code'] = code
data['token'] = await self.code_token(code) data['token'] = await self.code_token(code)
await self.db.set(session, data) await session_db.set(session, data)
return response return response
else: else:
return await self.html_resp('auth') return await self.html_resp('auth')
@ -186,7 +188,7 @@ class MusicAppFactory(AppFactory):
async def state(request: web.Request) -> web.Response: async def state(request: web.Request) -> web.Response:
session = str(request.query.get('session')) session = str(request.query.get('session'))
return web.json_response( return web.json_response(
data=f"{bytes_hash(session.encode())}" data=f'{bytes_hash(session.encode())}'
) )
@routes.get('/status/') @routes.get('/status/')
@ -209,6 +211,6 @@ class MusicAppFactory(AppFactory):
) )
@classmethod @classmethod
async def start(cls, db: Db, client: discord.Client): async def start(cls, client: discord.Client):
factory = cls(await request_token('music-client', 'token'), db, client) factory = cls(await request_token('music-client', 'token'), client)
await start_app(factory.app()) await start_app(factory.app())

View File

@ -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
)

View File

@ -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

View File

@ -3,11 +3,6 @@
<script src="/main.js"></script> <script src="/main.js"></script>
<script> <script>
(async () => { (async () => {
const a = document.createElement('a'); root.append(await pageHome());
a.href = '/login/';
a.innerText = 'login';
root.append(a);
logEl(JSON.stringify(await sessionStatus(), undefined, 2));
root.append(await userAvatarImg());
})(); })();
</script> </script>

View File

@ -2,12 +2,12 @@
<div id="root"></div> <div id="root"></div>
<script src="/main.js"></script> <script src="/main.js"></script>
<script> <script>
authbase = '$$DISCORD_AUTH$$';
(async () => { (async () => {
const a = document.createElement('a'); const a = await aAuth();
a.href = "$$DISCORD_AUTH$$&state=" + await sessionState();
a.innerText = 'auth';
root.append(a); root.append(a);
logEl(sessionStr()); logEl(sessionStr());
logEl(await sessionState()); logEl(await sessionState());
a.click();
})(); })();
</script> </script>

View File

@ -1,4 +1,5 @@
html, body { html, body {
color: white; color: white;
background: black; background: black;
margin: 0;
} }

View File

@ -49,10 +49,45 @@ const userUsername = async () => {
return user && user['username']; return user && user['username'];
}; };
const userAvatarImg = async () => { const userAvatarImg = async () => {
const img = document.createElement('img'); const avatar = await userAvatarUrl();
img.src = await userAvatarUrl(); if (avatar) {
img.width = 128; const img = document.createElement('img');
img.height = 128; img.src = avatar;
img.alt = await userUsername(); img.width = 128;
return img; 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;
}; };

28
v6d3music/mainaudio.py Normal file
View File

@ -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))

107
v6d3music/queueaudio.py Normal file
View File

@ -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

View File

@ -1,34 +1,27 @@
import asyncio import asyncio
import concurrent.futures
import json
import os import os
import shlex import shlex
import string
import subprocess import subprocess
import time import time
from collections import deque
from io import StringIO
from typing import Any, AsyncIterable, Iterable, Optional, TypeAlias
import discord import discord
from ptvp35 import Db, KVJson
from v6d1tokens.client import request_token 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.handle_content import handle_content
from v6d2ctx.lock_for import lock_for from v6d2ctx.lock_for import lock_for
from v6d2ctx.serve import serve from v6d2ctx.serve import serve
import v6d3music.extract from v6d3music.app import MusicAppFactory, session_db
import v6d3music.ffmpegnormalaudio
from v6d3music.app import MusicAppFactory
from v6d3music.cache_url import cache_db from v6d3music.cache_url import cache_db
from v6d3music.config import myroot, prefix from v6d3music.config import prefix
from v6d3music.real_url import real_url 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.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.options_for_effects import options_for_effects
from v6d3music.utils.sparq import sparq from v6d3music.utils.presets import allowed_presets
from v6d3music.ytaudio import YTAudio from v6d3music.yt_audios import yt_audios
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
@ -45,9 +38,6 @@ client = discord.Client(
reactions=True 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 vcs_restored = False
@ -85,8 +75,9 @@ async def on_ready():
name='феноменально', name='феноменально',
) )
) )
if not vcs_restored: async with lock_for('vcs_restored', '...'):
await restore_vcs() if not vcs_restored:
await restore_vcs()
@at('commands', 'help') @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`') 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] = {} 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', '/')
@at('commands', 'play') @at('commands', 'play')
async def play(ctx: Context, args: list[str]) -> None: 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: if create:
source = mainasrcs.setdefault( source = mainasrcs.setdefault(
vc.guild, vc.guild,
MainAudio(await QueueAudio.create(vc.guild), volume=volume_db.get(vc.guild.id, 0.2)) await MainAudio.create(vc.guild)
) )
else: else:
raise Explicit('not playing') raise Explicit('not playing')
@ -644,7 +361,7 @@ async def save_job():
async def start_app(): async def start_app():
await MusicAppFactory.start(session_db, client) await MusicAppFactory.start(client)
async def setup_tasks(): async def setup_tasks():

View File

@ -1,8 +1,8 @@
import json import json
import v6d3music.extract import v6d3music.utils.extract
params = json.loads(input()) params = json.loads(input())
url = json.loads(input()) url = json.loads(input())
kwargs = 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)))

View File

@ -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
)

10
v6d3music/utils/catch.py Normal file
View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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}

View File

@ -1,10 +1,6 @@
from collections import namedtuple
import discord.utils import discord.utils
import youtube_dl import youtube_dl
eerror = namedtuple('eerror', ['content'])
def extract(params: dict, url: str, kwargs: dict): def extract(params: dict, url: str, kwargs: dict):
try: try:

View File

@ -0,0 +1,3 @@
from typing import Any, TypeAlias
info_tuple: TypeAlias = tuple[dict[str, Any], str, int, bool]

View File

@ -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)}

View File

@ -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())

20
v6d3music/yt_audios.py Normal file
View File

@ -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