stability check

This commit is contained in:
AF 2023-04-07 10:04:09 +00:00
parent abae781b52
commit 5615271518
3 changed files with 91 additions and 79 deletions

View File

@ -60,9 +60,11 @@ presets: {shlex.join(allowed_presets)}
queue = await mainservice.context(ctx, create=True, force_play=False).queue() queue = await mainservice.context(ctx, create=True, force_play=False).queue()
if attachments: if attachments:
args = ["[[", *(attachment.url for attachment in attachments), "]]"] + args args = ["[[", *(attachment.url for attachment in attachments), "]]"] + args
added = 0
async for audio in mainservice.yt_audios(ctx, args): async for audio in mainservice.yt_audios(ctx, args):
queue.append(audio) queue.append(audio)
await ctx.reply("done") added += 1
await ctx.reply(f"added track(s): {added}")
@at("cancel") @at("cancel")
async def cancel(ctx: Context, _args: list[str]) -> None: async def cancel(ctx: Context, _args: list[str]) -> None:

View File

@ -31,7 +31,8 @@ class QueueAudio(discord.AudioSource):
def update_sources(self): def update_sources(self):
for i in range(PRE_SET_LENGTH): for i in range(PRE_SET_LENGTH):
try: try:
self.queue[i].set_source_if_necessary() audio = self.queue[i]
audio.set_source_given_index(i)
except IndexError: except IndexError:
return return
@ -63,7 +64,7 @@ class QueueAudio(discord.AudioSource):
def append(self, audio: YTAudio): def append(self, audio: YTAudio):
if len(self.queue) < PRE_SET_LENGTH: if len(self.queue) < PRE_SET_LENGTH:
audio.set_source_if_necessary() audio.set_source_given_index(len(self.queue))
self.queue.append(audio) self.queue.append(audio)
def _popleft(self, audio: YTAudio): def _popleft(self, audio: YTAudio):

View File

