I promise I'm not doing this on purpose (#4565)

* Prep for 0.7.2

* So What di i do here? I done Magic, magic only found in the tales of old.

* turns out formatting is something important

* fixes

* improved Error handling when Global API is enabled

* further improve resuming logic

* more of dat dark voodoo blood magic

* major tweaks to auto restore when auto play is enabled 👀

* fix duplicated "Auto play stated." message + Auto play restart :feelsgoodman:

* missed these

* fix the new duplicated fucking message bitch.

* Let discord handle player reconnects

* eh

* `Requires force install`, utilize new Exponential Backoff object on player and safer reconnect logic, emulating d.py and WL.

* hmmmmm gotta monitor

* mother fucking brackets

* Why didnt i consider this the first time?????????????

* new error code to handle?

* soooooooooooooooo these are import so why arent we ensuring they are set.

* improved logging

* improved logging

* aaaaaaaaaaaaaaa

* We need our own error and special handling to not conflict with dpy

* (Last Known Bug) Fix the infinite loop of 4006 that sometimes could happen as an edge case after a successful resume.

* This will require a force reinstall to install `RLL 0.8.0`, this properly fixes the bug mentioned on the previous commit.

* address "Localtrack names/paths need to be escaped." comment

* address Fixators crash mentioned in #AT

* style

* fix preda's crash mentioned in PR

* add a thing here add a thing there add a thing everywhere

* style

* fixes here, fixes there, and backbone for curated playlist.

* bypass aiohttp and githubs and cloudflare and yo mammas cache

* I propose the new style is no style.

* allow curated playlist to be updated it `[p]playlist update` and show the diff

* fix `[p]summon` not resuming playback until next track.

* Hopefully handle predas rate limits.

* what else did i break now

* Update Lavalink.jar build

* lets try this

* reset the queue

* Bring Edge commits over fix a bunch of shiz again

* Bring Edge commits over fix a bunch of shiz again

* Handle 4014 OPs, Change `skip_votes` key to be an int rather than guild object

* aaaaaaaaaaaaaaa im dumb

* ...

* Simplify some shiz + use a set instead of a list for votes.

Co-authored-by: aikaterna <20862007+aikaterna@users.noreply.github.com>
This commit is contained in:
Draper 2021-04-05 20:02:24 +01:00 committed by GitHub
parent 1199f160d0
commit b7d8b0552e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 890 additions and 113 deletions

View File

@ -211,6 +211,7 @@ class AudioAPIInterface:
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
youtube_api_error = None
global_api = self.cog.global_api_user.get("can_read")
async for track in AsyncIter(tracks):
if isinstance(track, str):
break
@ -267,13 +268,15 @@ class AudioAPIInterface:
track_count += 1
if notifier is not None and ((track_count % 2 == 0) or (track_count == total_tracks)):
await notifier.notify_user(current=track_count, total=total_tracks, key="youtube")
if notifier is not None and youtube_api_error:
if notifier is not None and (youtube_api_error and not global_api):
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Failing to get tracks, skipping remaining."),
)
await notifier.update_embed(error_embed)
break
elif notifier is not None and (youtube_api_error and global_api):
continue
if CacheLevel.set_spotify().is_subset(current_cache_level):
task = ("insert", ("spotify", database_entries))
self.append_task(ctx, *task)
@ -447,11 +450,12 @@ class AudioAPIInterface:
List of Youtube URLs.
"""
await self.global_cache_api._get_api_key()
globaldb_toggle = await self.config.global_db_enabled()
globaldb_toggle = self.cog.global_api_user.get("can_read")
global_entry = globaldb_toggle and query_global
track_list: List = []
has_not_allowed = False
youtube_api_error = None
skip_youtube_api = False
try:
current_cache_level = CacheLevel(await self.config.cache_level())
guild_data = await self.config.guild(ctx.guild).all()
@ -518,7 +522,7 @@ class AudioAPIInterface:
llresponse["loadType"] = "V2_COMPAT"
llresponse = LoadResult(llresponse)
val = llresponse or None
if val is None:
if val is None and not skip_youtube_api:
try:
val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
@ -526,6 +530,7 @@ class AudioAPIInterface:
except YouTubeApiError as err:
val = None
youtube_api_error = err.message
skip_youtube_api = True
if not youtube_api_error:
if youtube_cache and val and llresponse is None:
task = ("update", ("youtube", {"track": track_info}))
@ -589,7 +594,9 @@ class AudioAPIInterface:
seconds=seconds,
)
if youtube_api_error or consecutive_fails >= (20 if global_entry else 10):
if (youtube_api_error and not global_entry) 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."),
@ -793,7 +800,7 @@ class AudioAPIInterface:
val = None
query = Query.process_input(query, self.cog.local_folder_current_path)
query_string = str(query)
globaldb_toggle = await self.config.global_db_enabled()
globaldb_toggle = self.cog.global_api_user.get("can_read")
valid_global_entry = False
results = None
called_api = False
@ -925,6 +932,7 @@ class AudioAPIInterface:
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)
notify_channel_id = player.fetch("channel")
playlist = None
tracks = None
if autoplaylist["enabled"]:
@ -973,7 +981,7 @@ class AudioAPIInterface:
and not query.local_track_path.exists()
):
continue
notify_channel = self.bot.get_channel(player.fetch("channel"))
notify_channel = self.bot.get_channel(notify_channel_id)
if not await self.cog.is_query_allowed(
self.config,
notify_channel,
@ -997,8 +1005,20 @@ class AudioAPIInterface:
)
player.add(player.channel.guild.me, track)
self.bot.dispatch(
"red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me
"red_audio_track_auto_play",
player.channel.guild,
track,
player.channel.guild.me,
player,
)
if notify_channel_id:
await self.config.guild_from_id(
guild_id=player.channel.guild.id
).currently_auto_playing_in.set([notify_channel_id, player.channel.id])
else:
await self.config.guild_from_id(
guild_id=player.channel.guild.id
).currently_auto_playing_in.set([])
if not player.current:
await player.play()

View File

@ -235,7 +235,7 @@ class PlaylistCompat23:
Trying to access the User scope without an user id.
"""
guild = data.get("guild") or kwargs.get("guild")
author: int = data.get("author") or 0
author: int = data.get("author") or kwargs.get("author") or 0
playlist_id = data.get("id") or playlist_number
name = data.get("name", "Unnamed")
playlist_url = data.get("playlist_url", None)

