Merge pull request #51 from ChocoMeow/Reconnect-player
Added Reconnect player after restarting the bot
This commit is contained in:
@@ -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()
|
||||
|
||||
14
function.py
14
function.py
@@ -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)
|
||||
|
||||
@@ -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 couldn’t be found. It’s 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,
|
||||
})
|
||||
|
||||
4
main.py
4
main.py
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user