@ -16,29 +16,32 @@ from v6d3music.utils.options_for_effects import *
from v6d3music.utils.sparq import * from v6d3music.utils.sparq import *
from v6d3music.utils.tor_prefix import * from v6d3music.utils.tor_prefix import *
__all__ = ('YTAudio',) __all__ = ("YTAudio",)
class YTAudio(discord.AudioSource): class YTAudio(discord.AudioSource):
source: FFmpegNormalAudio source: FFmpegNormalAudio
def __init__( def __init__(
self, self,
servicing: YTAServicing, servicing: YTAServicing,
url: str, url: str,
origin: str, origin: str,
description: str, description: str,
options: Optional[str], options: Optional[str],
rby: discord.Member | None, rby: discord.Member | None,
already_read: int, already_read: int,
tor: bool, tor: bool,
/, /,
*, *,
stop_at: int | None = None stop_at: int | None = None,
): ):
self.servicing = servicing self.servicing = servicing
self.url = url self.url = url
self.origin = origin self.origin = origin
self.unstable = False
if "https://soundcloud.com/" in self.origin:
self.unstable = True
self.description = description self.description = description
self.options = options self.options = options
self.rby = rby self.rby = rby
@ -56,16 +59,23 @@ class YTAudio(discord.AudioSource):
return {url: duration for url, duration in self._durations.items() if url == self.url} return {url: duration for url, duration in self._durations.items() if url == self.url}
def set_source_if_necessary(self): def set_source_if_necessary(self):
if not hasattr(self, 'source'): if not hasattr(self, "source"):
self.set_source() self.set_source()
def set_source_if_stable(self):
if not self.unstable:
self.set_source_if_necessary()
def set_source_given_index(self, index: int):
if index:
self.set_source_if_stable()
else:
self.set_source_if_necessary()
def set_source(self): def set_source(self):
self.schedule_duration_update() self.schedule_duration_update()
self.source = FFmpegNormalAudio( self.source = FFmpegNormalAudio(
self.url, self.url, options=self.options, before_options=self.before_options(), tor=self.tor
options=self.options,
before_options=self.before_options(),
tor=self.tor
) )
def set_already_read(self, already_read: int): def set_already_read(self, already_read: int):
@ -87,7 +97,7 @@ class YTAudio(discord.AudioSource):
seconds = round(self.source_seconds()) seconds = round(self.source_seconds())
minutes, seconds = divmod(seconds, 60) minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
return f'{hours}:{minutes:02d}:{seconds:02d}' return f"{hours}:{minutes:02d}:{seconds:02d}"
def _schedule_duration_update(self): def _schedule_duration_update(self):
self.loop.create_task(self.update_duration()) self.loop.create_task(self.update_duration())
@ -99,17 +109,22 @@ class YTAudio(discord.AudioSource):
url: str = self.url url: str = self.url
if url in self._durations: if url in self._durations:
return return
self._durations.setdefault(url, '') self._durations.setdefault(url, "")
if self.tor: if self.tor:
args = [*tor_prefix()] args = [*tor_prefix()]
else: else:
args = [] args = []
args += [ args += [
'ffprobe', '-i', url, "ffprobe",
'-show_entries', 'format=duration', "-i",
'-v', 'quiet', url,
'-of', 'default=noprint_wrappers=1:nokey=1', "-show_entries",
'-sexagesimal' "format=duration",
"-v",
"quiet",
"-of",
"default=noprint_wrappers=1:nokey=1",
"-sexagesimal",
] ]
ap = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE) ap = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE)
code = await ap.wait() code = await ap.wait()
@ -117,14 +132,14 @@ class YTAudio(discord.AudioSource):
pass pass
else: else:
assert ap.stdout is not None assert ap.stdout is not None
self._durations[url] = (await ap.stdout.read()).decode().strip().split('.')[0] self._durations[url] = (await ap.stdout.read()).decode().strip().split(".")[0]
async def _update_duration(self): async def _update_duration(self):
async with self._duration_lock: async with self._duration_lock:
await self._do_update_duration() await self._do_update_duration()
async def _update_duration_context(self, context: CoroContext): async def _update_duration_context(self, context: CoroContext):
context.events.send(CoroStatusChanged({'ytaudio': 'duration'})) context.events.send(CoroStatusChanged({"ytaudio": "duration"}))
await self._update_duration() await self._update_duration()
async def update_duration(self): async def update_duration(self):
@ -134,23 +149,21 @@ class YTAudio(discord.AudioSource):
duration = self._durations.get(self.url) duration = self._durations.get(self.url)
if duration is None: if duration is None:
self.schedule_duration_update() self.schedule_duration_update()
return duration or '?:??:??' return duration or "?:??:??"
def before_options(self) -> str: def before_options(self) -> str:
before_options = '' before_options = ""
if 'https' in self.url: if "https" in self.url and not self.unstable:
before_options += ( before_options += (
'-reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 60 -copy_unknown' " -reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 60 -copy_unknown"
) )
if self.already_read: if self.already_read:
before_options += ( before_options += f" -ss {self.source_seconds()}"
f' -ss {self.source_seconds()}' return before_options.strip()
)
return before_options
def estimated_seconds_duration(self) -> float: def estimated_seconds_duration(self) -> float:
duration = self.duration() duration = self.duration()
_m = re.match(r'(\d+):(\d+):(\d+)', duration) _m = re.match(r"(\d+):(\d+):(\d+)", duration)
if _m is None: if _m is None:
return 0.0 return 0.0
else: else:
@ -170,25 +183,21 @@ class YTAudio(discord.AudioSource):
if self.regenerating: if self.regenerating:
return FILL return FILL
if self.stop_at is not None and self.already_read >= self.stop_at - 1: if self.stop_at is not None and self.already_read >= self.stop_at - 1:
return b'' return b""
self.already_read += 1 self.already_read += 1
ret: bytes = self.source.read() ret: bytes = self.source.read()
if not ret and (not (droppable := self.source.droppable()) or self.underran()): if not ret and (not (droppable := self.source.droppable()) or self.underran()):
if self.attempts < 5 or random.random() > .1: if self.attempts < 5 or random.random() > 0.1:
self.attempts += 1 self.attempts += 1
self.regenerating = True self.regenerating = True
self.loop.create_task( self.loop.create_task(self.regenerate("underran" if droppable else "not droppable"))
self.regenerate(
'underran' if droppable else 'not droppable'
)
)
return FILL return FILL
else: else:
print(f'dropped {self.origin}') print(f"dropped {self.origin}")
return ret return ret
def cleanup(self): def cleanup(self):
if hasattr(self, 'source'): if hasattr(self, "source"):
self.source.cleanup() self.source.cleanup()
def can_be_skipped_by(self, member: discord.Member) -> bool: def can_be_skipped_by(self, member: discord.Member) -> bool:
@ -210,65 +219,65 @@ class YTAudio(discord.AudioSource):
def hybernate(self) -> dict: def hybernate(self) -> dict:
return { return {
'url': self.url, "url": self.url,
'origin': self.origin, "origin": self.origin,
'description': self.description, "description": self.description,
'options': self.options, "options": self.options,
'rby': None if self.rby is None else self.rby.id, "rby": None if self.rby is None else self.rby.id,
'already_read': self.already_read, "already_read": self.already_read,
'tor': self.tor, "tor": self.tor,
'stop_at': self.stop_at, "stop_at": self.stop_at,
'durations': self._reduced_durations(), "durations": self._reduced_durations(),
} }
@classmethod @classmethod
async def respawn(cls, servicing: YTAServicing, guild: discord.Guild, respawn: dict) -> 'YTAudio': async def respawn(cls, servicing: YTAServicing, guild: discord.Guild, respawn: dict) -> "YTAudio":
member_id: int | None = respawn['rby'] member_id: int | None = respawn["rby"]
if member_id is None: if member_id is None:
member = None member = None
else: else:
member = guild.get_member(member_id) member = guild.get_member(member_id)
if member is None: if member is None:
try: try:
member = await guild.fetch_member(respawn['rby']) member = await guild.fetch_member(respawn["rby"])
guild._add_member(member) guild._add_member(member)
except discord.NotFound: except discord.NotFound:
member = None member = None
audio = YTAudio( audio = YTAudio(
servicing, servicing,
respawn['url'], respawn["url"],
respawn['origin'], respawn["origin"],
respawn['description'], respawn["description"],
respawn['options'], respawn["options"],
member, member,
respawn['already_read'], respawn["already_read"],
respawn.get('tor', False), respawn.get("tor", False),
stop_at=respawn.get('stop_at', None), stop_at=respawn.get("stop_at", None),
) )
audio._durations |= respawn.get('durations', {}) audio._durations |= respawn.get("durations", {})
return audio return audio
async def regenerate(self, reason: str): async def regenerate(self, reason: str):
try: try:
print(f'regenerating {self.origin} {reason=}') print(f"regenerating {self.origin} {reason=}")
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor) self.url = await real_url(self.servicing.caching, self.origin, True, self.tor)
if hasattr(self, 'source'): if hasattr(self, "source"):
self.source.cleanup() self.source.cleanup()
self.set_source() self.set_source()
print(f'regenerated {self.origin}') print(f"regenerated {self.origin}")
finally: finally:
self.regenerating = False self.regenerating = False
async def pubjson(self, member: discord.Member) -> dict: async def pubjson(self, member: discord.Member) -> dict:
return { return {
'seconds': self.source_seconds(), "seconds": self.source_seconds(),
'timecode': self.source_timecode(), "timecode": self.source_timecode(),
'duration': self.duration(), "duration": self.duration(),
'description': self.description, "description": self.description,
'canbeskipped': self.can_be_skipped_by(member), "canbeskipped": self.can_be_skipped_by(member),
} }
def copy(self) -> 'YTAudio': def copy(self) -> "YTAudio":
return YTAudio( return YTAudio(
self.servicing, self.servicing,
self.url, self.url,
@ -280,9 +289,9 @@ class YTAudio(discord.AudioSource):
self.tor, self.tor,
) )
def branch(self) -> 'YTAudio': def branch(self) -> "YTAudio":
if self.stop_at is not None: if self.stop_at is not None:
raise Explicit('already branched') raise Explicit("already branched")
self.stop_at = stop_at = self.already_read + 50 self.stop_at = stop_at = self.already_read + 50
audio = YTAudio( audio = YTAudio(
self.servicing, self.servicing,