View File

@ -2,7 +2,7 @@ import asyncio
import datetime
import json
from collections import Counter
from collections import Counter, defaultdict
from pathlib import Path
from typing import Mapping
@ -77,6 +77,9 @@ class Audio(
self.session = aiohttp.ClientSession(json_serialize=json.dumps)
self.cog_ready_event = asyncio.Event()
self._ws_resume = defaultdict(asyncio.Event)
self._ws_op_codes = defaultdict(asyncio.LifoQueue)
self.cog_init_task = None
self.global_api_user = {
"fetched": False,
@ -85,10 +88,12 @@ class Audio(
"can_delete": False,
}
self._ll_guild_updates = set()
self._diconnected_shard = set()
self._last_ll_update = datetime.datetime.now(datetime.timezone.utc)
default_global = dict(
schema_version=1,
bundled_playlist_version=0,
owner_notification=0,
cache_level=0,
cache_age=365,
@ -107,8 +112,14 @@ class Audio(
default_guild = dict(
auto_play=False,
currently_auto_playing_in=None,
auto_deafen=True,
autoplaylist={"enabled": False, "id": None, "name": None, "scope": None},
autoplaylist=dict(
enabled=True,
id=42069,
name="Aikaterna's curated tracks",
scope=PlaylistScope.GLOBAL.value,
),
persist_queue=True,
disconnect=False,
dj_enabled=False,

View File

@ -4,7 +4,7 @@ import asyncio
import datetime
from abc import ABC, abstractmethod
from collections import Counter
from collections import Counter, defaultdict
from pathlib import Path
from typing import Set, TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple, Union
@ -41,7 +41,7 @@ class MixinMeta(ABC):
db_conn: Optional[APSWConnectionWrapper]
session: aiohttp.ClientSession
skip_votes: MutableMapping[int, List[int]]
skip_votes: MutableMapping[int, Set[int]]
play_lock: MutableMapping[int, bool]
_daily_playlist_cache: MutableMapping[int, bool]
_daily_global_playlist_cache: MutableMapping[int, bool]
@ -62,12 +62,14 @@ class MixinMeta(ABC):
player_automated_timer_task: Optional[asyncio.Task]
cog_init_task: Optional[asyncio.Task]
cog_ready_event: asyncio.Event
_ws_resume: defaultdict[Any, asyncio.Event]
_ws_op_codes: defaultdict[int, asyncio.LifoQueue]
_default_lavalink_settings: Mapping
permission_cache = discord.Permissions
_last_ll_update: datetime.datetime
_ll_guild_updates: Set[int]
_diconnected_shard: Set[int]
@abstractmethod
async def command_llsetup(self, ctx: commands.Context):
@ -306,6 +308,14 @@ class MixinMeta(ABC):
async def _playlist_check(self, ctx: commands.Context) -> bool:
raise NotImplementedError()
@abstractmethod
async def _build_bundled_playlist(self, forced: bool = None) -> None:
raise NotImplementedError()
@abstractmethod
def decode_track(self, track: str, decode_errors: str = "") -> MutableMapping:
raise NotImplementedError()
@abstractmethod
async def can_manage_playlist(
self, scope: str, playlist: "Playlist", ctx: commands.Context, user, guild

View File

@ -1,12 +1,15 @@
from abc import ABC
from typing import Final
from base64 import b64decode
from io import BytesIO
import struct
from redbot import VersionInfo
from redbot.core import commands
from ..converters import get_lazy_converter, get_playlist_converter
__version__ = VersionInfo.from_json({"major": 2, "minor": 3, "micro": 0, "releaselevel": "final"})
__version__ = VersionInfo.from_json({"major": 2, "minor": 4, "micro": 0, "releaselevel": "final"})
__author__ = ["aikaterna", "Draper"]
@ -57,3 +60,90 @@ class CompositeMetaClass(type(commands.Cog), type(ABC)):
"""
pass
# Both DataReader and DataWriter are taken from https://github.com/Devoxin/Lavalink.py/blob/master/lavalink/datarw.py
# These are licenced under MIT, Thanks Devoxin for putting these together!
# The license can be found in https://github.com/Devoxin/Lavalink.py/blob/master/LICENSE
class DataReader:
def __init__(self, ts):
self._buf = BytesIO(b64decode(ts))
def _read(self, n):
return self._buf.read(n)
def read_byte(self):
return self._read(1)
def read_boolean(self):
(result,) = struct.unpack("B", self.read_byte())
return result != 0
def read_unsigned_short(self):
(result,) = struct.unpack(">H", self._read(2))
return result
def read_int(self):
(result,) = struct.unpack(">i", self._read(4))
return result
def read_long(self):
(result,) = struct.unpack(">Q", self._read(8))
return result
def read_utf(self):
text_length = self.read_unsigned_short()
return self._read(text_length)
class DataWriter:
def __init__(self):
self._buf = BytesIO()
def _write(self, data):
self._buf.write(data)
def write_byte(self, byte):
self._buf.write(byte)
def write_boolean(self, b):
enc = struct.pack("B", 1 if b else 0)
self.write_byte(enc)
def write_unsigned_short(self, s):
enc = struct.pack(">H", s)
self._write(enc)
def write_int(self, i):
enc = struct.pack(">i", i)
self._write(enc)
def write_long(self, l):
enc = struct.pack(">Q", l)
self._write(enc)
def write_utf(self, s):
utf = s.encode("utf8")
byte_len = len(utf)
if byte_len > 65535:
raise OverflowError("UTF string may not exceed 65535 bytes!")
self.write_unsigned_short(byte_len)
self._write(utf)
def finish(self):
with BytesIO() as track_buf:
byte_len = self._buf.getbuffer().nbytes
flags = byte_len | (1 << 30)
enc_flags = struct.pack(">i", flags)
track_buf.write(enc_flags)
self._buf.seek(0)
track_buf.write(self._buf.read())
self._buf.close()
track_buf.seek(0)
return track_buf.read()

View File

@ -558,7 +558,13 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
@command_audioset_autoplay.command(name="reset")
async def command_audioset_autoplay_reset(self, ctx: commands.Context):
"""Resets auto-play to the default playlist."""
playlist_data = dict(enabled=False, id=None, name=None, scope=None)
playlist_data = dict(
enabled=True,
id=42069,
name="Aikaterna's curated tracks",
scope=PlaylistScope.GLOBAL.value,
)
await self.config.guild(ctx.guild).autoplaylist.set(playlist_data)
return await self.send_embed_msg(
ctx,

View File

@ -68,10 +68,14 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
eq = player.fetch("eq")
player.queue = []
player.store("playing_song", None)
player.store("autoplay_notified", False)
if eq:
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
await player.stop()
await player.disconnect()
await self.config.guild_from_id(guild_id=ctx.guild.id).currently_auto_playing_in.set(
[]
)
self._ll_guild_updates.discard(ctx.guild.id)
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@ -91,6 +95,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
}
expected = tuple(emoji.values())
player = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if player.current:
arrow = await self.draw_time(ctx)
pos = self.format_time(player.position)
@ -212,7 +218,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Manage Tracks"),
description=_("You need the DJ role to pause or resume tracks."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if not player.current:
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
description = await self.get_track_description(
@ -266,7 +273,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
"to enqueue the previous song tracks."
),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if player.fetch("prev_song") is None:
return await self.send_embed_msg(
ctx, title=_("Unable To Play Tracks"), description=_("No previous track.")
@ -332,7 +340,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Seek Tracks"),
description=_("You need the DJ role or be the track requester to use seek."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if player.current:
if player.current.is_stream:
return await self.send_embed_msg(
@ -405,6 +414,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Toggle Shuffle"),
description=_("You must be in the voice channel to toggle shuffle."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
shuffle = await self.config.guild(ctx.guild).shuffle()
await self.config.guild(ctx.guild).shuffle.set(not shuffle)
@ -448,6 +459,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Toggle Shuffle"),
description=_("You must be in the voice channel to toggle shuffle."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
bumped = await self.config.guild(ctx.guild).shuffle_bumped()
await self.config.guild(ctx.guild).shuffle_bumped.set(not bumped)
@ -504,7 +517,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Skip Tracks"),
description=_("You can only skip the current track."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if vote_enabled:
if not can_skip:
if skip_to_track is not None:
@ -516,10 +530,10 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
),
)
if ctx.author.id in self.skip_votes[ctx.guild.id]:
self.skip_votes[ctx.guild.id].remove(ctx.author.id)
self.skip_votes[ctx.guild.id].discard(ctx.author.id)
reply = _("I removed your vote to skip.")
else:
self.skip_votes[ctx.guild.id].append(ctx.author.id)
self.skip_votes[ctx.guild.id].add(ctx.author.id)
reply = _("You voted to skip.")
num_votes = len(self.skip_votes[ctx.guild.id])
@ -532,7 +546,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
vote = int(100 * num_votes / num_members)
percent = await self.config.guild(ctx.guild).vote_percent()
if vote >= percent:
self.skip_votes[ctx.guild.id] = []
self.skip_votes[ctx.guild.id] = set()
await self.send_embed_msg(ctx, title=_("Vote threshold met."))
return await self._skip_action(ctx)
else:
@ -583,6 +597,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Stop Player"),
description=_("You need the DJ role to stop the music."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if (
player.is_playing
or (not player.is_playing and player.paused)
@ -597,7 +613,11 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
player.store("prev_requester", None)
player.store("prev_song", None)
player.store("requester", None)
player.store("autoplay_notified", False)
await player.stop()
await self.config.guild_from_id(guild_id=ctx.guild.id).currently_auto_playing_in.set(
[]
)
await self.send_embed_msg(ctx, title=_("Stopping..."))
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@ -642,17 +662,28 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
description=_("I don't have permission to connect to your channel."),
)
if not self._player_check(ctx):
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
else:
player = lavalink.get_player(ctx.guild.id)
if ctx.author.voice.channel == player.channel:
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if (
ctx.author.voice.channel == player.channel
and ctx.guild.me in ctx.author.voice.channel.members
):
ctx.command.reset_cooldown(ctx)
return
await player.move_to(ctx.author.voice.channel)
await self.self_deafen(player)
await player.move_to(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
except AttributeError:
ctx.command.reset_cooldown(ctx)
return await self.send_embed_msg(
@ -693,23 +724,33 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Change Volume"),
description=_("You must be in the voice channel to change the volume."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if dj_enabled and not can_skip and not await self._has_dj_role(ctx, ctx.author):
return await self.send_embed_msg(
ctx,
title=_("Unable To Change Volume"),
description=_("You need the DJ role to change the volume."),
)
if vol < 0:
vol = 0
if vol > 150:
vol = 150
await self.config.guild(ctx.guild).volume.set(vol)
if self._player_check(ctx):
await lavalink.get_player(ctx.guild.id).set_volume(vol)
player = lavalink.get_player(ctx.guild.id)
await player.set_volume(vol)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
else:
await self.config.guild(ctx.guild).volume.set(vol)
if self._player_check(ctx):
await lavalink.get_player(ctx.guild.id).set_volume(vol)
player = lavalink.get_player(ctx.guild.id)
await player.set_volume(vol)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
embed = discord.Embed(title=_("Volume:"), description=str(vol) + "%")
if not self._player_check(ctx):
embed.set_footer(text=_("Nothing playing."))
@ -741,6 +782,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Toggle Repeat"),
description=_("You must be in the voice channel to toggle repeat."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
autoplay = await self.config.guild(ctx.guild).auto_play()
repeat = await self.config.guild(ctx.guild).repeat()
@ -784,6 +827,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Modify Queue"),
description=_("You must be in the voice channel to manage the queue."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
if isinstance(index_or_url, int):
if index_or_url > len(player.queue) or index_or_url < 1:
return await self.send_embed_msg(
@ -864,7 +909,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Bump Track"),
description=_("Song number must be greater than 1 and within the queue limit."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
bump_index = index - 1
bump_song = player.queue[bump_index]
bump_song.extras["bumped"] = True

View File

@ -173,7 +173,8 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Load Preset"),
description=_("You need the DJ role to load equalizer presets."),
)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq_values)
await self._eq_check(ctx, player)
eq = player.fetch("eq", Equalizer())
@ -202,6 +203,8 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
description=_("You need the DJ role to reset the equalizer."),
)
player = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
eq = player.fetch("eq", Equalizer())
for band in range(eq.band_count):
@ -284,6 +287,8 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
return await eq_exists_msg.edit(embed=embed2)
player = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
eq = player.fetch("eq", Equalizer())
to_append = {eq_preset: {"author": ctx.author.id, "bands": eq.bands}}
new_eq_presets = {**eq_presets, **to_append}
@ -325,6 +330,8 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
)
player = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
band_names = [
"25",
"40",

View File

@ -223,7 +223,7 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
msg = "----" + _("Connection Settings") + "---- \n"
msg += _("Host: [{host}]\n").format(host=host)
msg += _("WS Port: [{port}]\n").format(port=ws_port)
if ws_port != rest_port:
if ws_port != rest_port and rest_port != 2333:
msg += _("Rest Port: [{port}]\n").format(port=rest_port)
msg += _("Password: [{password}]\n").format(password=password)
try:

View File

@ -78,10 +78,14 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(
ctx,
@ -185,10 +189,14 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(
ctx,
@ -450,10 +458,14 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(
ctx,
@ -566,10 +578,14 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(
ctx,
@ -626,10 +642,8 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not guild_data["auto_play"]:
await ctx.invoke(self.command_audioset_autoplay_toggle)
if not guild_data["notify"] and (
(player.current and not player.current.extras.get("autoplay")) or not player.current
):
await self.send_embed_msg(ctx, title=_("Auto play started."))
if not guild_data["notify"] and not player.fetch("autoplay_notified", False):
pass
elif player.current:
await self.send_embed_msg(ctx, title=_("Adding a track to queue."))
@ -692,10 +706,14 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Search For Tracks"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(
ctx,

View File

@ -1678,7 +1678,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
try:
if not await self.can_manage_playlist(scope, playlist, ctx, author, guild):
return
if playlist.url:
if playlist.url or playlist.id == 42069:
player = lavalink.get_player(ctx.guild.id)
added, removed, playlist = await self._maybe_update_playlist(
ctx, player, playlist

View File

@ -338,10 +338,14 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Shuffle Queue"),
description=_("I don't have permission to connect to your channel."),
)
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
ctx.command.reset_cooldown(ctx)
return await self.send_embed_msg(

View File

@ -32,8 +32,10 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
if await self.bot.cog_disabled_in_guild(self, guild):
player = lavalink.get_player(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([])
return
track_identifier = track.track_identifier
@ -157,7 +159,9 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.api_interface.persistent_queue_api.delete_scheduled()
@commands.Cog.listener()
async def on_red_audio_track_enqueue(self, guild: discord.Guild, track, requester):
async def on_red_audio_track_enqueue(
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
):
if not (track and guild):
return
persist_cache = self._persist_queue_cache.setdefault(
@ -181,3 +185,28 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.api_interface.persistent_queue_api.drop(guild.id)
await asyncio.sleep(5)
await self.api_interface.persistent_queue_api.delete_scheduled()
@commands.Cog.listener()
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)

View File

@ -244,7 +244,7 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
lavalink.unregister_event_listener(self.lavalink_event_handler)
lavalink.unregister_update_listener(self.lavalink_update_handler)
self.bot.loop.create_task(lavalink.close())
self.bot.loop.create_task(lavalink.close(self.bot))
if self.player_manager is not None:
self.bot.loop.create_task(self.player_manager.shutdown())
@ -259,12 +259,31 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.cog_ready_event.wait()
if after.channel != before.channel:
try:
self.skip_votes[before.channel.guild.id].remove(member.id)
self.skip_votes[before.channel.guild.id].discard(member.id)
except (ValueError, KeyError, AttributeError):
pass
# if (
# member == member.guild.me
# and before.channel
# and after.channel
# and after.channel.id != before.channel.id
# ):
# try:
# player = lavalink.get_player(member.guild.id)
# if player.is_playing:
# await player.resume(player.current, start=player.position, replace=False)
# log.debug("Bot changed channel - Resume playback")
# except:
# log.debug("Bot changed channel - Unable to resume playback")
channel = self.rgetattr(member, "voice.channel", None)
bot_voice_state = self.rgetattr(member, "guild.me.voice.self_deaf", None)
if channel and bot_voice_state is False:
if (
channel
and bot_voice_state is False
and await self.config.guild(member.guild).auto_deafen()
):
try:
player = lavalink.get_player(channel.guild.id)
except (KeyError, AttributeError):
@ -272,3 +291,15 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
else:
if player.channel.id == channel.id:
await self.self_deafen(player)
@commands.Cog.listener()
async def on_shard_disconnect(self, shard_id):
self._diconnected_shard.add(shard_id)
@commands.Cog.listener()
async def on_shard_ready(self, shard_id):
self._diconnected_shard.discard(shard_id)
@commands.Cog.listener()
async def on_shard_resumed(self, shard_id):
self._diconnected_shard.discard(shard_id)

View File

@ -3,9 +3,12 @@ import contextlib
import datetime
import logging
from pathlib import Path
from typing import Dict
import discord
import lavalink
from discord.backoff import ExponentialBackoff
from discord.gateway import DiscordWebSocket
from redbot.core.i18n import Translator, set_contextual_locales_from_guild
from ...errors import DatabaseError, TrackEnqueueError
@ -13,6 +16,9 @@ from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Events.lavalink")
ws_audio_log = logging.getLogger("red.Audio.WS.Audio")
ws_audio_log.setLevel(logging.WARNING)
_ = Translator("Audio", Path(__file__))
@ -28,27 +34,62 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
) -> None:
current_track = player.current
current_channel = player.channel
if not current_channel:
return
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.FORCED_DISCONNECT:
self.bot.dispatch("red_audio_audio_disconnect", guild)
await self.config.guild_from_id(guild_id=guild_id).currently_auto_playing_in.set([])
self._ll_guild_updates.discard(guild.id)
return
if event_type == lavalink.LavalinkEvents.WEBSOCKET_CLOSED:
deafen = guild_data["auto_deafen"]
event_channel_id = extra.get("channelID")
_error_code = extra.get("code")
if _error_code in [1000] or not guild:
if _error_code == 1000:
await player.resume(player.current, start=player.position, replace=False)
return
await self._ws_op_codes[guild_id].put((event_channel_id, _error_code))
try:
if guild_id not in self._ws_resume:
self._ws_resume[guild_id].set()
await self._websocket_closed_handler(
guild=guild, player=player, extra=extra, deafen=deafen, disconnect=disconnect
)
except Exception:
log.exception(
f"Error in WEBSOCKET_CLOSED handling for guild: {player.channel.guild.id}"
)
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_extras = self.rgetattr(current_track, "extras", {})
current_id = self.rgetattr(current_track, "_info", {}).get("identifier")
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 = await self.get_track_description(
current_track, self.local_folder_current_path
@ -59,7 +100,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.maybe_reset_error_counter(player)
if event_type == lavalink.LavalinkEvents.TRACK_START:
self.skip_votes[guild_id] = []
self.skip_votes[guild_id] = set()
playing_song = player.fetch("playing_song")
requester = player.fetch("requester")
player.store("prev_song", playing_song)
@ -71,25 +112,35 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
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 and autoplay:
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 (
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 = player.fetch("channel")
notify_channel = self.bot.get_channel(notify_channel)
if notify_channel:
await self.send_embed_msg(
@ -97,7 +148,6 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
)
return
except TrackEnqueueError:
notify_channel = player.fetch("channel")
notify_channel = self.bot.get_channel(notify_channel)
if notify_channel:
await self.send_embed_msg(
@ -116,18 +166,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
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:
if not (description and notify_channel):
return
if current_stream:
dur = "LIVE"
@ -166,6 +205,9 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
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:
@ -197,10 +239,14 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
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:
@ -240,3 +286,209 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
)
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
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(
f"Received an op code for a channel that is no longer valid; {event_channel_id} "
f"in guild: {guild_id} - Active channel {channel_id} | "
f"Reason: Error code {code} & {reason}."
)
self._ws_op_codes[guild_id]._init(self._ws_op_codes[guild_id]._maxsize)
return
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
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(
f"Player resumed in channel {channel_id} in guild: {guild_id} | "
f"Reason: Error code {code} & {reason}."
)
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 - "
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, replace=True)
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.resume(
player.current, start=player.position, replace=True, 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,) 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(
f"Player resumed in channel {channel_id} in guild: {guild_id} | "
f"Reason: Error code {code} & {reason}."
)
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(
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, replace=True)
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.resume(
player.current, start=player.position, replace=True, 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:
if not player.paused and player.current:
player.store("resumes", player.fetch("resumes", 0) + 1)
await player.resume(player.current, start=player.position, replace=False)
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}"
)
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()

View File

@ -99,7 +99,6 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
bot=self.bot,
host=host,
password=password,
rest_port=ws_port,
ws_port=ws_port,
timeout=timeout,
resume_key=f"Red-Core-Audio-{self.bot.user.id}-{data_manager.instance_name}",

View File

@ -47,15 +47,36 @@ class PlayerTasks(MixinMeta, metaclass=CompositeMetaClass):
servers.update(pause_times)
async for sid in AsyncIter(servers, steps=5):
server_obj = self.bot.get_guild(sid)
if sid in stop_times and await self.config.guild(server_obj).emptydc_enabled():
if not server_obj:
stop_times.pop(sid, None)
pause_times.pop(sid, None)
try:
player = lavalink.get_player(sid)
await self.api_interface.persistent_queue_api.drop(sid)
player.store("autoplay_notified", False)
await player.stop()
await player.disconnect()
await self.config.guild_from_id(
guild_id=sid
).currently_auto_playing_in.set([])
except Exception as err:
debug_exc_log(
log, err, f"Exception raised in Audio's emptydc_timer for {sid}."
)
elif sid in stop_times and await self.config.guild(server_obj).emptydc_enabled():
emptydc_timer = await self.config.guild(server_obj).emptydc_timer()
if (time.time() - stop_times[sid]) >= emptydc_timer:
stop_times.pop(sid)
try:
player = lavalink.get_player(sid)
await self.api_interface.persistent_queue_api.drop(sid)
player.store("autoplay_notified", False)
await player.stop()
await player.disconnect()
await self.config.guild_from_id(
guild_id=sid
).currently_auto_playing_in.set([])
except Exception as err:
if "No such player for that guild" in str(err):
stop_times.pop(sid, None)

View File

@ -10,12 +10,14 @@ import lavalink
from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replaced
from redbot.core.utils.dbtools import APSWConnectionWrapper
from ...apis.interface import AudioAPIInterface
from ...apis.playlist_wrapper import PlaylistWrapper
from ...audio_logging import debug_exc_log
from ...errors import DatabaseError, TrackEnqueueError
from ...utils import task_callback
from ..abc import MixinMeta
from ..cog_utils import _OWNER_NOTIFICATION, _SCHEMA_VERSION, CompositeMetaClass
@ -52,6 +54,7 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
)
await self.playlist_api.delete_scheduled()
await self.api_interface.persistent_queue_api.delete_scheduled()
await self._build_bundled_playlist()
self.lavalink_restart_connect()
self.player_automated_timer_task = self.bot.loop.create_task(
self.player_automated_timer()
@ -66,13 +69,29 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
async def restore_players(self):
tries = 0
tracks_to_restore = await self.api_interface.persistent_queue_api.fetch_all()
await asyncio.sleep(10)
while not lavalink.node._nodes:
await asyncio.sleep(1)
tries += 1
if tries > 60:
log.exception("Unable to restore players, couldn't connect to Lavalink.")
return
metadata = {}
all_guilds = await self.config.all_guilds()
async for guild_id, guild_data in AsyncIter(all_guilds.items(), steps=100):
if guild_data["auto_play"]:
if guild_data["currently_auto_playing_in"]:
notify_channel, vc_id = guild_data["currently_auto_playing_in"]
metadata[guild_id] = (notify_channel, vc_id)
for guild_id, track_data in itertools.groupby(tracks_to_restore, key=lambda x: x.guild_id):
await asyncio.sleep(0)
tries = 0
try:
player: Optional[lavalink.Player]
player: Optional[lavalink.Player] = None
track_data = list(track_data)
guild = self.bot.get_guild(guild_id)
if not guild:
continue
persist_cache = self._persist_queue_cache.setdefault(
guild_id, await self.config.guild(guild).persist_queue()
)
@ -88,40 +107,48 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
player = None
except KeyError:
player = None
vc = 0
guild_data = await self.config.guild_from_id(guild.id).all()
shuffle = guild_data["shuffle"]
repeat = guild_data["repeat"]
volume = guild_data["volume"]
shuffle_bumped = guild_data["shuffle_bumped"]
auto_deafen = guild_data["auto_deafen"]
if player is None:
while tries < 25 and vc is not None:
while tries < 5 and vc is not None:
try:
vc = guild.get_channel(track_data[-1].room_id)
notify_channel_id, vc_id = metadata.pop(
guild_id, (None, track_data[-1].room_id)
)
vc = guild.get_channel(vc_id)
if not vc:
break
perms = vc.permissions_for(guild.me)
if not (perms.connect and perms.speak):
vc = None
break
await lavalink.connect(vc)
await lavalink.connect(vc, deafen=auto_deafen)
player = lavalink.get_player(guild.id)
player.store("connect", datetime.datetime.utcnow())
player.store("guild", guild_id)
await self.self_deafen(player)
player.store("channel", notify_channel_id)
break
except IndexError:
await asyncio.sleep(5)
tries += 1
except Exception as exc:
tries += 1
debug_exc_log(log, exc, "Failed to restore music voice channel")
if vc is None:
break
else:
await asyncio.sleep(1)
if tries >= 25 or guild is None or vc is None:
if tries >= 5 or guild is None or vc is None or player is None:
await self.api_interface.persistent_queue_api.drop(guild_id)
continue
shuffle = await self.config.guild(guild).shuffle()
repeat = await self.config.guild(guild).repeat()
volume = await self.config.guild(guild).volume()
shuffle_bumped = await self.config.guild(guild).shuffle_bumped()
player.repeat = repeat
player.shuffle = shuffle
player.shuffle_bumped = shuffle_bumped
@ -137,6 +164,90 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
debug_exc_log(log, err, f"Error restoring player in {guild_id}")
await self.api_interface.persistent_queue_api.drop(guild_id)
for guild_id, (notify_channel_id, vc_id) in metadata.items():
guild = self.bot.get_guild(guild_id)
player: Optional[lavalink.Player] = None
vc = 0
tries = 0
if not guild:
continue
if self.lavalink_connection_aborted:
player = None
else:
try:
player = lavalink.get_player(guild_id)
except IndexError:
player = None
except KeyError:
player = None
if player is None:
guild_data = await self.config.guild_from_id(guild.id).all()
shuffle = guild_data["shuffle"]
repeat = guild_data["repeat"]
volume = guild_data["volume"]
shuffle_bumped = guild_data["shuffle_bumped"]
auto_deafen = guild_data["auto_deafen"]
while tries < 5 and vc is not None:
try:
vc = guild.get_channel(vc_id)
if not vc:
break
perms = vc.permissions_for(guild.me)
if not (perms.connect and perms.speak):
vc = None
break
await lavalink.connect(vc, deafen=auto_deafen)
player = lavalink.get_player(guild.id)
player.store("connect", datetime.datetime.utcnow())
player.store("guild", guild_id)
player.store("channel", notify_channel_id)
break
except IndexError:
await asyncio.sleep(5)
tries += 1
except Exception as exc:
tries += 1
debug_exc_log(log, exc, "Failed to restore music voice channel")
if vc is None:
break
else:
await asyncio.sleep(1)
if tries >= 5 or guild is None or vc is None or player is None:
continue
player.repeat = repeat
player.shuffle = shuffle
player.shuffle_bumped = shuffle_bumped
if player.volume != volume:
await player.set_volume(volume)
player.maybe_shuffle()
if not player.is_playing:
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
del metadata
del all_guilds
async def maybe_message_all_owners(self):
current_notification = await self.config.owner_notification()
if current_notification == _OWNER_NOTIFICATION:

View File

@ -99,10 +99,14 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
description = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=description)
try:
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except AttributeError:
return await self.send_embed_msg(ctx, title=_("Connect to a voice channel first."))
except IndexError:
@ -239,24 +243,26 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
if query.is_local:
search_list += "`{0}.` **{1}**\n[{2}]\n".format(
search_track_num,
track.title,
LocalPath(track.uri, self.local_folder_current_path).to_string_user(),
discord.utils.escape_markdown(track.title),
discord.utils.escape_markdown(
LocalPath(track.uri, self.local_folder_current_path).to_string_user()
),
)
else:
search_list += "`{0}.` **[{1}]({2})**\n".format(
search_track_num, track.title, track.uri
search_track_num, discord.utils.escape_markdown(track.title), track.uri
)
except AttributeError:
track = Query.process_input(track, self.local_folder_current_path)
if track.is_local and command != "search":
search_list += "`{}.` **{}**\n".format(
search_track_num, track.to_string_user()
search_track_num, discord.utils.escape_markdown(track.to_string_user())
)
if track.is_album:
folder = True
else:
search_list += "`{}.` **{}**\n".format(
search_track_num, track.to_string_user()
search_track_num, discord.utils.escape_markdown(track.to_string_user())
)
if hasattr(tracks[0], "uri") and hasattr(tracks[0], "track_identifier"):
title = _("Tracks Found:")

View File

@ -4,6 +4,7 @@ import logging
from pathlib import Path
from typing import List, Union
import discord
import lavalink
from fuzzywuzzy import process
@ -121,7 +122,7 @@ class LocalTrackUtilities(MixinMeta, metaclass=CompositeMetaClass):
if percent_match > 85:
search_list.extend(
[
i.to_string_user()
discord.utils.escape_markdown(i.to_string_user())
for i in to_search
if i.local_track_path is not None
and i.local_track_path.name == track_match

View File

@ -5,14 +5,14 @@ import functools
import json
import logging
import re
import struct
from pathlib import Path
from typing import Any, Final, Mapping, MutableMapping, Pattern, Union, cast
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core import bank, commands
from redbot.core.commands import Context
from redbot.core.i18n import Translator
@ -22,7 +22,7 @@ from redbot.core.utils.chat_formatting import humanize_number
from ...apis.playlist_interface import get_all_playlist_for_migration23
from ...utils import PlaylistScope, task_callback
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
from ..cog_utils import CompositeMetaClass, DataReader
log = logging.getLogger("red.cogs.Audio.cog.Utilities.miscellaneous")
_ = Translator("Audio", Path(__file__))
@ -338,3 +338,47 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass):
if database_entries:
await self.api_interface.local_cache_api.lavalink.insert(database_entries)
def decode_track(self, track: str, decode_errors: str = "ignore") -> MutableMapping:
"""
Decodes a base64 track string into an AudioTrack object.
Parameters
----------
track: :class:`str`
The base64 track string.
decode_errors: :class:`str`
The action to take upon encountering erroneous characters within track titles.
Returns
-------
:class:`AudioTrack`
"""
reader = DataReader(track)
flags = (reader.read_int() & 0xC0000000) >> 30
(version,) = (
struct.unpack("B", reader.read_byte()) if flags & 1 != 0 else 1
) # pylint: disable=unused-variable
title = reader.read_utf().decode(errors=decode_errors)
author = reader.read_utf().decode()
length = reader.read_long()
identifier = reader.read_utf().decode()
is_stream = reader.read_boolean()
uri = reader.read_utf().decode() if reader.read_boolean() else None
source = reader.read_utf().decode()
position = reader.read_long() # noqa: F841 pylint: disable=unused-variable
track_object = {
"track": track,
"info": {
"title": title,
"author": author,
"length": length,
"identifier": identifier,
"isStream": is_stream,
"uri": uri,
"isSeekable": not is_stream,
},
}
return track_object

View File

@ -218,10 +218,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
return
if not await self.config.guild_from_id(guild_id).auto_deafen():
return
channel_id = player.channel.id
node = player.manager.node
voice_ws = node.get_voice_ws(guild_id)
await voice_ws.voice_state(guild_id, channel_id, self_deaf=True)
await player.channel.guild.change_voice_state(channel=player.channel, self_deaf=True)
async def _get_spotify_tracks(
self, ctx: commands.Context, query: Query, forced: bool = False
@ -646,7 +643,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
lock=self.update_player_lock,
notifier=notifier,
forced=forced,
query_global=await self.config.global_db_enabled(),
query_global=self.global_api_user.get("can_read"),
)
except SpotifyFetchError as error:
self.update_player_lock(ctx, False)
@ -716,8 +713,10 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
and player.position == 0
and len(player.queue) == 0
):
await player.move_to(user_channel)
await self.self_deafen(player)
await player.move_to(
user_channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
return True
else:
return False

View File

@ -4,14 +4,17 @@ import datetime
import json
import logging
import math
import random
import time
from pathlib import Path
from typing import List, MutableMapping, Optional, Tuple, Union
import aiohttp
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
@ -19,7 +22,7 @@ from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate
from ...apis.playlist_interface import Playlist, create_playlist
from ...apis.playlist_interface import Playlist, PlaylistCompat23, create_playlist
from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
from ...audio_logging import debug_exc_log
from ...errors import TooManyMatches, TrackEnqueueError
@ -29,6 +32,9 @@ from ..cog_utils import CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Utilities.playlists")
_ = Translator("Audio", Path(__file__))
CURRATED_DATA = (
"https://gist.githubusercontent.com/Drapersniper/cbe10d7053c844f8c69637bb4fd9c5c3/raw/json"
)
class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
@ -470,6 +476,18 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
async def _maybe_update_playlist(
self, ctx: commands.Context, player: lavalink.player_manager.Player, playlist: Playlist
) -> Tuple[List[lavalink.Track], List[lavalink.Track], Playlist]:
if playlist.id == 42069:
_, updated_tracks = await self._get_bundled_playlist_tracks()
results = {}
old_tracks = playlist.tracks_obj
new_tracks = [lavalink.Track(data=track) for track in updated_tracks]
removed = list(set(old_tracks) - set(new_tracks))
added = list(set(new_tracks) - set(old_tracks))
if removed or added:
await playlist.edit(results)
return added, removed, playlist
if playlist.url is None:
return [], [], playlist
results = {}
@ -517,10 +535,14 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
description=_("I don't have permission to connect to your channel."),
)
return False
await lavalink.connect(ctx.author.voice.channel)
await lavalink.connect(
ctx.author.voice.channel,
deafen=await self.config.guild_from_id(ctx.guild.id).auto_deafen(),
)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id)
except IndexError:
await self.send_embed_msg(
ctx,
@ -659,3 +681,50 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
return ctx.name if ctx else _("the Server") if the else _("Server")
elif scope == PlaylistScope.USER.value:
return str(ctx) if ctx else _("the User") if the else _("User")
async def _get_bundled_playlist_tracks(self):
async with aiohttp.ClientSession(json_serialize=json.dumps) as session:
async with session.get(
CURRATED_DATA + f"?timestamp={int(time.time())}",
headers={"content-type": "application/json"},
) as response:
if response.status != 200:
return 0, []
try:
data = json.loads(await response.read())
except Exception:
log.exception("Curated playlist couldn't be parsed, report this error.")
data = {}
web_version = data.get("version", 0)
entries = data.get("entries", [])
if entries:
random.shuffle(entries)
tracks = []
async for entry in AsyncIter(entries, steps=25):
with contextlib.suppress(Exception):
tracks.append(self.decode_track(entry))
return web_version, tracks
async def _build_bundled_playlist(self, forced=False):
current_version = await self.config.bundled_playlist_version()
web_version, tracks = await self._get_bundled_playlist_tracks()
if not forced and current_version >= web_version:
return
playlist_data = dict()
playlist_data["name"] = "Aikaterna's curated tracks"
playlist_data["tracks"] = tracks
playlist = await PlaylistCompat23.from_json(
bot=self.bot,
playlist_api=self.playlist_api,
scope=PlaylistScope.GLOBAL.value,
playlist_number=42069,
data=playlist_data,
guild=None,
author=self.bot.user.id,
)
await playlist.save()
await self.config.bundled_playlist_version.set(web_version)
log.info("Curated playlist has been updated.")

View File

@ -14,7 +14,7 @@ lavalink:
http: true
local: true
sentryDsn: ""
bufferDurationMs: 400
bufferDurationMs: 1000
youtubePlaylistLoadLimit: 10000
logging:
file:

View File

@ -22,7 +22,7 @@ from .errors import LavalinkDownloadFailed
from .utils import task_callback
_ = Translator("Audio", pathlib.Path(__file__))
log = logging.getLogger("red.audio.manager")
log = logging.getLogger("red.Audio.manager")
JAR_VERSION: Final[str] = "3.3.2.3"
JAR_BUILD: Final[int] = 1212
LAVALINK_DOWNLOAD_URL: Final[str] = (
@ -234,6 +234,7 @@ class ServerManager:
line = await self._proc.stdout.readline()
if _RE_READY_LINE.search(line):
self.ready.set()
log.info("Internal Lavalink server is ready to receive requests.")
break
if _FAILED_TO_START.search(line):
raise RuntimeError(f"Lavalink failed to start: {line.decode().strip()}")

View File

@ -51,12 +51,13 @@ install_requires =
idna==2.10
Markdown==3.3.3
multidict==5.1.0
PyNaCl==1.3.0
Pygments==2.7.4
python-dateutil==2.8.1
python-Levenshtein-wheels==0.13.2
pytz==2021.1
PyYAML==5.4.1
Red-Lavalink==0.7.2
Red-Lavalink==0.8.0
rich==9.9.0
schema==0.7.4
six==1.15.0

View File

@ -26,6 +26,7 @@ install_requires =
schema
tqdm
uvloop; sys_platform != "win32" and platform_python_implementation == "CPython"
PyNaCl
[options.extras_require]
docs =