Merge V3/feature/audio into V3/develop (a.k.a. audio refactor) (#3459)

This commit is contained in:
Draper
2020-05-20 21:30:06 +01:00
committed by GitHub
parent ef76affd77
commit 8fa47cb789
53 changed files with 12372 additions and 10144 deletions

View File

@@ -0,0 +1,13 @@
import logging
from ..cog_utils import CompositeMetaClass
from .cog import AudioEvents
from .dpy import DpyEvents
from .lavalink import LavalinkEvents
from .red import RedEvents
log = logging.getLogger("red.cogs.Audio.cog.Events")
class Events(AudioEvents, DpyEvents, LavalinkEvents, RedEvents, metaclass=CompositeMetaClass):
"""Class joining all event subclasses"""

View File

@@ -0,0 +1,147 @@
import asyncio
import datetime
import logging
import time
from typing import Optional
import discord
import lavalink
from redbot.core import commands
from ...apis.playlist_interface import Playlist, delete_playlist, get_playlist
from ...audio_logging import debug_exc_log
from ...utils import PlaylistScope
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Events.audio")
class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
@commands.Cog.listener()
async def on_red_audio_track_start(
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
):
if not (track and guild):
return
track_identifier = track.track_identifier
if self.playlist_api is not None:
daily_cache = self._daily_playlist_cache.setdefault(
guild.id, await self.config.guild(guild).daily_playlists()
)
global_daily_playlists = self._daily_global_playlist_cache.setdefault(
self.bot.user.id, await self.config.daily_playlists()
)
today = datetime.date.today()
midnight = datetime.datetime.combine(today, datetime.datetime.min.time())
today_id = int(time.mktime(today.timetuple()))
track = self.track_to_json(track)
if daily_cache:
name = f"Daily playlist - {today}"
playlist: Optional[Playlist]
try:
playlist = await get_playlist(
playlist_api=self.playlist_api,
playlist_number=today_id,
scope=PlaylistScope.GUILD.value,
bot=self.bot,
guild=guild,
author=self.bot.user,
)
except RuntimeError:
playlist = None
if playlist:
tracks = playlist.tracks
tracks.append(track)
await playlist.edit({"tracks": tracks})
else:
playlist = Playlist(
bot=self.bot,
scope=PlaylistScope.GUILD.value,
author=self.bot.user.id,
playlist_id=today_id,
name=name,
playlist_url=None,
tracks=[track],
guild=guild,
playlist_api=self.playlist_api,
)
await playlist.save()
if global_daily_playlists:
global_name = f"Global Daily playlist - {today}"
try:
playlist = await get_playlist(
playlist_number=today_id,
scope=PlaylistScope.GLOBAL.value,
bot=self.bot,
guild=guild,
author=self.bot.user,
playlist_api=self.playlist_api,
)
except RuntimeError:
playlist = None
if playlist:
tracks = playlist.tracks
tracks.append(track)
await playlist.edit({"tracks": tracks})
else:
playlist = Playlist(
bot=self.bot,
scope=PlaylistScope.GLOBAL.value,
author=self.bot.user.id,
playlist_id=today_id,
name=global_name,
playlist_url=None,
tracks=[track],
guild=guild,
playlist_api=self.playlist_api,
)
await playlist.save()
too_old = midnight - datetime.timedelta(days=8)
too_old_id = int(time.mktime(too_old.timetuple()))
try:
await delete_playlist(
scope=PlaylistScope.GUILD.value,
playlist_id=too_old_id,
guild=guild,
author=self.bot.user,
playlist_api=self.playlist_api,
bot=self.bot,
)
except Exception as err:
debug_exc_log(log, err, f"Failed to delete daily playlist ID: {too_old_id}")
try:
await delete_playlist(
scope=PlaylistScope.GLOBAL.value,
playlist_id=too_old_id,
guild=guild,
author=self.bot.user,
playlist_api=self.playlist_api,
bot=self.bot,
)
except Exception as err:
debug_exc_log(log, err, f"Failed to delete global daily playlist ID: {too_old_id}")
@commands.Cog.listener()
async def on_red_audio_queue_end(
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
):
if not (track and guild):
return
if self.api_interface is not None and self.playlist_api is not None:
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5)
await self.playlist_api.delete_scheduled()
@commands.Cog.listener()
async def on_red_audio_track_end(
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
):
if not (track and guild):
return
if self.api_interface is not None and self.playlist_api is not None:
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5)
await self.playlist_api.delete_scheduled()

View File

