diff --git a/changelog.d/audio/3165.bugfix.1.rst b/changelog.d/audio/3165.bugfix.1.rst new file mode 100644 index 00000000000..2c29f9902ee --- /dev/null +++ b/changelog.d/audio/3165.bugfix.1.rst @@ -0,0 +1 @@ +Fixed an error that was thrown when running ``[p]audioset dj``. \ No newline at end of file diff --git a/changelog.d/audio/3165.enhance.1.rst b/changelog.d/audio/3165.enhance.1.rst new file mode 100644 index 00000000000..00771829716 --- /dev/null +++ b/changelog.d/audio/3165.enhance.1.rst @@ -0,0 +1 @@ +Better error handling the player is unable to play multiple tracks in sequence. \ No newline at end of file diff --git a/changelog.d/audio/3201.feature.1.rst b/changelog.d/audio/3201.feature.1.rst new file mode 100644 index 00000000000..7c5beb4ca35 --- /dev/null +++ b/changelog.d/audio/3201.feature.1.rst @@ -0,0 +1 @@ +``[p]remove`` command now accepts an URL or Index, if an URL is used it will remove all tracks in the queue with that URL. \ No newline at end of file diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index 00af5e97e21..27fd58d1e16 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -5,18 +5,18 @@ import heapq import json import logging -import math import random import re import time import traceback -from collections import namedtuple +from collections import namedtuple, Counter from io import StringIO from typing import List, Optional, Tuple, Union, cast import aiohttp import discord import lavalink +import math from fuzzywuzzy import process import redbot.core @@ -122,7 +122,6 @@ def __init__(self, bot): status=False, use_external_lavalink=False, restrict=True, - current_version=redbot.core.VersionInfo.from_str("3.0.0a0").to_json(), localpath=str(cog_data_path(raw_name="Audio")), **self._default_lavalink_settings, ) @@ -163,6 +162,9 @@ def __init__(self, bot): self.config.register_guild(**default_guild) self.config.register_global(**default_global) self.music_cache = MusicCache(bot, self.session, path=str(cog_data_path(raw_name="Audio"))) + self._error_counter = Counter() + self._error_timer = {} + self._disconnected_players = {} self.play_lock = {} self._manager: Optional[ServerManager] = None @@ -391,6 +393,62 @@ async def attempt_connect(self, timeout: int = 50): "tracebacks for details." ) + async def error_reset(self, player: lavalink.Player): + guild = player.channel.guild + now = time.time() + seconds_allowed = 10 + last_error = self._error_timer.setdefault(guild.id, now) + if now - seconds_allowed > last_error: + self._error_timer[guild.id] = 0 + self._error_counter[guild.id] = 0 + + async def increase_error_counter(self, player: lavalink.Player) -> bool: + guild = player.channel.guild + now = time.time() + self._error_counter[guild.id] += 1 + self._error_timer[guild.id] = now + return self._error_counter[guild.id] >= 5 + + @staticmethod + async def _players_check(): + try: + get_single_title = lavalink.active_players()[0].current.title + query = audio_dataclasses.Query.process_input(lavalink.active_players()[0].current.uri) + if get_single_title == "Unknown title": + get_single_title = lavalink.active_players()[0].current.uri + if not get_single_title.startswith("http"): + get_single_title = get_single_title.rsplit("/", 1)[-1] + elif query.is_local: + get_single_title = "{} - {}".format( + lavalink.active_players()[0].current.author, + lavalink.active_players()[0].current.title, + ) + else: + get_single_title = lavalink.active_players()[0].current.title + playing_servers = len(lavalink.active_players()) + except IndexError: + get_single_title = None + playing_servers = 0 + return get_single_title, playing_servers + + async def _status_check(self, playing_servers): + if playing_servers == 0: + await self.bot.change_presence(activity=None) + if playing_servers == 1: + single_title = await self._players_check() + await self.bot.change_presence( + activity=discord.Activity( + name=single_title[0], type=discord.ActivityType.listening + ) + ) + if playing_servers > 1: + await self.bot.change_presence( + activity=discord.Activity( + name=_("music in {} servers").format(playing_servers), + type=discord.ActivityType.playing, + ) + ) + async def event_handler( self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra ): @@ -400,46 +458,7 @@ async def event_handler( status = await self.config.status() repeat = await self.config.guild(player.channel.guild).repeat() - async def _players_check(): - try: - get_single_title = lavalink.active_players()[0].current.title - query = audio_dataclasses.Query.process_input( - lavalink.active_players()[0].current.uri - ) - if get_single_title == "Unknown title": - get_single_title = lavalink.active_players()[0].current.uri - if not get_single_title.startswith("http"): - get_single_title = get_single_title.rsplit("/", 1)[-1] - elif query.is_local: - get_single_title = "{} - {}".format( - lavalink.active_players()[0].current.author, - lavalink.active_players()[0].current.title, - ) - else: - get_single_title = lavalink.active_players()[0].current.title - playing_servers = len(lavalink.active_players()) - except IndexError: - get_single_title = None - playing_servers = 0 - return get_single_title, playing_servers - - async def _status_check(playing_servers): - if playing_servers == 0: - await self.bot.change_presence(activity=None) - if playing_servers == 1: - single_title = await _players_check() - await self.bot.change_presence( - activity=discord.Activity( - name=single_title[0], type=discord.ActivityType.listening - ) - ) - if playing_servers > 1: - await self.bot.change_presence( - activity=discord.Activity( - name=_("music in {} servers").format(playing_servers), - type=discord.ActivityType.playing, - ) - ) + await self.error_reset(player) if event_type == lavalink.LavalinkEvents.TRACK_START: self.skip_votes[player.channel.guild] = [] @@ -549,16 +568,16 @@ async def _status_check(playing_servers): player.store("notify_message", notify_message) if event_type == lavalink.LavalinkEvents.TRACK_START and status: - player_check = await _players_check() - await _status_check(player_check[1]) + player_check = await self._players_check() + await self._status_check(player_check[1]) if event_type == lavalink.LavalinkEvents.TRACK_END and status: await asyncio.sleep(1) if not player.is_playing: - player_check = await _players_check() - await _status_check(player_check[1]) + player_check = await self._players_check() + await self._status_check(player_check[1]) - if event_type == lavalink.LavalinkEvents.QUEUE_END and notify and not autoplay: + if not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and notify: notify_channel = player.fetch("channel") if notify_channel: notify_channel = self.bot.get_channel(notify_channel) @@ -568,36 +587,16 @@ async def _status_check(playing_servers): ) await notify_channel.send(embed=embed) - elif event_type == lavalink.LavalinkEvents.QUEUE_END and disconnect and not autoplay: - self.bot.dispatch("red_audio_audio_disconnect", player.channel.guild) + elif not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and disconnect: await player.disconnect() + self.bot.dispatch("red_audio_audio_disconnect", player.channel.guild) if event_type == lavalink.LavalinkEvents.QUEUE_END and status: - player_check = await _players_check() - await _status_check(player_check[1]) + player_check = await self._players_check() + await self._status_check(player_check[1]) if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION: message_channel = player.fetch("channel") - if message_channel: - message_channel = self.bot.get_channel(message_channel) - query = audio_dataclasses.Query.process_input(player.current.uri) - if player.current and query.is_local: - query = audio_dataclasses.Query.process_input(player.current.uri) - if player.current.title == "Unknown title": - description = "{}".format(query.track.to_string_hidden()) - else: - song = bold("{} - {}").format(player.current.author, player.current.title) - description = "{}\n{}".format(song, query.track.to_string_hidden()) - else: - description = bold("[{}]({})").format(player.current.title, player.current.uri) - - embed = discord.Embed( - colour=(await self.bot.get_embed_color(message_channel)), - title=_("Track Error"), - description="{}\n{}".format(extra, description), - ) - embed.set_footer(text=_("Skipping...")) - await message_channel.send(embed=embed) while True: if player.current in player.queue: player.queue.remove(player.current) @@ -605,7 +604,61 @@ async def _status_check(playing_servers): break if repeat: player.current = None - await player.skip() + self._error_counter.setdefault(player.channel.guild.id, 0) + if player.channel.guild.id not in self._error_counter: + self._error_counter[player.channel.guild.id] = 0 + early_exit = await self.increase_error_counter(player) + if early_exit: + self._disconnected_players[player.channel.guild.id] = True + self.play_lock[player.channel.guild.id] = False + eq = player.fetch("eq") + player.queue = [] + player.store("playing_song", None) + if eq: + await self.config.custom("EQUALIZER", player.channel.guild.id).eq_bands.set( + eq.bands + ) + await player.stop() + await player.disconnect() + self.bot.dispatch("red_audio_audio_disconnect", player.channel.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) + else: + query = audio_dataclasses.Query.process_input(player.current.uri) + if player.current and query.is_local: + query = audio_dataclasses.Query.process_input(player.current.uri) + if player.current.title == "Unknown title": + description = "{}".format(query.track.to_string_hidden()) + else: + song = bold("{} - {}").format( + player.current.author, player.current.title + ) + description = "{}\n{}".format(song, query.track.to_string_hidden()) + else: + description = bold("[{}]({})").format( + player.current.title, player.current.uri + ) + + embed = discord.Embed( + colour=(await self.bot.get_embed_color(message_channel)), + title=_("Track Error"), + description="{}\n{}".format(extra, description), + ) + embed.set_footer(text=_("Skipping...")) + await message_channel.send(embed=embed) + await player.skip() async def play_query( self, @@ -1017,7 +1070,7 @@ async def dj(self, ctx: commands.Context): try: pred = MessagePredicate.valid_role(ctx) await ctx.bot.wait_for("message", timeout=15.0, check=pred) - await ctx.invoke(self.role, pred.result) + await ctx.invoke(self.role, role_name=pred.result) except asyncio.TimeoutError: return await self._embed_msg(ctx, _("Response timed out, try again later.")) @@ -6049,7 +6102,6 @@ async def _build_search_page(ctx: commands.Context, tracks, page_num): search_track_num, track.title, track.uri ) except AttributeError: - # query = Query.process_input(track) track = audio_dataclasses.Query.process_input(track) if track.is_local and command != "search": search_list += "`{}.` **{}**\n".format( @@ -6299,7 +6351,8 @@ async def _can_instaskip(self, ctx: commands.Context, member: discord.Member): return False - async def _is_alone(self, ctx: commands.Context): + @staticmethod + async def _is_alone(ctx: commands.Context): channel_members = rgetattr(ctx, "guild.me.voice.channel.members", []) nonbots = sum(m.id != ctx.author.id for m in channel_members if not m.bot) return nonbots < 1