Merge pull request #51 from ChocoMeow/Reconnect-player

Added Reconnect player after restarting the bot
This commit is contained in:
Choco
2025-03-16 11:08:05 +08:00
committed by GitHub
7 changed files with 163 additions and 30 deletions

View File

@@ -21,9 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import voicelink
import os
import asyncio
import discord
import voicelink
import function as func
from discord.ext import commands
@@ -36,10 +37,10 @@ class Listeners(commands.Cog):
self.voicelink = voicelink.NodePool()
bot.loop.create_task(self.start_nodes())
bot.loop.create_task(self.restore_last_session_players())
async def start_nodes(self) -> None:
"""Connect and intiate nodes."""
await self.bot.wait_until_ready()
for n in func.settings.nodes.values():
try:
await self.voicelink.create_node(
@@ -52,6 +53,87 @@ class Listeners(commands.Cog):
except Exception as e:
func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
async def restore_last_session_players(self) -> None:
"""Re-establish connections for players from the last session."""
await self.bot.wait_until_ready()
players = func.open_json(func.LAST_SESSION_FILE_NAME)
if not players:
return
for data in players:
try:
channel_id = data.get("channel_id")
if not channel_id:
continue
channel = self.bot.get_channel(channel_id)
if not channel:
continue
elif not any(False if member.bot or member.voice.self_deaf else True for member in channel.members):
continue
dj_member = channel.guild.get_member(data.get("dj"))
if not dj_member:
continue
# Get the guild settings
settings = await func.get_settings(channel.guild.id)
# Connect to the channel and initialize the player.
player: voicelink.Player = await channel.connect(
cls=voicelink.Player(self.bot, channel, func.TempCtx(dj_member, channel), settings)
)
# Restore the queue.
queue_data = data.get("queue", {})
for track_data in queue_data.get("tracks", []):
track_id = track_data.get("track_id")
if not track_id:
continue
decoded_track = voicelink.decode(track_id)
requester = channel.guild.get_member(track_data.get("requester_id"))
track = voicelink.Track(track_id=track_id, info=decoded_track, requester=requester)
player.queue._queue.append(track)
# Restore queue settings.
player.queue._position = queue_data.get("position", 0) - 1
repeat_mode = queue_data.get("repeat_mode", "OFF")
try:
loop_mode = voicelink.LoopType[repeat_mode]
except KeyError:
loop_mode = voicelink.LoopType.OFF
player.queue._repeat.set_mode(loop_mode)
player.queue._repeat_position = queue_data.get("repeat_position")
# Restore player settings
player.dj = dj_member
player.settings['autoplay'] = data.get('autoplay', False)
# Resume playback or invoke the controller based on the player's state.
if not player.is_playing:
await player.do_next()
if is_paused := data.get("is_paused"):
await player.set_pause(is_paused, self.bot.user)
if position := data.get("position"):
await player.seek(int(position), self.bot.user)
await asyncio.sleep(5)
except Exception as e:
func.logger.error(f"Error encountered while restoring a player for channel ID {channel_id}.", exc_info=e)
# Delete the last session file if it exists.
try:
file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
if os.path.exists(file_path):
os.remove(file_path)
except Exception as del_error:
func.logger.error("Failed to remove session file: %s", file_path, exc_info=del_error)
@commands.Cog.listener()
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):
await player.do_next()

View File

@@ -73,6 +73,14 @@ USER_BASE: dict[str, Any] = {
}
ALLOWED_MENTIONS = discord.AllowedMentions().none()
LAST_SESSION_FILE_NAME = "last-session.json"
#-------------- Vocard Classes --------------
class TempCtx():
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
self.author: discord.Member = author
self.channel: discord.VoiceChannel = channel
self.guild: discord.Guild = channel.guild
#-------------- Vocard Functions --------------
def open_json(path: str) -> dict:
@@ -85,9 +93,9 @@ def open_json(path: str) -> dict:
def update_json(path: str, new_data: dict) -> None:
data = open_json(path)
if not data:
return
data.update(new_data)
data = new_data
else:
data.update(new_data)
with open(os.path.join(ROOT_DIR, path), "w") as json_file:
json.dump(data, json_file, indent=4)

View File

