timestamps
This commit is contained in:
parent
5d2c80e1b3
commit
dbce057615
@ -2,6 +2,7 @@ import asyncio
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -112,14 +113,78 @@ class YTAudio(discord.AudioSource):
|
|||||||
self.loaded = False
|
self.loaded = False
|
||||||
self.regenerating = False
|
self.regenerating = False
|
||||||
self.set_source()
|
self.set_source()
|
||||||
|
self._durations: dict[str, str] = {}
|
||||||
|
|
||||||
def set_source(self):
|
def set_source(self):
|
||||||
|
self.schedule_duration_update()
|
||||||
self.source = discord.FFmpegPCMAudio(
|
self.source = discord.FFmpegPCMAudio(
|
||||||
self.url,
|
self.url,
|
||||||
options=self.options,
|
options=self.options,
|
||||||
before_options=self.before_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):
|
def before_options(self):
|
||||||
before_options = ''
|
before_options = ''
|
||||||
if 'https' in self.url:
|
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'
|
'-reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 10 -copy_unknown'
|
||||||
)
|
)
|
||||||
if self.already_read:
|
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
|
return before_options
|
||||||
|
|
||||||
def read(self) -> bytes:
|
def read(self) -> bytes:
|
||||||
@ -287,10 +354,10 @@ class QueueAudio(discord.AudioSource):
|
|||||||
else:
|
else:
|
||||||
raise Explicit('not an administrator')
|
raise Explicit('not an administrator')
|
||||||
|
|
||||||
def format(self) -> str:
|
async def format(self) -> str:
|
||||||
stream = StringIO()
|
stream = StringIO()
|
||||||
for i, audio in enumerate(list(self.queue)):
|
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()
|
return stream.getvalue()
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
@ -415,7 +482,7 @@ async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[s
|
|||||||
return YTAudio(
|
return YTAudio(
|
||||||
await real_url(info['url'], False),
|
await real_url(info['url'], False),
|
||||||
info['url'],
|
info['url'],
|
||||||
f'{escape(info.get("title"))} `Rby` {ctx.member}',
|
f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}',
|
||||||
options,
|
options,
|
||||||
ctx.member,
|
ctx.member,
|
||||||
already_read
|
already_read
|
||||||
@ -506,6 +573,7 @@ async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
|
|||||||
mainasrcs: dict[discord.Guild, MainAudio] = {}
|
mainasrcs: dict[discord.Guild, MainAudio] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
match args:
|
||||||
@ -553,7 +621,7 @@ async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Explicit("not playing")
|
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)
|
vc.play(source)
|
||||||
return source
|
return source
|
||||||
|
|
||||||
@ -603,19 +671,44 @@ async def skip(ctx: Context, args: list[str]) -> None:
|
|||||||
await ctx.reply('done')
|
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')
|
@at('commands', 'queue')
|
||||||
async def queue_(ctx: Context, args: list[str]) -> None:
|
async def queue_(ctx: Context, args: list[str]) -> None:
|
||||||
match args:
|
match args:
|
||||||
case ['help']:
|
case ['help']:
|
||||||
await ctx.reply('current queue')
|
await ctx.reply('current queue')
|
||||||
case []:
|
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']:
|
case ['clear']:
|
||||||
(await queue_for(ctx, create=False)).clear(ctx.member)
|
(await queue_for(ctx, create=False)).clear(ctx.member)
|
||||||
await ctx.reply('done')
|
await ctx.reply('done')
|
||||||
case ['resume']:
|
case ['resume']:
|
||||||
async with lock_for(ctx.guild, 'not in a guild'):
|
async with lock_for(ctx.guild, 'not in a guild'):
|
||||||
await queue_for(ctx, create=True)
|
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 _:
|
case _:
|
||||||
raise Explicit("misformatted")
|
raise Explicit("misformatted")
|
||||||
|
|
||||||
@ -686,6 +779,7 @@ async def save_vcs():
|
|||||||
for vc in list(client.voice_clients):
|
for vc in list(client.voice_clients):
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
if vc.is_playing():
|
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()))
|
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
|
||||||
queue_db.set_nowait('vcs', vcs)
|
queue_db.set_nowait('vcs', vcs)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user