Skip to content

Commit 7e8bfb6

Browse files
authored
Merge pull request #51 from ChocoMeow/Reconnect-player
Added Reconnect player after restarting the bot
2 parents 0ab2438 + 3a1440c commit 7e8bfb6

7 files changed

Lines changed: 163 additions & 30 deletions

File tree

cogs/listeners.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
SOFTWARE.
2222
"""
2323

24-
import voicelink
24+
import os
2525
import asyncio
2626
import discord
27+
import voicelink
2728
import function as func
2829

2930
from discord.ext import commands
@@ -36,10 +37,10 @@ def __init__(self, bot: commands.Bot):
3637
self.voicelink = voicelink.NodePool()
3738

3839
bot.loop.create_task(self.start_nodes())
40+
bot.loop.create_task(self.restore_last_session_players())
3941

4042
async def start_nodes(self) -> None:
4143
"""Connect and intiate nodes."""
42-
await self.bot.wait_until_ready()
4344
for n in func.settings.nodes.values():
4445
try:
4546
await self.voicelink.create_node(
@@ -52,6 +53,87 @@ async def start_nodes(self) -> None:
5253
except Exception as e:
5354
func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
5455

56+
async def restore_last_session_players(self) -> None:
57+
"""Re-establish connections for players from the last session."""
58+
await self.bot.wait_until_ready()
59+
players = func.open_json(func.LAST_SESSION_FILE_NAME)
60+
if not players:
61+
return
62+
63+
for data in players:
64+
try:
65+
channel_id = data.get("channel_id")
66+
if not channel_id:
67+
continue
68+
69+
channel = self.bot.get_channel(channel_id)
70+
if not channel:
71+
continue
72+
elif not any(False if member.bot or member.voice.self_deaf else True for member in channel.members):
73+
continue
74+
75+
dj_member = channel.guild.get_member(data.get("dj"))
76+
if not dj_member:
77+
continue
78+
79+
# Get the guild settings
80+
settings = await func.get_settings(channel.guild.id)
81+
82+
# Connect to the channel and initialize the player.
83+
player: voicelink.Player = await channel.connect(
84+
cls=voicelink.Player(self.bot, channel, func.TempCtx(dj_member, channel), settings)
85+
)
86+
87+
# Restore the queue.
88+
queue_data = data.get("queue", {})
89+
for track_data in queue_data.get("tracks", []):
90+
track_id = track_data.get("track_id")
91+
if not track_id:
92+
continue
93+
94+
decoded_track = voicelink.decode(track_id)
95+
requester = channel.guild.get_member(track_data.get("requester_id"))
96+
track = voicelink.Track(track_id=track_id, info=decoded_track, requester=requester)
97+
player.queue._queue.append(track)
98+
99+
# Restore queue settings.
100+
player.queue._position = queue_data.get("position", 0) - 1
101+
repeat_mode = queue_data.get("repeat_mode", "OFF")
102+
try:
103+
loop_mode = voicelink.LoopType[repeat_mode]
104+
except KeyError:
105+
loop_mode = voicelink.LoopType.OFF
106+
player.queue._repeat.set_mode(loop_mode)
107+
player.queue._repeat_position = queue_data.get("repeat_position")
108+
109+
# Restore player settings
110+
player.dj = dj_member
111+
player.settings['autoplay'] = data.get('autoplay', False)
112+
113+
# Resume playback or invoke the controller based on the player's state.
114+
if not player.is_playing:
115+
await player.do_next()
116+
117+
if is_paused := data.get("is_paused"):
118+
await player.set_pause(is_paused, self.bot.user)
119+
120+
if position := data.get("position"):
121+
await player.seek(int(position), self.bot.user)
122+
123+
await asyncio.sleep(5)
124+
125+
except Exception as e:
126+
func.logger.error(f"Error encountered while restoring a player for channel ID {channel_id}.", exc_info=e)
127+
128+
# Delete the last session file if it exists.
129+
try:
130+
file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
131+
if os.path.exists(file_path):
132+
os.remove(file_path)
133+
134+
except Exception as del_error:
135+
func.logger.error("Failed to remove session file: %s", file_path, exc_info=del_error)
136+
55137
@commands.Cog.listener()
56138
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):
57139
await player.do_next()

function.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@
7373
}
7474

7575
ALLOWED_MENTIONS = discord.AllowedMentions().none()
76+
LAST_SESSION_FILE_NAME = "last-session.json"
77+
78+
#-------------- Vocard Classes --------------
79+
class TempCtx():
80+
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
81+
self.author: discord.Member = author
82+
self.channel: discord.VoiceChannel = channel
83+
self.guild: discord.Guild = channel.guild
7684

7785
#-------------- Vocard Functions --------------
7886
def open_json(path: str) -> dict:
@@ -85,9 +93,9 @@ def open_json(path: str) -> dict:
8593
def update_json(path: str, new_data: dict) -> None:
8694
data = open_json(path)
8795
if not data:
88-
return
89-
90-
data.update(new_data)
96+
data = new_data
97+
else:
98+
data.update(new_data)
9199

92100
with open(os.path.join(ROOT_DIR, path), "w") as json_file:
93101
json.dump(data, json_file, indent=4)

ipc/methods.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@
2222
"stage_announce_template": str
2323
}
2424

25-
class TempCtx():
26-
def __init__(self, author: Member, channel: VoiceChannel) -> None:
27-
self.author = author
28-
self.channel = channel
29-
self.guild = channel.guild
30-
3125
class SystemMethod:
3226
def __init__(self, function: callable, *, credit: int = 1):
3327
self.function: callable = function
@@ -44,9 +38,9 @@ def require_permission(only_admin: bool = False):
4438
def decorator(func) -> callable:
4539
async def wrapper(player: Player, member: Member, dict: Dict) -> Optional[Dict]:
4640
if only_admin and not member.guild_permissions.manage_guild:
47-
return error_msg("Only the admins may use this funciton!", user_id=member.id)
41+
return error_msg("Only the admins may use this function!", user_id=member.id)
4842
if not player.is_privileged(member):
49-
return error_msg("Only the DJ or admins may use this funciton!", user_id=member.id)
43+
return error_msg("Only the DJ or admins may use this function!", user_id=member.id)
5044
return await func(player, member, dict)
5145
return wrapper
5246
return decorator
@@ -67,7 +61,7 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player:
6761
channel = member.voice.channel
6862
try:
6963
settings = await func.get_settings(channel.guild.id)
70-
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
64+
player: Player = await channel.connect(cls=Player(bot, channel, func.TempCtx(member, channel), settings))
7165
await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]})
7266
return player
7367
except:
@@ -438,13 +432,13 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
438432
"userId": str(user_id)
439433
}
440434

441-
assgined_playlist_id = _assign_playlist_id(list(playlist.keys()))
435+
assigned_playlist_id = _assign_playlist_id(list(playlist.keys()))
442436
data = {'uri': playlist_url, 'perms': {'read': []}, 'name': name, 'type': 'link'} if playlist_url else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
443-
await func.update_user(user_id, {"$set": {f"playlist.{assgined_playlist_id}": data}})
437+
await func.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}})
444438
return {
445439
"op": "updatePlaylist",
446440
"status": "created",
447-
"playlistId": assgined_playlist_id,
441+
"playlistId": assigned_playlist_id,
448442
"msg": f"You have created '{name}' playlist.",
449443
"userId": str(user_id),
450444
"data": data
@@ -579,7 +573,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
579573
if refer_id not in share_playlists:
580574
return error_msg("The shared playlist couldn’t be found. It’s possible that the user has already deleted it.", user_id=user_id)
581575

582-
assgined_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys()))
576+
assigned_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys()))
583577
playlist_name = f"Share{time.strftime('%M%S', time.gmtime(int(mail['time'])))}"
584578
share_playlist = share_playlists.get(refer_id)
585579
share_playlist.update({
@@ -588,7 +582,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
588582
})
589583
await func.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
590584
await func.update_user(user_id, {"$set": {
591-
f'playlist.{assgined_playlist_id}': {
585+
f'playlist.{assigned_playlist_id}': {
592586
'user': mail['sender'], 'referId': mail['referId'],
593587
'name': playlist_name,
594588
'type': 'share'
@@ -597,7 +591,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
597591
}})
598592

599593
payload.update({
600-
"playlistId": assgined_playlist_id,
594+
"playlistId": assigned_playlist_id,
601595
"msg": f"You have created '{playlist_name}' playlist.",
602596
"data": share_playlist,
603597
})

main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
import aiohttp
2828
import update
2929
import logging
30+
import voicelink
3031
import function as func
3132

3233
from discord.ext import commands
3334
from ipc import IPCClient
3435
from motor.motor_asyncio import AsyncIOMotorClient
3536
from logging.handlers import TimedRotatingFileHandler
36-
from voicelink import VoicelinkException
3737
from addons import Settings
3838

3939
class Translator(discord.app_commands.Translator):
@@ -181,7 +181,7 @@ async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
181181
embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {func.settings.invite_link}")
182182
return await ctx.reply(embed=embed)
183183

184-
elif not issubclass(error.__class__, VoicelinkException):
184+
elif not issubclass(error.__class__, voicelink.VoicelinkException):
185185
error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
186186
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)
187187

views/debug.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import discord
2525
import io
26+
import os
2627
import contextlib
2728
import textwrap
2829
import traceback
@@ -132,7 +133,7 @@ async def callback(self, interaction: discord.Interaction) -> None:
132133
selected = self.values[0].lower()
133134
try:
134135
if selected == "all":
135-
for name in self.bot.cogs.keys():
136+
for name in self.bot.cogs.copy().keys():
136137
await self.bot.reload_extension(f"cogs.{name.lower()}")
137138
else:
138139
await self.bot.reload_extension(f"cogs.{selected}")
@@ -384,4 +385,31 @@ async def sync(self, interaction: discord.Interaction, button: discord.ui.Button
384385
async def nodes(self, interaction: discord.Interaction, button: discord.ui.Button):
385386
view = NodesPanel(self.bot)
386387
await interaction.response.send_message(embed=view.build_embed(), view=view, ephemeral=True)
387-
view.message = await interaction.original_response()
388+
view.message = await interaction.original_response()
389+
390+
@discord.ui.button(label="Stop-Bot", emoji="🔴")
391+
async def stop(self, interaction: discord.Interaction, button: discord.ui.Button):
392+
for name in self.bot.cogs.copy().keys():
393+
try:
394+
await self.bot.unload_extension(name)
395+
except:
396+
pass
397+
398+
player_data = []
399+
for identifier, node in voicelink.NodePool._nodes.items():
400+
for guild_id, player in node._players.copy().items():
401+
if not player.guild.me.voice or not player.current:
402+
continue
403+
404+
player_data.append(player.data)
405+
try:
406+
await player.teardown()
407+
except:
408+
pass
409+
410+
session_file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
411+
if os.path.exists(session_file_path):
412+
os.remove(session_file_path)
413+
414+
func.update_json(func.LAST_SESSION_FILE_NAME, player_data)
415+
await interaction.client.close()

voicelink/objects.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ def formatted_length(self) -> str:
132132
def data(self) -> dict:
133133
return {
134134
"track_id": self.track_id,
135-
"info": self.info,
136-
"thumbnail": self.thumbnail
135+
"requester_id": self.requester.id
137136
}
138137

139138
class Playlist:

voicelink/player.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ def ping(self) -> float:
224224
"""Calculates and returns the player's current ping in seconds."""
225225
return round(self._ping / 1000, 2)
226226