@@ -0,0 +1,184 @@
import asyncio
import logging
import re
from pathlib import Path
from typing import Final, Pattern
import discord
import lavalink
from aiohttp import ClientConnectorError
from redbot.core import commands
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
from ...audio_logging import debug_exc_log
from ...errors import TrackEnqueueError
log = logging.getLogger("red.cogs.Audio.cog.Events.dpy")
RE_CONVERSION: Final[Pattern] = re.compile('Converting to "(.*)" failed for parameter "(.*)".')
class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
async def cog_before_invoke(self, ctx: commands.Context) -> None:
await self.cog_ready_event.wait()
# check for unsupported arch
# Check on this needs refactoring at a later date
# so that we have a better way to handle the tasks
if self.command_llsetup in [ctx.command, ctx.command.root_parent]:
pass
elif self.lavalink_connect_task and self.lavalink_connect_task.cancelled():
await ctx.send(
_(
"You have attempted to run Audio's Lavalink server on an unsupported"
" architecture. Only settings related commands will be available."
)
)
raise RuntimeError(
"Not running audio command due to invalid machine architecture for Lavalink."
)
# with contextlib.suppress(Exception):
# player = lavalink.get_player(ctx.guild.id)
# notify_channel = player.fetch("channel")
# if not notify_channel:
# player.store("channel", ctx.channel.id)
dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
)
self._daily_playlist_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).daily_playlists()
)
self._daily_global_playlist_cache.setdefault(
self.bot.user.id, await self.config.daily_playlists()
)
if self.local_folder_current_path is None:
self.local_folder_current_path = Path(await self.config.localpath())
if dj_enabled:
dj_role = self._dj_role_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_role()
)
dj_role_obj = ctx.guild.get_role(dj_role)
if not dj_role_obj:
await self.config.guild(ctx.guild).dj_enabled.set(None)
self._dj_status_cache[ctx.guild.id] = None
await self.config.guild(ctx.guild).dj_role.set(None)
self._dj_role_cache[ctx.guild.id] = None
await self.send_embed_msg(ctx, title=_("No DJ role found. Disabling DJ mode."))
async def cog_after_invoke(self, ctx: commands.Context) -> None:
await self.maybe_run_pending_db_tasks(ctx)
async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
error = getattr(error, "original", error)
handled = False
if isinstance(error, commands.ArgParserFailure):
handled = True
msg = _("`{user_input}` is not a valid value for `{command}`").format(
user_input=error.user_input, command=error.cmd,
)
if error.custom_help_msg:
msg += f"\n{error.custom_help_msg}"
await self.send_embed_msg(
ctx, title=_("Unable To Parse Argument"), description=msg, error=True,
)
if error.send_cmd_help:
await ctx.send_help()
elif isinstance(error, commands.ConversionFailure):
handled = True
if error.args:
if match := RE_CONVERSION.search(error.args[0]):
await self.send_embed_msg(
ctx,
title=_("Invalid Argument"),
description=_(
"The argument you gave for `{}` is not valid: I was expecting a `{}`."
).format(match.group(2), match.group(1)),
error=True,
)
else:
await self.send_embed_msg(
ctx, title=_("Invalid Argument"), description=error.args[0], error=True,
)
else:
await ctx.send_help()
elif isinstance(error, (IndexError, ClientConnectorError)) and any(
e in str(error).lower() for e in ["no nodes found.", "cannot connect to host"]
):
handled = True
await self.send_embed_msg(
ctx,
title=_("Invalid Environment"),
description=_("Connection to Lavalink has been lost."),
error=True,
)
debug_exc_log(log, error, "This is a handled error")
elif isinstance(error, KeyError) and "such player for that guild" in str(error):
handled = True
await self.send_embed_msg(
ctx,
title=_("No Player Available"),
description=_("The bot is not connected to a voice channel."),
error=True,
)
debug_exc_log(log, error, "This is a handled error")
elif isinstance(error, (TrackEnqueueError, asyncio.exceptions.TimeoutError)):
handled = True
await self.send_embed_msg(
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment,"
"try again in a few minutes."
),
error=True,
)
debug_exc_log(log, error, "This is a handled error")
if not isinstance(
error,
(
commands.CheckFailure,
commands.UserInputError,
commands.DisabledCommand,
commands.CommandOnCooldown,
commands.MaxConcurrencyReached,
),
):
self.update_player_lock(ctx, False)
if self.api_interface is not None:
await self.api_interface.run_tasks(ctx)
if not handled:
await self.bot.on_command_error(ctx, error, unhandled_by_cog=True)
def cog_unload(self) -> None:
if not self.cog_cleaned_up:
self.bot.dispatch("red_audio_unload", self)
self.session.detach()
self.bot.loop.create_task(self._close_database())
if self.player_automated_timer_task:
self.player_automated_timer_task.cancel()
if self.lavalink_connect_task:
self.lavalink_connect_task.cancel()
if self.cog_init_task:
self.cog_init_task.cancel()
lavalink.unregister_event_listener(self.lavalink_event_handler)
self.bot.loop.create_task(lavalink.close())
if self.player_manager is not None:
self.bot.loop.create_task(self.player_manager.shutdown())
self.cog_cleaned_up = True
@commands.Cog.listener()
async def on_voice_state_update(
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
) -> None:
await self.cog_ready_event.wait()
if after.channel != before.channel:
try:
self.skip_votes[before.channel.guild].remove(member.id)
except (ValueError, KeyError, AttributeError):
pass

View File

@@ -0,0 +1,192 @@
import asyncio
import contextlib
import logging
import discord
import lavalink
from ...errors import DatabaseError
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
log = logging.getLogger("red.cogs.Audio.cog.Events.lavalink")
class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
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()

View File

@@ -0,0 +1,21 @@
import logging
from typing import Mapping
from redbot.core import commands
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Events.red")
class RedEvents(MixinMeta, metaclass=CompositeMetaClass):
@commands.Cog.listener()
async def on_red_api_tokens_update(
self, service_name: str, api_tokens: Mapping[str, str]
) -> None:
if service_name == "youtube":
self.api_interface.youtube_api.update_token(api_tokens)
elif service_name == "spotify":
self.api_interface.spotify_api.update_token(api_tokens)
elif service_name == "audiodb":
self.api_interface.global_cache_api.update_token(api_tokens)