timestamps
This commit is contained in:
parent
5d2c80e1b3
commit
dbce057615
@ -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,7 +779,8 @@ async def save_vcs():
|
||||
for vc in list(client.voice_clients):
|
||||
await asyncio.sleep(0.01)
|
||||
if vc.is_playing():
|
||||
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
|
||||
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)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user