diff --git a/v6d3music/commands.py b/v6d3music/commands.py index a51b1d5..9dcdeb2 100644 --- a/v6d3music/commands.py +++ b/v6d3music/commands.py @@ -60,9 +60,11 @@ presets: {shlex.join(allowed_presets)} queue = await mainservice.context(ctx, create=True, force_play=False).queue() if attachments: args = ["[[", *(attachment.url for attachment in attachments), "]]"] + args + added = 0 async for audio in mainservice.yt_audios(ctx, args): queue.append(audio) - await ctx.reply("done") + added += 1 + await ctx.reply(f"added track(s): {added}") @at("cancel") async def cancel(ctx: Context, _args: list[str]) -> None: diff --git a/v6d3music/core/queueaudio.py b/v6d3music/core/queueaudio.py index f098789..1a658a2 100644 --- a/v6d3music/core/queueaudio.py +++ b/v6d3music/core/queueaudio.py @@ -31,7 +31,8 @@ class QueueAudio(discord.AudioSource): def update_sources(self): for i in range(PRE_SET_LENGTH): try: - self.queue[i].set_source_if_necessary() + audio = self.queue[i] + audio.set_source_given_index(i) except IndexError: return @@ -63,7 +64,7 @@ class QueueAudio(discord.AudioSource): def append(self, audio: YTAudio): if len(self.queue) < PRE_SET_LENGTH: - audio.set_source_if_necessary() + audio.set_source_given_index(len(self.queue)) self.queue.append(audio) def _popleft(self, audio: YTAudio): diff --git a/v6d3music/core/ytaudio.py b/v6d3music/core/ytaudio.py index de1fd2c..ece829f 100644 --- a/v6d3music/core/ytaudio.py +++ b/v6d3music/core/ytaudio.py @@ -16,29 +16,32 @@ from v6d3music.utils.options_for_effects import * from v6d3music.utils.sparq import * from v6d3music.utils.tor_prefix import * -__all__ = ('YTAudio',) +__all__ = ("YTAudio",) class YTAudio(discord.AudioSource): source: FFmpegNormalAudio def __init__( - self, - servicing: YTAServicing, - url: str, - origin: str, - description: str, - options: Optional[str], - rby: discord.Member | None, - already_read: int, - tor: bool, - /, - *, - stop_at: int | None = None + self, + servicing: YTAServicing, + url: str, + origin: str, + description: str, + options: Optional[str], + rby: discord.Member | None, + already_read: int, + tor: bool, + /, + *, + stop_at: int | None = None, ): self.servicing = servicing self.url = url self.origin = origin + self.unstable = False + if "https://soundcloud.com/" in self.origin: + self.unstable = True self.description = description self.options = options 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} def set_source_if_necessary(self): - if not hasattr(self, 'source'): + if not hasattr(self, "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): self.schedule_duration_update() self.source = FFmpegNormalAudio( - self.url, - options=self.options, - before_options=self.before_options(), - tor=self.tor + self.url, options=self.options, before_options=self.before_options(), tor=self.tor ) def set_already_read(self, already_read: int): @@ -87,7 +97,7 @@ class YTAudio(discord.AudioSource): seconds = round(self.source_seconds()) minutes, seconds = divmod(seconds, 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): self.loop.create_task(self.update_duration()) @@ -99,17 +109,22 @@ class YTAudio(discord.AudioSource): url: str = self.url if url in self._durations: return - self._durations.setdefault(url, '') + self._durations.setdefault(url, "") if self.tor: args = [*tor_prefix()] else: args = [] args += [ - 'ffprobe', '-i', url, - '-show_entries', 'format=duration', - '-v', 'quiet', - '-of', 'default=noprint_wrappers=1:nokey=1', - '-sexagesimal' + "ffprobe", + "-i", + url, + "-show_entries", + "format=duration", + "-v", + "quiet", + "-of", + "default=noprint_wrappers=1:nokey=1", + "-sexagesimal", ] ap = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE) code = await ap.wait() @@ -117,14 +132,14 @@ class YTAudio(discord.AudioSource): pass else: 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 with self._duration_lock: await self._do_update_duration() async def _update_duration_context(self, context: CoroContext): - context.events.send(CoroStatusChanged({'ytaudio': 'duration'})) + context.events.send(CoroStatusChanged({"ytaudio": "duration"})) await self._update_duration() async def update_duration(self): @@ -134,23 +149,21 @@ class YTAudio(discord.AudioSource): duration = self._durations.get(self.url) if duration is None: self.schedule_duration_update() - return duration or '?:??:??' + return duration or "?:??:??" def before_options(self) -> str: - before_options = '' - if 'https' in self.url: + before_options = "" + if "https" in self.url and not self.unstable: 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: - before_options += ( - f' -ss {self.source_seconds()}' - ) - return before_options + before_options += f" -ss {self.source_seconds()}" + return before_options.strip() def estimated_seconds_duration(self) -> float: duration = self.duration() - _m = re.match(r'(\d+):(\d+):(\d+)', duration) + _m = re.match(r"(\d+):(\d+):(\d+)", duration) if _m is None: return 0.0 else: @@ -170,25 +183,21 @@ class YTAudio(discord.AudioSource): if self.regenerating: return FILL if self.stop_at is not None and self.already_read >= self.stop_at - 1: - return b'' + return b"" self.already_read += 1 ret: bytes = self.source.read() 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.regenerating = True - self.loop.create_task( - self.regenerate( - 'underran' if droppable else 'not droppable' - ) - ) + self.loop.create_task(self.regenerate("underran" if droppable else "not droppable")) return FILL else: - print(f'dropped {self.origin}') + print(f"dropped {self.origin}") return ret def cleanup(self): - if hasattr(self, 'source'): + if hasattr(self, "source"): self.source.cleanup() def can_be_skipped_by(self, member: discord.Member) -> bool: @@ -210,65 +219,65 @@ class YTAudio(discord.AudioSource): def hybernate(self) -> dict: return { - 'url': self.url, - 'origin': self.origin, - 'description': self.description, - 'options': self.options, - 'rby': None if self.rby is None else self.rby.id, - 'already_read': self.already_read, - 'tor': self.tor, - 'stop_at': self.stop_at, - 'durations': self._reduced_durations(), + "url": self.url, + "origin": self.origin, + "description": self.description, + "options": self.options, + "rby": None if self.rby is None else self.rby.id, + "already_read": self.already_read, + "tor": self.tor, + "stop_at": self.stop_at, + "durations": self._reduced_durations(), } @classmethod - async def respawn(cls, servicing: YTAServicing, guild: discord.Guild, respawn: dict) -> 'YTAudio': - member_id: int | None = respawn['rby'] + async def respawn(cls, servicing: YTAServicing, guild: discord.Guild, respawn: dict) -> "YTAudio": + member_id: int | None = respawn["rby"] if member_id is None: member = None else: member = guild.get_member(member_id) if member is None: try: - member = await guild.fetch_member(respawn['rby']) + member = await guild.fetch_member(respawn["rby"]) guild._add_member(member) except discord.NotFound: member = None audio = YTAudio( servicing, - respawn['url'], - respawn['origin'], - respawn['description'], - respawn['options'], + respawn["url"], + respawn["origin"], + respawn["description"], + respawn["options"], member, - respawn['already_read'], - respawn.get('tor', False), - stop_at=respawn.get('stop_at', None), + respawn["already_read"], + respawn.get("tor", False), + stop_at=respawn.get("stop_at", None), ) - audio._durations |= respawn.get('durations', {}) + audio._durations |= respawn.get("durations", {}) return audio async def regenerate(self, reason: str): 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) - if hasattr(self, 'source'): + if hasattr(self, "source"): self.source.cleanup() self.set_source() - print(f'regenerated {self.origin}') + print(f"regenerated {self.origin}") finally: self.regenerating = False async def pubjson(self, member: discord.Member) -> dict: return { - 'seconds': self.source_seconds(), - 'timecode': self.source_timecode(), - 'duration': self.duration(), - 'description': self.description, - 'canbeskipped': self.can_be_skipped_by(member), + "seconds": self.source_seconds(), + "timecode": self.source_timecode(), + "duration": self.duration(), + "description": self.description, + "canbeskipped": self.can_be_skipped_by(member), } - def copy(self) -> 'YTAudio': + def copy(self) -> "YTAudio": return YTAudio( self.servicing, self.url, @@ -280,9 +289,9 @@ class YTAudio(discord.AudioSource): self.tor, ) - def branch(self) -> 'YTAudio': + def branch(self) -> "YTAudio": if self.stop_at is not None: - raise Explicit('already branched') + raise Explicit("already branched") self.stop_at = stop_at = self.already_read + 50 audio = YTAudio( self.servicing,