help catch + effects command + time fix + allowed presets

This commit is contained in:
AF 2022-03-29 19:13:36 +03:00
parent dbce057615
commit dc86f70c40

View File

@ -9,7 +9,7 @@ import subprocess
import time import time
from collections import deque from collections import deque
from io import StringIO from io import StringIO
from typing import Optional, AsyncIterable, Any from typing import Optional, AsyncIterable, Any, Iterable
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import discord import discord
@ -17,7 +17,7 @@ import nacl.hash
import youtube_dl import youtube_dl
from ptvp35 import Db, KVJson from ptvp35 import Db, KVJson
from v6d1tokens.client import request_token from v6d1tokens.client import request_token
from v6d2ctx.context import Context, at, escape, monitor, Benchmark, Explicit from v6d2ctx.context import Context, at, escape, monitor, Benchmark, Explicit, Implicit
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
@ -75,7 +75,7 @@ async def restore_vcs():
@client.event @client.event
async def on_ready(): async def on_ready():
print("ready") print('ready')
await client.change_presence(activity=discord.Game( await client.change_presence(activity=discord.Game(
name='феноменально', name='феноменально',
)) ))
@ -92,6 +92,31 @@ 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`')
def speed_quotient(options: str) -> float:
options = options or ''
options = ''.join(c for c in options if not c.isspace())
options += ','
quotient: float = 1.0
asetrate: str
for asetrate in re.findall(r'asetrate=([0-9.]+?),', options):
try:
quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE
except ValueError:
pass
atempo: str
for atempo in re.findall(r'atempo=([0-9.]+?),', options):
try:
quotient *= float(atempo)
except ValueError:
pass
quotient = max(0.1, min(10.0, quotient))
return quotient
def sparq(options: str) -> float:
return speed_quotient(options) * discord.opus.Encoder.FRAME_LENGTH / 1000
class YTAudio(discord.AudioSource): class YTAudio(discord.AudioSource):
source: discord.FFmpegPCMAudio source: discord.FFmpegPCMAudio
@ -127,30 +152,11 @@ class YTAudio(discord.AudioSource):
self.already_read = already_read self.already_read = already_read
self.set_source() self.set_source()
def _speed_quotient(self) -> float: def set_seconds(self, seconds: float):
options = self.options or '' self.set_already_read(round(seconds / sparq(self.options)))
options = ''.join(c for c in options if not c.isspace())
options += ','
quotient: float = 1.0
asetrate: str
for asetrate in re.findall(r'asetrate=([0-9.]+?),', options):
try:
quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE
except ValueError:
pass
atempo: str
for atempo in re.findall(r'atempo=([0-9.]+?),', options):
try:
quotient *= float(atempo)
except ValueError:
pass
return quotient
def speed_quotient(self) -> float:
return max(0.5, min(2.0, self._speed_quotient()))
def source_seconds(self) -> float: def source_seconds(self) -> float:
return self.already_read * self.speed_quotient() * discord.opus.Encoder.FRAME_LENGTH / 1000 return self.already_read * sparq(self.options)
def source_timecode(self) -> str: def source_timecode(self) -> str:
seconds = round(self.source_seconds()) seconds = round(self.source_seconds())
@ -266,6 +272,12 @@ class YTAudio(discord.AudioSource):
FILL = b'\x00' * discord.opus.Encoder.FRAME_SIZE FILL = b'\x00' * discord.opus.Encoder.FRAME_SIZE
def assert_admin(member: discord.Member):
permissions: discord.Permissions = member.guild_permissions
if not permissions.administrator:
raise Explicit('not an administrator')
class QueueAudio(discord.AudioSource): class QueueAudio(discord.AudioSource):
def __init__(self, guild: discord.Guild, respawned: list[YTAudio]): def __init__(self, guild: discord.Guild, respawned: list[YTAudio]):
self.queue: deque[YTAudio] = deque() self.queue: deque[YTAudio] = deque()
@ -328,31 +340,22 @@ class QueueAudio(discord.AudioSource):
return False return False
def clear(self, member: discord.Member) -> None: def clear(self, member: discord.Member) -> None:
permissions: discord.Permissions = member.guild_permissions assert_admin(member)
if permissions.administrator: self.cleanup()
self.cleanup()
else:
raise Explicit('not an administrator')
def swap(self, member: discord.Member, a: int, b: int) -> None: def swap(self, member: discord.Member, a: int, b: int) -> None:
permissions: discord.Permissions = member.guild_permissions assert_admin(member)
if permissions.administrator: if max(a, b) >= len(self.queue):
if max(a, b) >= len(self.queue): return
return self.queue[a], self.queue[b] = self.queue[b], self.queue[a]
self.queue[a], self.queue[b] = self.queue[b], self.queue[a]
else:
raise Explicit('not an administrator')
def move(self, member: discord.Member, a: int, b: int) -> None: def move(self, member: discord.Member, a: int, b: int) -> None:
permissions: discord.Permissions = member.guild_permissions assert_admin(member)
if permissions.administrator: if max(a, b) >= len(self.queue):
if max(a, b) >= len(self.queue): return
return audio = self.queue[a]
audio = self.queue[a] self.queue.remove(audio)
self.queue.remove(audio) self.queue.insert(b, audio)
self.queue.insert(b, audio)
else:
raise Explicit('not an administrator')
async def format(self) -> str: async def format(self) -> str:
stream = StringIO() stream = StringIO()
@ -375,13 +378,11 @@ class MainAudio(discord.PCMVolumeTransformer):
super().__init__(self.queue, volume=volume) super().__init__(self.queue, volume=volume)
async def set(self, volume: float, member: discord.Member): async def set(self, volume: float, member: discord.Member):
assert_admin(member)
if volume < 0.01: if volume < 0.01:
raise Explicit('volume too small') raise Explicit('volume too small')
if volume > 1: if volume > 1:
raise Explicit('volume too big') raise Explicit('volume too big')
permissions: discord.Permissions = member.guild_permissions
if not permissions.administrator:
raise Explicit('not an administrator')
self.volume = volume self.volume = volume
await volume_db.set(member.guild.id, volume) await volume_db.set(member.guild.id, volume)
@ -469,14 +470,17 @@ async def real_url(url: str, override: bool) -> str:
return rurl return rurl
def options_for_effects(effects: str) -> Optional[str]:
return f'-af {shlex.quote(effects)}' if effects else None
async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int) -> YTAudio: async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int) -> YTAudio:
if effects: if effects:
permissions: discord.Permissions = ctx.member.guild_permissions if effects not in allowed_effects:
if not permissions.administrator: assert_admin(ctx.member)
raise Explicit("not an administrator")
if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'): if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'):
raise Explicit('malformed effects') raise Explicit('malformed effects')
options = f'-af {shlex.quote(effects)}' options = options_for_effects(effects)
else: else:
options = None options = None
return YTAudio( return YTAudio(
@ -494,8 +498,8 @@ async def entries_for_url(url: str) -> AsyncIterable[
]: ]:
info = await aextract( info = await aextract(
{ {
"playlistend": 128, 'playlistend': 128,
"logtostderr": True 'logtostderr': True
}, },
url, url,
download=False, download=False,
@ -530,6 +534,8 @@ presets: dict[str, str] = {
'difference': 'aeval=val(0)-val(1)|val(1)-val(0)', 'difference': 'aeval=val(0)-val(1)|val(1)-val(0)',
'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)', 'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)',
} }
allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'mono']
allowed_effects = {'', *(presets[key] for key in allowed_presets)}
async def entries_effects_for_args(args: list[str]) -> AsyncIterable[tuple[dict[str, Any], str, int]]: async def entries_effects_for_args(args: list[str]) -> AsyncIterable[tuple[dict[str, Any], str, int]]:
@ -543,17 +549,17 @@ async def entries_effects_for_args(args: list[str]) -> AsyncIterable[tuple[dict[
effects = None effects = None
case _: case _:
raise RuntimeError raise RuntimeError
timestamp = 0 seconds = 0
match args: match args:
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal(): case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
timestamp = 3600 * int(h) + 60 * int(m) + int(s) seconds = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s, *args] if m.isdecimal() and s.isdecimal(): case [m, s, *args] if m.isdecimal() and s.isdecimal():
timestamp = 60 * int(m) + int(s) seconds = 60 * int(m) + int(s)
case [s, *args] if s.isdecimal(): case [s, *args] if s.isdecimal():
timestamp = int(s) seconds = int(s)
case [*args]: case [*args]:
pass pass
already_read = timestamp * 1000 / discord.opus.Encoder.FRAME_LENGTH already_read = round(seconds / sparq(options_for_effects(effects)))
async for info in entries_for_url(url): async for info in entries_for_url(url):
yield info, effects, already_read yield info, effects, already_read
@ -573,39 +579,49 @@ async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
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:
match args: await catch(
case [] | ['help']: ctx, args,
await ctx.reply(''' f'''
`play ...args` `play ...args`
`play url [- effects] ...args` `play url [- effects] ...args`
'''.strip()) `play url [+ preset] ...args`
case _: presets: {shlex.join(allowed_presets)}
async with lock_for(ctx.guild, 'not in a guild'): ''',
queue = await queue_for(ctx, create=True) (), 'help'
async for audio in yt_audios(ctx, args): )
queue.append(audio) async with lock_for(ctx.guild, 'not in a guild'):
await ctx.reply('done') queue = await queue_for(ctx, create=True)
async for audio in yt_audios(ctx, args):
queue.append(audio)
await ctx.reply('done')
async def raw_vc_for(ctx: Context) -> discord.VoiceClient: async def raw_vc_for(ctx: Context) -> discord.VoiceClient:
if ctx.guild is None: if ctx.guild is None:
raise Explicit("not in a guild") raise Explicit('not in a guild')
vc: discord.VoiceProtocol = ctx.guild.voice_client vc: discord.VoiceProtocol = ctx.guild.voice_client
if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected(): if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState = ctx.member.voice vs: discord.VoiceState = ctx.member.voice
if vs is None: if vs is None:
raise Explicit("not connected") raise Explicit('not connected')
vch: discord.VoiceChannel = vs.channel vch: discord.VoiceChannel = vs.channel
if vch is None: if vch is None:
raise Explicit("not connected") raise Explicit('not connected')
try: try:
vc: discord.VoiceProtocol = await vch.connect() vc: discord.VoiceProtocol = await vch.connect()
except discord.ClientException: except discord.ClientException:
await ctx.guild.fetch_channels() await ctx.guild.fetch_channels()
raise Explicit("try again later") raise Explicit('try again later')
assert isinstance(vc, discord.VoiceClient) assert isinstance(vc, discord.VoiceClient)
return vc return vc
@ -620,7 +636,7 @@ async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio
MainAudio(await QueueAudio.create(vc.guild), volume=volume_db.get(vc.guild.id, 0.2)) MainAudio(await QueueAudio.create(vc.guild), volume=volume_db.get(vc.guild.id, 0.2))
) )
else: else:
raise Explicit("not playing") raise Explicit('not playing')
if vc.source != source or create and not vc.is_playing(): if vc.source != source or create and not vc.is_playing():
vc.play(source) vc.play(source)
return source return source
@ -647,12 +663,10 @@ async def queue_for(ctx: Context, *, create: bool) -> QueueAudio:
@at('commands', 'skip') @at('commands', 'skip')
async def skip(ctx: Context, args: list[str]) -> None: async def skip(ctx: Context, args: list[str]) -> None:
match args: await catch(ctx, args, '''
case ['help']:
await ctx.reply('''
`skip [first] [last]` `skip [first] [last]`
'''.strip()) ''', 'help')
return match args:
case []: case []:
queue = await queue_for(ctx, create=False) queue = await queue_for(ctx, create=False)
queue.skip_at(0, ctx.member) queue.skip_at(0, ctx.member)
@ -667,34 +681,58 @@ async def skip(ctx: Context, args: list[str]) -> None:
if not queue.skip_at(pos0, ctx.member): if not queue.skip_at(pos0, ctx.member):
pos0 += 1 pos0 += 1
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
await ctx.reply('done') await ctx.reply('done')
@at('commands', 'to') @at('commands', 'to')
async def skip_to(ctx: Context, args: list[str]) -> None: async def skip_to(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`to [[h]] [m] s`
''', 'help')
match args: match args:
case ['help']:
await ctx.reply('`to [[h]] [m] s`')
return
case [h, m, s] if h.isdecimal() and m.isdecimal() and s.isdecimal(): case [h, m, s] if h.isdecimal() and m.isdecimal() and s.isdecimal():
timestamp = 3600 * int(h) + 60 * int(m) + int(s) seconds = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s] if m.isdecimal() and s.isdecimal(): case [m, s] if m.isdecimal() and s.isdecimal():
timestamp = 60 * int(m) + int(s) seconds = 60 * int(m) + int(s)
case [s] if s.isdecimal(): case [s] if s.isdecimal():
timestamp = int(s) seconds = int(s)
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
already_read = timestamp * 1000 / discord.opus.Encoder.FRAME_LENGTH
queue = await queue_for(ctx, create=False) queue = await queue_for(ctx, create=False)
queue.queue[0].set_already_read(already_read) queue.queue[0].set_seconds(seconds)
@at('commands', 'effects')
async def effects_(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`effects - effects`
`effects + preset`
''', 'help')
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = presets[preset]
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await queue_for(ctx, create=False)
yta = queue.queue[0]
seconds = yta.source_seconds()
yta.options = options_for_effects(effects)
yta.set_seconds(seconds)
@at('commands', 'queue') @at('commands', 'queue')
async def queue_(ctx: Context, args: list[str]) -> None: async def queue_(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`queue`
`queue clear`
`queue resume`
`queue pause`
''', 'help')
match args: match args:
case ['help']:
await ctx.reply('current queue')
case []: case []:
await ctx.long((await (await queue_for(ctx, create=False)).format()).strip() or 'no queue') await ctx.long((await (await queue_for(ctx, create=False)).format()).strip() or 'no queue')
case ['clear']: case ['clear']:
@ -710,43 +748,46 @@ async def queue_(ctx: Context, args: list[str]) -> None:
vc.pause() vc.pause()
await ctx.reply('done') await ctx.reply('done')
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
@at('commands', 'swap') @at('commands', 'swap')
async def swap(ctx: Context, args: list[str]) -> None: async def swap(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`swap a b`
''', 'help')
match args: match args:
case ['help']:
await ctx.reply('`swap a b`')
case [a, b] if a.isdecimal() and b.isdecimal(): case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b) a, b = int(a), int(b)
(await queue_for(ctx, create=False)).swap(ctx.member, a, b) (await queue_for(ctx, create=False)).swap(ctx.member, a, b)
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
@at('commands', 'move') @at('commands', 'move')
async def move(ctx: Context, args: list[str]) -> None: async def move(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`move a b`
''', 'help')
match args: match args:
case ['help']:
await ctx.reply('`move a b`')
case [a, b] if a.isdecimal() and b.isdecimal(): case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b) a, b = int(a), int(b)
(await queue_for(ctx, create=False)).move(ctx.member, a, b) (await queue_for(ctx, create=False)).move(ctx.member, a, b)
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
@at('commands', 'volume') @at('commands', 'volume')
async def volume_(ctx: Context, args: list[str]) -> None: async def volume_(ctx: Context, args: list[str]) -> None:
await catch(ctx, args, '''
`volume volume`
''', 'help')
match args: match args:
case ['help']:
await ctx.reply('`volume 0.2`')
case [volume]: case [volume]:
volume = float(volume) volume = float(volume)
await (await main_for(ctx, create=False)).set(volume, ctx.member) await (await main_for(ctx, create=False)).set(volume, ctx.member)
case _: case _:
raise Explicit("misformatted") raise Explicit('misformatted')
@at('commands', 'pause') @at('commands', 'pause')