timestamps

This commit is contained in:
AF 2022-02-26 00:59:17 +03:00
parent 5d2c80e1b3
commit dbce057615

View File

@ -2,6 +2,7 @@ import asyncio
import concurrent.futures
import os
import random
import re
import shlex
import string
import subprocess
@ -112,14 +113,78 @@ class YTAudio(discord.AudioSource):
self.loaded = False
self.regenerating = False
self.set_source()
self._durations: dict[str, str] = {}
def set_source(self):
self.schedule_duration_update()
self.source = discord.FFmpegPCMAudio(
self.url,
options=self.options,
before_options=self.before_options()
)
def set_already_read(self, already_read: int):
self.already_read = already_read
self.set_source()
def _speed_quotient(self) -> float:
options = self.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
return quotient
def speed_quotient(self) -> float:
return max(0.5, min(2.0, self._speed_quotient()))
def source_seconds(self) -> float:
return self.already_read * self.speed_quotient() * discord.opus.Encoder.FRAME_LENGTH / 1000
def source_timecode(self) -> str:
seconds = round(self.source_seconds())
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return f'{hours}:{minutes:02d}:{seconds:02d}'
def schedule_duration_update(self):
asyncio.get_running_loop().create_task(self.update_duration())
async def update_duration(self):
url: str = self.url
if url in self._durations:
return
self._durations.setdefault(url, '')
p = subprocess.Popen(
f'ffprobe -i {shlex.quote(url)}'
' -show_entries format=duration -v quiet -of csv="p=0" -sexagesimal',
stdout=subprocess.PIPE,
shell=True
)
with Benchmark('FFP'):
code = await loop.run_in_executor(None, p.wait)
if code:
pass
else:
self._durations[url] = p.stdout.read().decode().strip().split('.')[0]
def duration(self) -> str:
duration = self._durations.get(self.url)
if duration is None:
self.schedule_duration_update()
return duration or '?:??:??'
def before_options(self):
before_options = ''
if 'https' in self.url:
@ -127,7 +192,9 @@ class YTAudio(discord.AudioSource):
'-reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 10 -copy_unknown'
)
if self.already_read:
before_options += f' -ss {self.already_read * discord.opus.Encoder.FRAME_LENGTH / 1000}'
before_options += (
f' -ss {self.source_seconds()}'
)
return before_options
def read(self) -> bytes:
@ -287,10 +354,10 @@ class QueueAudio(discord.AudioSource):
else:
raise Explicit('not an administrator')
def format(self) -> str:
async def format(self) -> str:
stream = StringIO()
for i, audio in enumerate(list(self.queue)):
stream.write(f'`[{i}]` {audio.description}\n')
stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n')
return stream.getvalue()
def cleanup(self):
@ -415,7 +482,7 @@ async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[s
return YTAudio(
await real_url(info['url'], False),
info['url'],
f'{escape(info.get("title"))} `Rby` {ctx.member}',
f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}',
options,
ctx.member,
already_read
@ -506,6 +573,7 @@ async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
mainasrcs: dict[discord.Guild, MainAudio] = {}
@at('commands', '/')
@at('commands', 'play')
async def play(ctx: Context, args: list[str]) -> None:
match args:
@ -553,7 +621,7 @@ async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio
)
else:
raise Explicit("not playing")
if vc.source != source or not vc.is_playing():
if vc.source != source or create and not vc.is_playing():
vc.play(source)
return source
@ -603,19 +671,44 @@ async def skip(ctx: Context, args: list[str]) -> None:
await ctx.reply('done')
@at('commands', 'to')
async def skip_to(ctx: Context, args: list[str]) -> None:
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():
timestamp = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s] if m.isdecimal() and s.isdecimal():
timestamp = 60 * int(m) + int(s)
case [s] if s.isdecimal():
timestamp = int(s)
case _:
raise Explicit("misformatted")
already_read = timestamp * 1000 / discord.opus.Encoder.FRAME_LENGTH
queue = await queue_for(ctx, create=False)
queue.queue[0].set_already_read(already_read)
@at('commands', 'queue')
async def queue_(ctx: Context, args: list[str]) -> None:
match args:
case ['help']:
await ctx.reply('current queue')
case []:
await ctx.long((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']:
(await queue_for(ctx, create=False)).clear(ctx.member)
await ctx.reply('done')
case ['resume']:
async with lock_for(ctx.guild, 'not in a guild'):
await queue_for(ctx, create=True)
await ctx.reply('done')
case ['pause']:
async with lock_for(ctx.guild, 'not in a guild'):
vc = await vc_for(ctx, create=True)
vc.pause()
await ctx.reply('done')
case _:
raise Explicit("misformatted")
@ -686,6 +779,7 @@ async def save_vcs():
for vc in list(client.voice_clients):
await asyncio.sleep(0.01)
if vc.is_playing():
if vc.guild is not None and vc.channel is not None:
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
queue_db.set_nowait('vcs', vcs)