227+
@property
228+
def autoplay(self) -> bool:
229+
return self.settings.get("autoplay", False)
230+
231+
@property
232+
def data(self) -> dict:
233+
return {
234+
"guild_id": self._guild.id,
235+
"channel_id": self.channel.id,
236+
"queue": {
237+
"tracks": [track.data for track in self.queue._queue],
238+
"position": self.queue._position,
239+
"repeat_mode": self.queue._repeat.current.name,
240+
"repeat_position": self.queue._repeat_position
241+
},
242+
"dj": self.dj.id,
243+
"is_paused": self.is_paused,
244+
"position": self.position,
245+
"autoplay": self.autoplay
246+
}
247+
227248
@property
228249
def is_ipc_connected(self) -> bool:
229250
"""Indicates whether the Inter-Process Communication (IPC) connection is active."""
@@ -388,7 +409,7 @@ async def do_next(self):
388409
track = self.queue.get()
389410

390411
if not track:
391-
if self.settings.get("autoplay", False) and await self.get_recommendations():
412+
if self.autoplay and await self.get_recommendations():
392413
return await self.do_next()
393414
else:
394415
try:
@@ -403,9 +424,7 @@ async def do_next(self):
403424
"$push": {"history": {"$each": [track.track_id], "$slice": -25}}
404425
}))
405426

406-
if self.settings.get('controller', True):
407-
await self.invoke_controller()
408-
427+
await self.invoke_controller()
409428
await self.update_voice_status()
410429

411430
if self.is_ipc_connected:
@@ -418,6 +437,9 @@ async def do_next(self):
418437

419438
async def invoke_controller(self):
420439
"""Sends or updates the music controller message in the designated channel."""
440+
if not self.settings.get('controller', True):
441+
return
442+
421443
if self._updating or not self.channel:
422444
return
423445

0 commit comments

Comments
 (0)