async def on_red_audio_track_auto_play( self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member, player: lavalink.Player, ): notify_channel = self.bot.get_channel(player.fetch("channel")) tries = 0 while not player._is_playing: await asyncio.sleep(0.1) if tries > 1000: return if notify_channel and not player.fetch("autoplay_notified", False): await self.send_embed_msg(notify_channel, title=_("Auto Play started.")) player.store("autoplay_notified", True)
async def _eq_check(self, ctx: commands.Context, player: lavalink.Player) -> None: eq = player.fetch("eq", Equalizer()) config_bands = await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands() if not config_bands: config_bands = eq.bands await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands) if eq.bands != config_bands: band_num = list(range(0, eq.band_count)) band_value = config_bands eq_dict = {} for k, v in zip(band_num, band_value): eq_dict[k] = v for band, value in eq_dict.items(): eq.set_gain(band, value) player.store("eq", eq) await self._apply_gains(ctx.guild.id, config_bands)
async def on_red_audio_track_auto_play( self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member, player: lavalink.Player, ): notify_channel = self.bot.get_channel(player.fetch("channel")) tries = 0 while not player._is_playing: await asyncio.sleep(0.1) if tries > 1000: return if notify_channel and not player.fetch("autoplay_notified", False): if (len(player.manager.players) < 10 or not player._last_resume and player._last_resume + datetime.timedelta(seconds=60) > datetime.datetime.now(tz=datetime.timezone.utc)): await self.send_embed_msg(notify_channel, title=_("Auto Play started.")) player.store("autoplay_notified", True)
async def lavalink_event_handler(self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra): if event_type == lavalink.LavalinkEvents.TRACK_START: track = player.current if track: embed = Embed( description="▶️ **Now playing** [{0.title}]({0.uri}) !". format(track)) message = await track.channel.send(embed=embed) track.start_message = message if not len(player.queue) > 1: player.store("m_msg", message) if event_type == lavalink.LavalinkEvents.TRACK_END: msg = player.fetch("m_msg") if (extra == lavalink.TrackEndReason.FINISHED or extra == lavalink.TrackEndReason.REPLACED): if msg: await msg.delete() if len(player.queue): await player.stop()
async def lavalink_event_handler(self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra) -> None: current_track = player.current current_channel = player.channel guild = self.rgetattr(current_channel, "guild", None) if not (current_channel and guild): player.store("autoplay_notified", False) await player.stop() await player.disconnect() return if await self.bot.cog_disabled_in_guild(self, guild): await player.stop() await player.disconnect() if guild: await self.config.guild_from_id( guild_id=guild.id).currently_auto_playing_in.set([]) return guild_id = self.rgetattr(guild, "id", None) if not guild: return guild_data = await self.config.guild(guild).all() disconnect = guild_data["disconnect"] if event_type == lavalink.LavalinkEvents.WEBSOCKET_CLOSED: deafen = guild_data["auto_deafen"] await self._websocket_closed_handler(guild=guild, player=player, extra=extra, deafen=deafen, disconnect=disconnect) return await set_contextual_locales_from_guild(self.bot, guild) current_requester = self.rgetattr(current_track, "requester", None) current_stream = self.rgetattr(current_track, "is_stream", None) current_length = self.rgetattr(current_track, "length", None) current_thumbnail = self.rgetattr(current_track, "thumbnail", None) current_id = self.rgetattr(current_track, "_info", {}).get("identifier") repeat = guild_data["repeat"] notify = guild_data["notify"] autoplay = guild_data["auto_play"] description = await self.get_track_description( current_track, self.local_folder_current_path) status = await self.config.status() log.debug( f"Received a new lavalink event for {guild_id}: {event_type}: {extra}" ) prev_song: lavalink.Track = player.fetch("prev_song") await self.maybe_reset_error_counter(player) if event_type == lavalink.LavalinkEvents.TRACK_START: self.skip_votes[guild] = [] playing_song = player.fetch("playing_song") requester = player.fetch("requester") player.store("prev_song", playing_song) player.store("prev_requester", requester) player.store("playing_song", current_track) player.store("requester", current_requester) self.bot.dispatch("red_audio_track_start", guild, current_track, current_requester) if guild_id and current_track: await self.api_interface.persistent_queue_api.played( guild_id=guild_id, track_id=current_track.track_identifier) notify_channel = player.fetch("channel") if notify_channel: await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set( [notify_channel, player.channel.id]) else: await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) if event_type == lavalink.LavalinkEvents.TRACK_END: prev_requester = player.fetch("prev_requester") self.bot.dispatch("red_audio_track_end", guild, prev_song, prev_requester) player.store("resume_attempts", 0) if event_type == lavalink.LavalinkEvents.QUEUE_END: prev_requester = player.fetch("prev_requester") self.bot.dispatch("red_audio_queue_end", guild, prev_song, prev_requester) if guild_id: await self.api_interface.persistent_queue_api.drop(guild_id) if player.is_auto_playing or (autoplay and not player.queue and player.fetch("playing_song") is not None and self.playlist_api is not None and self.api_interface is not None): notify_channel = player.fetch("channel") try: await self.api_interface.autoplay(player, self.playlist_api) except DatabaseError: notify_channel = self.bot.get_channel(notify_channel) if notify_channel: await self.send_embed_msg( notify_channel, title=_("Couldn't get a valid track.")) return except TrackEnqueueError: notify_channel = self.bot.get_channel(notify_channel) if notify_channel: await self.send_embed_msg( notify_channel, title=_("Unable to Get Track"), description=_( "I'm unable to get a track from Lavalink at the moment, try again in a few " "minutes."), ) return if event_type == lavalink.LavalinkEvents.TRACK_START and notify: notify_channel = player.fetch("channel") if notify_channel: notify_channel = self.bot.get_channel(notify_channel) if player.fetch("notify_message") is not None: with contextlib.suppress(discord.HTTPException): await player.fetch("notify_message").delete() if not (description and notify_channel): return if current_stream: dur = "LIVE" else: dur = self.format_time(current_length) thumb = None if await self.config.guild(guild ).thumbnail() and current_thumbnail: thumb = current_thumbnail notify_message = await self.send_embed_msg( notify_channel, description=description, footer=_("Track length: {length} | Requested by: {user}"). format(length=dur, user=current_requester), thumbnail=thumb, author={ "name": _("Now Playing"), "url": "https://cdn.discordapp.com/emojis/572861527049109515.gif", }, ) player.store("notify_message", notify_message) if event_type == lavalink.LavalinkEvents.TRACK_START and status: player_check = await self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type == lavalink.LavalinkEvents.TRACK_END and status: await asyncio.sleep(1) if not player.is_playing: player_check = await self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type == lavalink.LavalinkEvents.QUEUE_END: if not autoplay: notify_channel = player.fetch("channel") if notify_channel and notify: notify_channel = self.bot.get_channel(notify_channel) await self.send_embed_msg(notify_channel, title=_("Queue ended.")) if disconnect: self.bot.dispatch("red_audio_audio_disconnect", guild) await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) await player.disconnect() self._ll_guild_updates.discard(guild.id) if status: player_check = await self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type in [ lavalink.LavalinkEvents.TRACK_EXCEPTION, lavalink.LavalinkEvents.TRACK_STUCK, ]: message_channel = player.fetch("channel") while True: if current_track in player.queue: player.queue.remove(current_track) else: break if repeat: player.current = None if not guild_id: return guild_id = int(guild_id) self._error_counter.setdefault(guild_id, 0) if guild_id not in self._error_counter: self._error_counter[guild_id] = 0 early_exit = await self.increase_error_counter(player) if early_exit: self._disconnected_players[guild_id] = True self.play_lock[guild_id] = False eq = player.fetch("eq") player.queue = [] player.store("playing_song", None) player.store("autoplay_notified", False) if eq: await self.config.custom("EQUALIZER", guild_id).eq_bands.set(eq.bands) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) self._ll_guild_updates.discard(guild_id) self.bot.dispatch("red_audio_audio_disconnect", guild) if message_channel: message_channel = self.bot.get_channel(message_channel) if early_exit: embed = discord.Embed( colour=await self.bot.get_embed_color(message_channel), title=_("Multiple Errors Detected"), description=_( "Closing the audio player " "due to multiple errors being detected. " "If this persists, please inform the bot owner " "as the Audio cog may be temporally unavailable."), ) await message_channel.send(embed=embed) return else: description = description or "" if event_type == lavalink.LavalinkEvents.TRACK_STUCK: embed = discord.Embed( colour=await self.bot.get_embed_color(message_channel), title=_("Track Stuck"), description= _("Playback of the song has stopped due to an unexcepted error.\n{error}" ).format(error=description), ) else: embed = discord.Embed( title=_("Track Error"), colour=await self.bot.get_embed_color(message_channel), description="{}\n{}".format( extra.replace("\n", ""), description), ) if current_id: asyncio.create_task( self.api_interface.global_cache_api. report_invalid(current_id)) await message_channel.send(embed=embed) await player.skip()
async def _websocket_closed_handler( self, guild: discord.Guild, player: lavalink.Player, extra: Dict, deafen: bool, disconnect: bool, ) -> None: guild_id = guild.id node = player.node voice_ws: DiscordWebSocket = node.get_voice_ws(guild_id) code = extra.get("code") by_remote = extra.get("byRemote", "") reason = extra.get("reason", "").strip() if self._ws_resume[guild_id].is_set(): ws_audio_log.debug( f"WS EVENT | Discarding WS Closed event for guild {guild_id} -> " f"Socket Closed {voice_ws.socket._closing or voice_ws.socket.closed}. " f"Code: {code} -- Remote: {by_remote} -- {reason}") return self._ws_resume[guild_id].set() if player.channel: current_perms = player.channel.permissions_for( player.channel.guild.me) has_perm = current_perms.speak and current_perms.connect else: has_perm = False channel_id = player.channel.id if voice_ws.socket._closing or voice_ws.socket.closed or not voice_ws.open: if player._con_delay: delay = player._con_delay.delay() else: player._con_delay = ExponentialBackoff(base=1) delay = player._con_delay.delay() ws_audio_log.warning( "YOU CAN IGNORE THIS UNLESS IT'S CONSISTENTLY REPEATING FOR THE SAME GUILD - " f"Voice websocket closed for guild {guild_id} -> " f"Socket Closed {voice_ws.socket._closing or voice_ws.socket.closed}. " f"Code: {code} -- Remote: {by_remote} -- {reason}") ws_audio_log.debug( f"Reconnecting to channel {channel_id} in guild: {guild_id} | {delay:.2f}s" ) await asyncio.sleep(delay) while voice_ws.socket._closing or voice_ws.socket.closed or not voice_ws.open: voice_ws = node.get_voice_ws(guild_id) await asyncio.sleep(0.1) if has_perm and player.current and player.is_playing: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Currently playing.") elif has_perm and player.paused and player.current: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.pause(pause=True) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Currently Paused.") elif has_perm and (not disconnect) and (not player.is_playing): player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Not playing, but auto disconnect disabled." ) self._ll_guild_updates.discard(guild_id) elif not has_perm: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected " f"from channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Missing permissions.") self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) else: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected " f"from channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Unknown.") self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) elif code in (42069, ): player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position) ws_audio_log.info( f"Player resumed in channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & {reason}.") elif code in (4015, 4014, 4009, 4006, 1006): if (code == 4006 and has_perm and player._last_resume and player._last_resume + datetime.timedelta(seconds=5) > datetime.datetime.now(tz=datetime.timezone.utc)): attempts = player.fetch("resume_attempts", 0) player.store("resume_attempts", attempts + 1) channel = self.bot.get_channel(player.channel.id) should_skip = not channel.members or all( m.bot for m in channel.members) if should_skip: ws_audio_log.info( "Voice websocket reconnected skipped " f"for channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & " "Player resumed too recently and no human members connected." ) return if player._con_delay: delay = player._con_delay.delay() else: player._con_delay = ExponentialBackoff(base=1) delay = player._con_delay.delay() ws_audio_log.debug( f"Reconnecting to channel {channel_id} in guild: {guild_id} | {delay:.2f}s" ) await asyncio.sleep(delay) if has_perm and player.current and player.is_playing: await player.connect(deafen=deafen) await player.resume(player.current, start=player.position) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Player is active.") elif has_perm and player.paused and player.current: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.pause(pause=True) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Player is paused.") elif has_perm and (not disconnect) and (not player.is_playing): player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) ws_audio_log.info( "Voice websocket reconnected " f"to channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Not playing.") self._ll_guild_updates.discard(guild_id) elif not has_perm: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected " f"from channel {channel_id} in guild: {guild_id} | " f"Reason: Error code {code} & Missing permissions.") self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) else: ws_audio_log.info( "WS EVENT - IGNORED (Healthy Socket) | " f"Voice websocket closed event for guild {guild_id} -> " f"Code: {code} -- Remote: {by_remote} -- {reason}") self._ws_resume[guild_id].clear()
async def _websocket_closed_handler( self, guild: discord.Guild, player: lavalink.Player, extra: Dict, deafen: bool, disconnect: bool, ) -> None: guild_id = guild.id event_channel_id = extra.get("channelID") try: if not self._ws_resume[guild_id].is_set(): await self._ws_resume[guild_id].wait() else: self._ws_resume[guild_id].clear() node = player.node voice_ws: DiscordWebSocket = node.get_voice_ws(guild_id) code = extra.get("code") by_remote = extra.get("byRemote", "") reason = extra.get("reason", "No Specified Reason").strip() channel_id = player.channel.id try: event_channel_id, to_handle_code = await self._ws_op_codes[ guild_id].get() except asyncio.QueueEmpty: log.debug("Empty queue - Resuming Processor - Early exit") return if code != to_handle_code: code = to_handle_code if player.channel.id != event_channel_id: code = 4014 if event_channel_id != channel_id: ws_audio_log.info( "Received an op code for a channel that is no longer valid; %d " "Reason: Error code %d & %s, %r", event_channel_id, code, reason, player, ) self._ws_op_codes[guild_id]._init( self._ws_op_codes[guild_id]._maxsize) return if player.channel: has_perm = self.can_join_and_speak(player.channel) else: has_perm = False if code in ( 1000, ) and has_perm and player.current and player.is_playing: player.store("resumes", player.fetch("resumes", 0) + 1) await player.resume(player.current, start=player.position, replace=True) ws_audio_log.info( "Player resumed | Reason: Error code %d & %s, %r", code, reason, player) self._ws_op_codes[guild_id]._init( self._ws_op_codes[guild_id]._maxsize) return if voice_ws.socket._closing or voice_ws.socket.closed or not voice_ws.open: if player._con_delay: delay = player._con_delay.delay() else: player._con_delay = ExponentialBackoff(base=1) delay = player._con_delay.delay() ws_audio_log.warning( "YOU CAN IGNORE THIS UNLESS IT'S CONSISTENTLY REPEATING FOR THE SAME GUILD - " "Voice websocket closed for guild %d -> " "Socket Closed %s. " "Code: %d -- Remote: %s -- %s, %r", guild_id, voice_ws.socket._closing or voice_ws.socket.closed, code, by_remote, reason, player, ) ws_audio_log.debug( "Reconnecting to channel %d in guild: %d | %.2fs", channel_id, guild_id, delay, ) await asyncio.sleep(delay) while voice_ws.socket._closing or voice_ws.socket.closed or not voice_ws.open: voice_ws = node.get_voice_ws(guild_id) await asyncio.sleep(0.1) if has_perm and player.current and player.is_playing: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position, replace=True) ws_audio_log.info( "Voice websocket reconnected " "Reason: Error code %d & Currently playing, %r", code, player, ) elif has_perm and player.paused and player.current: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position, replace=True, pause=True) ws_audio_log.info( "Voice websocket reconnected " "Reason: Error code %d & Currently Paused, %r", code, player, ) elif has_perm and (not disconnect) and (not player.is_playing): player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) ws_audio_log.info( "Voice websocket reconnected " "Reason: Error code %d & Not playing, but auto disconnect disabled, %r", code, player, ) self._ll_guild_updates.discard(guild_id) elif not has_perm: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected " "Reason: Error code %d & Missing permissions, %r", code, player, ) self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) else: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected Reason: Error code %d & Unknown, %r", code, player, ) self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) elif code in ( 42069, ) and has_perm and player.current and player.is_playing: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position, replace=True) ws_audio_log.info( "Player resumed - Reason: Error code %d & %s, %r", code, reason, player) elif code in (4015, 4009, 4006, 4000, 1006): if player._con_delay: delay = player._con_delay.delay() else: player._con_delay = ExponentialBackoff(base=1) delay = player._con_delay.delay() ws_audio_log.debug( "Reconnecting to channel %d in guild: %d | %.2fs", channel_id, guild_id, delay) await asyncio.sleep(delay) if has_perm and player.current and player.is_playing: await player.connect(deafen=deafen) await player.resume(player.current, start=player.position, replace=True) ws_audio_log.info( "Voice websocket reconnected " "Reason: Error code %d & Player is active, %r", code, player, ) elif has_perm and player.paused and player.current: player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) await player.resume(player.current, start=player.position, replace=True, pause=True) ws_audio_log.info( "Voice websocket reconnected " "Reason: Error code %d & Player is paused, %r", code, player, ) elif has_perm and (not disconnect) and (not player.is_playing): player.store("resumes", player.fetch("resumes", 0) + 1) await player.connect(deafen=deafen) ws_audio_log.info( "Voice websocket reconnected " "to channel %d in guild: %d | " "Reason: Error code %d & Not playing, %r", channel_id, guild_id, code, player, ) self._ll_guild_updates.discard(guild_id) elif not has_perm: self.bot.dispatch("red_audio_audio_disconnect", guild) ws_audio_log.info( "Voice websocket disconnected " "Reason: Error code %d & Missing permissions, %r", code, player, ) self._ll_guild_updates.discard(guild_id) player.store("autoplay_notified", False) await player.stop() await player.disconnect() await self.config.guild_from_id( guild_id=guild_id).currently_auto_playing_in.set([]) else: if not player.paused and player.current: player.store("resumes", player.fetch("resumes", 0) + 1) await player.resume(player.current, start=player.position, replace=True) ws_audio_log.info( "WS EVENT - SIMPLE RESUME (Healthy Socket) | " "Voice websocket closed event " "Code: %d -- Remote: %s -- %s, %r", code, by_remote, reason, player, ) else: ws_audio_log.info( "WS EVENT - IGNORED (Healthy Socket) | " "Voice websocket closed event " "Code: %d -- Remote: %s -- %s, %r", code, by_remote, reason, player, ) except Exception: log.exception("Error in task") finally: self._ws_op_codes[guild_id]._init( self._ws_op_codes[guild_id]._maxsize) self._ws_resume[guild_id].set()
async def autoplay(self, player: lavalink.Player): autoplaylist = await self.config.guild(player.channel.guild ).autoplaylist() current_cache_level = CacheLevel(await self.config.cache_level()) cache_enabled = CacheLevel.set_lavalink().is_subset( current_cache_level) playlist = None tracks = None if autoplaylist["enabled"]: with contextlib.suppress(Exception): playlist = await get_playlist( autoplaylist["id"], autoplaylist["scope"], self.bot, player.channel.guild, player.channel.guild.me, ) tracks = playlist.tracks_obj if not tracks or not getattr(playlist, "tracks", None): if cache_enabled: tracks = await self.get_random_from_db() if not tracks: ctx = namedtuple("Context", "message") (results, called_api) = await self.lavalink_query( ctx(player.channel.guild), player, audio_dataclasses.Query.process_input(_TOP_100_US), ) tracks = list(results.tracks) if tracks: multiple = len(tracks) > 1 track = tracks[0] valid = not multiple tries = len(tracks) while valid is False and multiple: tries -= 1 if tries <= 0: raise DatabaseError("No valid entry found") track = random.choice(tracks) query = audio_dataclasses.Query.process_input(track) await asyncio.sleep(0.001) if not query.valid: continue if query.is_local and not query.track.exists(): continue if not await is_allowed( player.channel.guild, (f"{track.title} {track.author} {track.uri} " f"{str(audio_dataclasses.Query.process_input(track))}"), ): log.debug( "Query is not allowed in " f"{player.channel.guild} ({player.channel.guild.id})") continue valid = True track.extras["autoplay"] = True player.add(player.channel.guild.me, track) self.bot.dispatch("red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me) if not player.current: await player.play()
async def spotify_enqueue( self, ctx: commands.Context, query_type: str, uri: str, enqueue: bool, player: lavalink.Player, lock: Callable, notifier: Optional[Notifier] = None, ) -> List[lavalink.Track]: track_list = [] has_not_allowed = False try: current_cache_level = CacheLevel(await self.config.cache_level()) guild_data = await self.config.guild(ctx.guild).all() # now = int(time.time()) enqueued_tracks = 0 consecutive_fails = 0 queue_dur = await queue_duration(ctx) queue_total_duration = lavalink.utils.format_time(queue_dur) before_queue_length = len(player.queue) tracks_from_spotify = await self._spotify_fetch_tracks( query_type, uri, params=None, notifier=notifier) total_tracks = len(tracks_from_spotify) if total_tracks < 1: lock(ctx, False) embed3 = discord.Embed( colour=await ctx.embed_colour(), title= _("This doesn't seem to be a supported Spotify URL or code." ), ) await notifier.update_embed(embed3) return track_list database_entries = [] time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) youtube_cache = CacheLevel.set_youtube().is_subset( current_cache_level) spotify_cache = CacheLevel.set_spotify().is_subset( current_cache_level) for track_count, track in enumerate(tracks_from_spotify): ( song_url, track_info, uri, artist_name, track_name, _id, _type, ) = self._get_spotify_track_info(track) database_entries.append({ "id": _id, "type": _type, "uri": uri, "track_name": track_name, "artist_name": artist_name, "song_url": song_url, "track_info": track_info, "last_updated": time_now, "last_fetched": time_now, }) val = None if youtube_cache: update = True with contextlib.suppress(SQLError): (val, update) = await self.database.fetch_one( "youtube", "youtube_url", {"track": track_info}) if update: val = None if val is None: val = await self._youtube_first_time_query( ctx, track_info, current_cache_level=current_cache_level) if youtube_cache and val: task = ("update", ("youtube", {"track": track_info})) self.append_task(ctx, *task) if val: try: (result, called_api) = await self.lavalink_query( ctx, player, audio_dataclasses.Query.process_input(val)) except (RuntimeError, aiohttp.ServerDisconnectedError): lock(ctx, False) error_embed = discord.Embed( colour=await ctx.embed_colour(), title= _("The connection was reset while loading the playlist." ), ) await notifier.update_embed(error_embed) break except asyncio.TimeoutError: lock(ctx, False) error_embed = discord.Embed( colour=await ctx.embed_colour(), title=_( "Player timeout, skipping remaining tracks."), ) await notifier.update_embed(error_embed) break track_object = result.tracks else: track_object = [] if (track_count % 2 == 0) or (track_count == total_tracks): key = "lavalink" seconds = "???" second_key = None await notifier.notify_user( current=track_count, total=total_tracks, key=key, seconds_key=second_key, seconds=seconds, ) if consecutive_fails >= 10: error_embed = discord.Embed( colour=await ctx.embed_colour(), title=_("Failing to get tracks, skipping remaining."), ) await notifier.update_embed(error_embed) break if not track_object: consecutive_fails += 1 continue consecutive_fails = 0 single_track = track_object[0] if not await is_allowed( ctx.guild, (f"{single_track.title} {single_track.author} {single_track.uri} " f"{str(audio_dataclasses.Query.process_input(single_track))}" ), ): has_not_allowed = True log.debug( f"Query is not allowed in {ctx.guild} ({ctx.guild.id})" ) continue track_list.append(single_track) if enqueue: if len(player.queue) >= 10000: continue if guild_data["maxlength"] > 0: if track_limit(single_track, guild_data["maxlength"]): enqueued_tracks += 1 player.add(ctx.author, single_track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author, ) else: enqueued_tracks += 1 player.add(ctx.author, single_track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author, ) if not player.current: await player.play() if len(track_list) == 0: if not has_not_allowed: raise SpotifyFetchError(message=_( "Nothing found.\nThe YouTube API key may be invalid " "or you may be rate limited on YouTube's search service.\n" "Check the YouTube API key again and follow the instructions " "at `{prefix}audioset youtubeapi`.").format( prefix=ctx.prefix)) player.maybe_shuffle() if enqueue and tracks_from_spotify: if total_tracks > enqueued_tracks: maxlength_msg = " {bad_tracks} tracks cannot be queued.".format( bad_tracks=(total_tracks - enqueued_tracks)) else: maxlength_msg = "" embed = discord.Embed( colour=await ctx.embed_colour(), title=_("Playlist Enqueued"), description=_( "Added {num} tracks to the queue.{maxlength_msg}"). format(num=enqueued_tracks, maxlength_msg=maxlength_msg), ) if not guild_data["shuffle"] and queue_dur > 0: embed.set_footer( text=_("{time} until start of playlist" " playback: starts at #{position} in queue" ).format(time=queue_total_duration, position=before_queue_length + 1)) await notifier.update_embed(embed) lock(ctx, False) if spotify_cache: task = ("insert", ("spotify", database_entries)) self.append_task(ctx, *task) except Exception as e: lock(ctx, False) raise e finally: lock(ctx, False) return track_list
async def _eq_interact( self, ctx: commands.Context, player: lavalink.Player, eq: Equalizer, message: discord.Message, selected: int, ) -> None: player.store("eq", eq) emoji = { "far_left": "\N{BLACK LEFT-POINTING TRIANGLE}", "one_left": "\N{LEFTWARDS BLACK ARROW}", "max_output": "\N{BLACK UP-POINTING DOUBLE TRIANGLE}", "output_up": "\N{UP-POINTING SMALL RED TRIANGLE}", "output_down": "\N{DOWN-POINTING SMALL RED TRIANGLE}", "min_output": "\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}", "one_right": "\N{BLACK RIGHTWARDS ARROW}", "far_right": "\N{BLACK RIGHT-POINTING TRIANGLE}", "reset": "\N{BLACK CIRCLE FOR RECORD}", "info": "\N{INFORMATION SOURCE}", } selector = f'{" " * 8}{" " * selected}^^' try: await message.edit( content=box(f"{eq.visualise()}\n{selector}", lang="ini")) except discord.errors.NotFound: return try: (react_emoji, react_user) = await self._get_eq_reaction(ctx, message, emoji) except TypeError: return if not react_emoji: await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands) await self._clear_react(message, emoji) if react_emoji == "\N{LEFTWARDS BLACK ARROW}": await self.remove_react(message, react_emoji, react_user) await self._eq_interact(ctx, player, eq, message, max(selected - 1, 0)) if react_emoji == "\N{BLACK RIGHTWARDS ARROW}": await self.remove_react(message, react_emoji, react_user) await self._eq_interact(ctx, player, eq, message, min(selected + 1, 14)) if react_emoji == "\N{UP-POINTING SMALL RED TRIANGLE}": await self.remove_react(message, react_emoji, react_user) _max = float("{:.2f}".format(min(eq.get_gain(selected) + 0.1, 1.0))) eq.set_gain(selected, _max) await self._apply_gain(ctx.guild.id, selected, _max) await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{DOWN-POINTING SMALL RED TRIANGLE}": await self.remove_react(message, react_emoji, react_user) _min = float("{:.2f}".format( max(eq.get_gain(selected) - 0.1, -0.25))) eq.set_gain(selected, _min) await self._apply_gain(ctx.guild.id, selected, _min) await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{BLACK UP-POINTING DOUBLE TRIANGLE}": await self.remove_react(message, react_emoji, react_user) _max = 1.0 eq.set_gain(selected, _max) await self._apply_gain(ctx.guild.id, selected, _max) await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}": await self.remove_react(message, react_emoji, react_user) _min = -0.25 eq.set_gain(selected, _min) await self._apply_gain(ctx.guild.id, selected, _min) await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{BLACK LEFT-POINTING TRIANGLE}": await self.remove_react(message, react_emoji, react_user) selected = 0 await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{BLACK RIGHT-POINTING TRIANGLE}": await self.remove_react(message, react_emoji, react_user) selected = 14 await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{BLACK CIRCLE FOR RECORD}": await self.remove_react(message, react_emoji, react_user) for band in range(eq.band_count): eq.set_gain(band, 0.0) await self._apply_gains(ctx.guild.id, eq.bands) await self._eq_interact(ctx, player, eq, message, selected) if react_emoji == "\N{INFORMATION SOURCE}": await self.remove_react(message, react_emoji, react_user) await ctx.send_help(self.command_equalizer) await self._eq_interact(ctx, player, eq, message, selected)
async def autoplay(self, player: lavalink.Player, playlist_api: PlaylistWrapper): """Enqueue a random track.""" autoplaylist = await self.config.guild(player.channel.guild ).autoplaylist() current_cache_level = CacheLevel(await self.config.cache_level()) cache_enabled = CacheLevel.set_lavalink().is_subset( current_cache_level) playlist = None tracks = None if autoplaylist["enabled"]: try: playlist = await get_playlist( autoplaylist["id"], autoplaylist["scope"], self.bot, playlist_api, player.channel.guild, player.channel.guild.me, ) tracks = playlist.tracks_obj except Exception as exc: debug_exc_log(log, exc, "Failed to fetch playlist for autoplay") if not tracks or not getattr(playlist, "tracks", None): if cache_enabled: track = await self.get_random_track_from_db() tracks = [] if not track else [track] if not tracks: ctx = namedtuple("Context", "message guild cog") (results, called_api) = await self.fetch_track( cast( commands.Context, ctx(player.channel.guild, player.channel.guild, self.cog)), player, Query.process_input(_TOP_100_US, self.cog.local_folder_current_path), ) tracks = list(results.tracks) if tracks: multiple = len(tracks) > 1 valid = not multiple tries = len(tracks) track = tracks[0] while valid is False and multiple: tries -= 1 if tries <= 0: raise DatabaseError("No valid entry found") track = random.choice(tracks) query = Query.process_input(track, self.cog.local_folder_current_path) await asyncio.sleep(0.001) if (not query.valid) or ( query.is_local and query.local_track_path is not None and not query.local_track_path.exists()): continue notify_channel = self.bot.get_channel(player.fetch("channel")) if not await self.cog.is_query_allowed( self.config, notify_channel, f"{track.title} {track.author} {track.uri} {query}", query_obj=query, ): if IS_DEBUG: log.debug( "Query is not allowed in " f"{player.channel.guild} ({player.channel.guild.id})" ) continue valid = True track.extras.update({ "autoplay": True, "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": player.channel.guild.me.id, }) player.add(player.channel.guild.me, track) self.bot.dispatch("red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me) if not player.current: await player.play()
async def spotify_enqueue( self, ctx: commands.Context, query_type: str, uri: str, enqueue: bool, player: lavalink.Player, lock: Callable, notifier: Optional[Notifier] = None, forced: bool = False, query_global: bool = True, ) -> List[lavalink.Track]: """Queries the Database then falls back to Spotify and YouTube APIs then Enqueued matched tracks. Parameters ---------- ctx: commands.Context The context this method is being called under. query_type : str Type of query to perform (Pl uri: str Spotify URL ID. enqueue:bool Whether or not to enqueue the tracks player: lavalink.Player The current Player. notifier: Notifier A Notifier object to handle the user UI notifications while tracks are loaded. lock: Callable A callable handling the Track enqueue lock while spotify tracks are being added. query_global: bool Whether or not to query the global API. forced: bool Ignore Cache and make a fetch from API. Returns ------- List[str] List of Youtube URLs. """ await self.global_cache_api._get_api_key() globaldb_toggle = await self.config.global_db_enabled() global_entry = globaldb_toggle and query_global track_list: List = [] has_not_allowed = False youtube_api_error = None try: current_cache_level = CacheLevel(await self.config.cache_level()) guild_data = await self.config.guild(ctx.guild).all() enqueued_tracks = 0 consecutive_fails = 0 queue_dur = await self.cog.queue_duration(ctx) queue_total_duration = self.cog.format_time(queue_dur) before_queue_length = len(player.queue) tracks_from_spotify = await self.fetch_from_spotify_api( query_type, uri, params=None, notifier=notifier) total_tracks = len(tracks_from_spotify) if total_tracks < 1 and notifier is not None: lock(ctx, False) embed3 = discord.Embed( colour=await ctx.embed_colour(), title= _("This doesn't seem to be a supported Spotify URL or code." ), ) await notifier.update_embed(embed3) return track_list database_entries = [] time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) youtube_cache = CacheLevel.set_youtube().is_subset( current_cache_level) spotify_cache = CacheLevel.set_spotify().is_subset( current_cache_level) async for track_count, track in AsyncIter( tracks_from_spotify).enumerate(start=1): ( song_url, track_info, uri, artist_name, track_name, _id, _type, ) = await self.spotify_api.get_spotify_track_info(track, ctx) database_entries.append({ "id": _id, "type": _type, "uri": uri, "track_name": track_name, "artist_name": artist_name, "song_url": song_url, "track_info": track_info, "last_updated": time_now, "last_fetched": time_now, }) val = None llresponse = None if youtube_cache: try: (val, last_updated ) = await self.local_cache_api.youtube.fetch_one( {"track": track_info}) except Exception as exc: debug_exc_log( log, exc, f"Failed to fetch {track_info} from YouTube table") should_query_global = globaldb_toggle and query_global and val is None if should_query_global: llresponse = await self.global_cache_api.get_spotify( track_name, artist_name) if llresponse: if llresponse.get("loadType") == "V2_COMPACT": llresponse["loadType"] = "V2_COMPAT" llresponse = LoadResult(llresponse) val = llresponse or None if val is None: try: val = await self.fetch_youtube_query( ctx, track_info, current_cache_level=current_cache_level) except YouTubeApiError as err: val = None youtube_api_error = err.message if not youtube_api_error: if youtube_cache and val and llresponse is None: task = ("update", ("youtube", {"track": track_info})) self.append_task(ctx, *task) if isinstance(llresponse, LoadResult): track_object = llresponse.tracks elif val: result = None if should_query_global: llresponse = await self.global_cache_api.get_call( val) if llresponse: if llresponse.get("loadType") == "V2_COMPACT": llresponse["loadType"] = "V2_COMPAT" llresponse = LoadResult(llresponse) result = llresponse or None if not result: try: (result, called_api) = await self.fetch_track( ctx, player, Query.process_input( val, self.cog.local_folder_current_path), forced=forced, should_query_global=not should_query_global, ) except (RuntimeError, aiohttp.ServerDisconnectedError): lock(ctx, False) error_embed = discord.Embed( colour=await ctx.embed_colour(), title= _("The connection was reset while loading the playlist." ), ) if notifier is not None: await notifier.update_embed(error_embed) break except asyncio.TimeoutError: lock(ctx, False) error_embed = discord.Embed( colour=await ctx.embed_colour(), title= _("Player timeout, skipping remaining tracks." ), ) if notifier is not None: await notifier.update_embed(error_embed) break track_object = result.tracks else: track_object = [] else: track_object = [] if (track_count % 2 == 0) or (track_count == total_tracks): key = "lavalink" seconds = "???" second_key = None if notifier is not None: await notifier.notify_user( current=track_count, total=total_tracks, key=key, seconds_key=second_key, seconds=seconds, ) if youtube_api_error or consecutive_fails >= ( 20 if global_entry else 10): error_embed = discord.Embed( colour=await ctx.embed_colour(), title=_("Failing to get tracks, skipping remaining."), ) if notifier is not None: await notifier.update_embed(error_embed) if youtube_api_error: lock(ctx, False) raise SpotifyFetchError(message=youtube_api_error) break if not track_object: consecutive_fails += 1 continue consecutive_fails = 0 single_track = track_object[0] query = Query.process_input(single_track, self.cog.local_folder_current_path) if not await self.cog.is_query_allowed( self.config, ctx, f"{single_track.title} {single_track.author} {single_track.uri} {query}", query_obj=query, ): has_not_allowed = True if IS_DEBUG: log.debug( f"Query is not allowed in {ctx.guild} ({ctx.guild.id})" ) continue track_list.append(single_track) if enqueue: if len(player.queue) >= 10000: continue if guild_data["maxlength"] > 0: if self.cog.is_track_length_allowed( single_track, guild_data["maxlength"]): enqueued_tracks += 1 single_track.extras.update({ "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, }) player.add(ctx.author, single_track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author, ) else: enqueued_tracks += 1 single_track.extras.update({ "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, }) player.add(ctx.author, single_track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author, ) if not player.current: await player.play() if enqueue and tracks_from_spotify: if total_tracks > enqueued_tracks: maxlength_msg = _( " {bad_tracks} tracks cannot be queued.").format( bad_tracks=(total_tracks - enqueued_tracks)) else: maxlength_msg = "" embed = discord.Embed( colour=await ctx.embed_colour(), title=_("Playlist Enqueued"), description=_( "Added {num} tracks to the queue.{maxlength_msg}"). format(num=enqueued_tracks, maxlength_msg=maxlength_msg), ) if not guild_data["shuffle"] and queue_dur > 0: embed.set_footer( text=_("{time} until start of playlist" " playback: starts at #{position} in queue" ).format(time=queue_total_duration, position=before_queue_length + 1)) if notifier is not None: await notifier.update_embed(embed) lock(ctx, False) if not track_list and not has_not_allowed: raise SpotifyFetchError(message=_( "Nothing found.\nThe YouTube API key may be invalid " "or you may be rate limited on YouTube's search service.\n" "Check the YouTube API key again and follow the instructions " "at `{prefix}audioset youtubeapi`.")) player.maybe_shuffle() if spotify_cache: task = ("insert", ("spotify", database_entries)) self.append_task(ctx, *task) except Exception as exc: lock(ctx, False) raise exc finally: lock(ctx, False) return track_list
async def lavalink_event_handler(self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra) -> None: current_track = player.current current_channel = player.channel guild = self.rgetattr(current_channel, "guild", None) guild_id = self.rgetattr(guild, "id", None) current_requester = self.rgetattr(current_track, "requester", None) current_stream = self.rgetattr(current_track, "is_stream", None) current_length = self.rgetattr(current_track, "length", None) current_thumbnail = self.rgetattr(current_track, "thumbnail", None) current_extras = self.rgetattr(current_track, "extras", {}) guild_data = await self.config.guild(guild).all() repeat = guild_data["repeat"] notify = guild_data["notify"] disconnect = guild_data["disconnect"] autoplay = guild_data["auto_play"] description = self.get_track_description( current_track, self.local_folder_current_path) status = await self.config.status() log.debug( f"Received a new lavalink event for {guild_id}: {event_type}: {extra}" ) prev_song: lavalink.Track = player.fetch("prev_song") await self.maybe_reset_error_counter(player) if event_type == lavalink.LavalinkEvents.TRACK_START: self.skip_votes[guild] = [] playing_song = player.fetch("playing_song") requester = player.fetch("requester") player.store("prev_song", playing_song) player.store("prev_requester", requester) player.store("playing_song", current_track) player.store("requester", current_requester) self.bot.dispatch("red_audio_track_start", guild, current_track, current_requester) if event_type == lavalink.LavalinkEvents.TRACK_END: prev_requester = player.fetch("prev_requester") self.bot.dispatch("red_audio_track_end", guild, prev_song, prev_requester) if event_type == lavalink.LavalinkEvents.QUEUE_END: prev_requester = player.fetch("prev_requester") self.bot.dispatch("red_audio_queue_end", guild, prev_song, prev_requester) if (autoplay and not player.queue and player.fetch("playing_song") is not None and self.playlist_api is not None and self.api_interface is not None): try: await self.api_interface.autoplay(player, self.playlist_api) except DatabaseError: notify_channel = player.fetch("channel") if notify_channel: notify_channel = self.bot.get_channel(notify_channel) await self.send_embed_msg( notify_channel, title=_("Couldn't get a valid track.")) return if event_type == lavalink.LavalinkEvents.TRACK_START and notify: notify_channel = player.fetch("channel") if notify_channel: notify_channel = self.bot.get_channel(notify_channel) if player.fetch("notify_message") is not None: with contextlib.suppress(discord.HTTPException): await player.fetch("notify_message").delete() if (autoplay and current_extras.get("autoplay") and (prev_song is None or (hasattr(prev_song, "extras") and not prev_song.extras.get("autoplay")))): await self.send_embed_msg(notify_channel, title=_("Auto Play started.")) if not description: return if current_stream: dur = "LIVE" else: dur = self.format_time(current_length) thumb = None if await self.config.guild(guild ).thumbnail() and current_thumbnail: thumb = current_thumbnail notify_message = await self.send_embed_msg( notify_channel, title=_("Now Playing"), description=description, footer=_("Track length: {length} | Requested by: {user}"). format(length=dur, user=current_requester), thumbnail=thumb, ) player.store("notify_message", notify_message) if event_type == lavalink.LavalinkEvents.TRACK_START and status: player_check = self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type == lavalink.LavalinkEvents.TRACK_END and status: await asyncio.sleep(1) if not player.is_playing: player_check = self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type == lavalink.LavalinkEvents.QUEUE_END: if not autoplay: notify_channel = player.fetch("channel") if notify_channel and notify: notify_channel = self.bot.get_channel(notify_channel) await self.send_embed_msg(notify_channel, title=_("Queue ended.")) if disconnect: self.bot.dispatch("red_audio_audio_disconnect", guild) await player.disconnect() if status: player_check = self.get_active_player_count() await self.update_bot_presence(*player_check) if event_type in [ lavalink.LavalinkEvents.TRACK_EXCEPTION, lavalink.LavalinkEvents.TRACK_STUCK, ]: message_channel = player.fetch("channel") while True: if current_track in player.queue: player.queue.remove(current_track) else: break if repeat: player.current = None if not guild_id: return self._error_counter.setdefault(guild_id, 0) if guild_id not in self._error_counter: self._error_counter[guild_id] = 0 early_exit = await self.increase_error_counter(player) if early_exit: self._disconnected_players[guild_id] = True self.play_lock[guild_id] = False eq = player.fetch("eq") player.queue = [] player.store("playing_song", None) if eq: await self.config.custom("EQUALIZER", guild_id).eq_bands.set(eq.bands) await player.stop() await player.disconnect() self.bot.dispatch("red_audio_audio_disconnect", guild) if message_channel: message_channel = self.bot.get_channel(message_channel) if early_exit: embed = discord.Embed( colour=await self.bot.get_embed_color(message_channel), title=_("Multiple Errors Detected"), description=_( "Closing the audio player " "due to multiple errors being detected. " "If this persists, please inform the bot owner " "as the Audio cog may be temporally unavailable."), ) await message_channel.send(embed=embed) return else: description = description or "" if event_type == lavalink.LavalinkEvents.TRACK_STUCK: embed = discord.Embed( colour=await self.bot.get_embed_color(message_channel), title=_("Track Stuck"), description="{}".format(description), ) else: embed = discord.Embed( title=_("Track Error"), colour=await self.bot.get_embed_color(message_channel), description="{}\n{}".format( extra.replace("\n", ""), description), ) await message_channel.send(embed=embed) await player.skip()