@@ -22,12 +22,6 @@ SCOPES = {
"stage_announce_template": str
}
class TempCtx():
def __init__(self, author: Member, channel: VoiceChannel) -> None:
self.author = author
self.channel = channel
self.guild = channel.guild
class SystemMethod:
def __init__(self, function: callable, *, credit: int = 1):
self.function: callable = function
@@ -44,9 +38,9 @@ def require_permission(only_admin: bool = False):
def decorator(func) -> callable:
async def wrapper(player: Player, member: Member, dict: Dict) -> Optional[Dict]:
if only_admin and not member.guild_permissions.manage_guild:
return error_msg("Only the admins may use this funciton!", user_id=member.id)
return error_msg("Only the admins may use this function!", user_id=member.id)
if not player.is_privileged(member):
return error_msg("Only the DJ or admins may use this funciton!", user_id=member.id)
return error_msg("Only the DJ or admins may use this function!", user_id=member.id)
return await func(player, member, dict)
return wrapper
return decorator
@@ -67,7 +61,7 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player:
channel = member.voice.channel
try:
settings = await func.get_settings(channel.guild.id)
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
player: Player = await channel.connect(cls=Player(bot, channel, func.TempCtx(member, channel), settings))
await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]})
return player
except:
@@ -438,13 +432,13 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"userId": str(user_id)
}
assgined_playlist_id = _assign_playlist_id(list(playlist.keys()))
assigned_playlist_id = _assign_playlist_id(list(playlist.keys()))
data = {'uri': playlist_url, 'perms': {'read': []}, 'name': name, 'type': 'link'} if playlist_url else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
await func.update_user(user_id, {"$set": {f"playlist.{assgined_playlist_id}": data}})
await func.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}})
return {
"op": "updatePlaylist",
"status": "created",
"playlistId": assgined_playlist_id,
"playlistId": assigned_playlist_id,
"msg": f"You have created '{name}' playlist.",
"userId": str(user_id),
"data": data
@@ -579,7 +573,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
if refer_id not in share_playlists:
return error_msg("The shared playlist couldnt be found. Its possible that the user has already deleted it.", user_id=user_id)
assgined_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys()))
assigned_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys()))
playlist_name = f"Share{time.strftime('%M%S', time.gmtime(int(mail['time'])))}"
share_playlist = share_playlists.get(refer_id)
share_playlist.update({
@@ -588,7 +582,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
})
await func.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
await func.update_user(user_id, {"$set": {
f'playlist.{assgined_playlist_id}': {
f'playlist.{assigned_playlist_id}': {
'user': mail['sender'], 'referId': mail['referId'],
'name': playlist_name,
'type': 'share'
@@ -597,7 +591,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
}})
payload.update({
"playlistId": assgined_playlist_id,
"playlistId": assigned_playlist_id,
"msg": f"You have created '{playlist_name}' playlist.",
"data": share_playlist,
})

View File

@@ -27,13 +27,13 @@ import os
import aiohttp
import update
import logging
import voicelink
import function as func
from discord.ext import commands
from ipc import IPCClient
from motor.motor_asyncio import AsyncIOMotorClient
from logging.handlers import TimedRotatingFileHandler
from voicelink import VoicelinkException
from addons import Settings
class Translator(discord.app_commands.Translator):
@@ -181,7 +181,7 @@ class Vocard(commands.Bot):
embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {func.settings.invite_link}")
return await ctx.reply(embed=embed)
elif not issubclass(error.__class__, VoicelinkException):
elif not issubclass(error.__class__, voicelink.VoicelinkException):
error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
func.logger.error(f"An unexpected error occurred in the {ctx.command.name} command on the {ctx.guild.name}({ctx.guild.id}).", exc_info=exception)

View File

@@ -23,6 +23,7 @@ SOFTWARE.
import discord
import io
import os
import contextlib
import textwrap
import traceback
@@ -132,7 +133,7 @@ class CogsDropdown(discord.ui.Select):
selected = self.values[0].lower()
try:
if selected == "all":
for name in self.bot.cogs.keys():
for name in self.bot.cogs.copy().keys():
await self.bot.reload_extension(f"cogs.{name.lower()}")
else:
await self.bot.reload_extension(f"cogs.{selected}")
@@ -384,4 +385,31 @@ class DebugView(discord.ui.View):
async def nodes(self, interaction: discord.Interaction, button: discord.ui.Button):
view = NodesPanel(self.bot)
await interaction.response.send_message(embed=view.build_embed(), view=view, ephemeral=True)
view.message = await interaction.original_response()
view.message = await interaction.original_response()
@discord.ui.button(label="Stop-Bot", emoji="🔴")
async def stop(self, interaction: discord.Interaction, button: discord.ui.Button):
for name in self.bot.cogs.copy().keys():
try:
await self.bot.unload_extension(name)
except:
pass
player_data = []
for identifier, node in voicelink.NodePool._nodes.items():
for guild_id, player in node._players.copy().items():
if not player.guild.me.voice or not player.current:
continue
player_data.append(player.data)
try:
await player.teardown()
except:
pass
session_file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
if os.path.exists(session_file_path):
os.remove(session_file_path)
func.update_json(func.LAST_SESSION_FILE_NAME, player_data)
await interaction.client.close()

View File

@@ -132,8 +132,7 @@ class Track:
def data(self) -> dict:
return {
"track_id": self.track_id,
"info": self.info,
"thumbnail": self.thumbnail
"requester_id": self.requester.id
}
class Playlist:

View File

@@ -224,6 +224,27 @@ class Player(VoiceProtocol):
"""Calculates and returns the player's current ping in seconds."""
return round(self._ping / 1000, 2)
@property
def autoplay(self) -> bool:
return self.settings.get("autoplay", False)
@property
def data(self) -> dict:
return {
"guild_id": self._guild.id,
"channel_id": self.channel.id,
"queue": {
"tracks": [track.data for track in self.queue._queue],
"position": self.queue._position,
"repeat_mode": self.queue._repeat.current.name,
"repeat_position": self.queue._repeat_position
},
"dj": self.dj.id,
"is_paused": self.is_paused,
"position": self.position,
"autoplay": self.autoplay
}
@property
def is_ipc_connected(self) -> bool:
"""Indicates whether the Inter-Process Communication (IPC) connection is active."""
@@ -388,7 +409,7 @@ class Player(VoiceProtocol):
track = self.queue.get()
if not track:
if self.settings.get("autoplay", False) and await self.get_recommendations():
if self.autoplay and await self.get_recommendations():
return await self.do_next()
else:
try:
@@ -403,9 +424,7 @@ class Player(VoiceProtocol):
"$push": {"history": {"$each": [track.track_id], "$slice": -25}}
}))
if self.settings.get('controller', True):
await self.invoke_controller()
await self.invoke_controller()
await self.update_voice_status()
if self.is_ipc_connected:
@@ -418,6 +437,9 @@ class Player(VoiceProtocol):
async def invoke_controller(self):
"""Sends or updates the music controller message in the designated channel."""
if not self.settings.get('controller', True):
return
if self._updating or not self.channel:
return