Added music controller into request channel

This commit is contained in:
Choco
2024-12-09 16:42:55 +08:00
parent 1576ba7957
commit 3d377fd5ab
3 changed files with 97 additions and 42 deletions

View File

@@ -36,8 +36,8 @@ from discord import (
VoiceProtocol,
Member,
Message,
PartialMessage,
Interaction,
errors
)
from discord.ext import commands
@@ -112,7 +112,7 @@ class Player(VoiceProtocol):
self.queue: Queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
self._node = NodePool.get_node()
self._current: Track = None
self._current: Optional[Track] = None
self._filters: Filters = Filters()
self._paused: bool = False
self._is_connected: bool = False
@@ -126,8 +126,8 @@ class Player(VoiceProtocol):
self._voice_state: dict = {}
self.controller: Message = None
self.updating: bool = False
self.controller: Union[Message, PartialMessage] = None
self._updating: bool = False
self.pause_votes = set()
self.resume_votes = set()
@@ -180,7 +180,7 @@ class Player(VoiceProtocol):
return self._is_connected and self._paused
@property
def current(self) -> Track:
def current(self) -> Optional[Track]:
"""Property which returns the currently playing track"""
return self._current
@@ -218,12 +218,26 @@ class Player(VoiceProtocol):
@property
def ping(self) -> float:
"""Calculates and returns the player's current ping in seconds."""
return round(self._ping / 1000, 2)
@property
def is_ipc_connected(self) -> bool:
"""Indicates whether the Inter-Process Communication (IPC) connection is active."""
return self._ipc._is_connected and self._ipc_connection
def get_msg(self, *keys) -> Union[list[str], str]:
"""Retrieves a localized message or list of messages based on the given keys
for the guild associated with this player.
"""
return func.get_lang_non_async(self.guild.id, *keys)
def required(self, leave=False):
"""
Calculates the number of votes required for a specific action in the voice channel.
If `leave` is True and the channel has three members, the requirement adjusts to 2 votes.
"""
if self.settings.get('votedisable'):
return 0
@@ -233,18 +247,22 @@ class Player(VoiceProtocol):
required = 2
return required
@property
def is_ipc_connected(self) -> bool:
return self._ipc._is_connected and self._ipc_connection
def is_user_join(self, user: Member):
"""Checks if a user is present in the voice channel or has 'Manage Server' permission."""
if user not in self.channel.members:
if not user.guild_permissions.manage_guild:
return False
return True
def is_privileged(self, user: Member, check_user_join: bool = True) -> bool:
"""
Determines if a user has privileged access.
Privileged access is granted if the user is in the bot access list,
has 'Manage Server' permission, or meets the DJ role criteria in the settings.
Raises an exception if `check_user_join` is True and the user is not in the channel.
"""
if user.id in func.settings.bot_access_user:
return True
@@ -256,11 +274,20 @@ class Player(VoiceProtocol):
return manage_perm or (self.settings['dj'] in [role.id for role in user.roles])
return self.dj.id == user.id or manage_perm
def build_embed(self, current_track: Track = None):
"""Builds an embed based on the current track state."""
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
raw = controller.get("active" if current_track else "inactive", {})
return build_embed(raw, self._ph)
async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict:
"""Sends an HTTP request to the node with the given method, query, and data."""
uri: str = f"sessions/{self._node._session_id}/players/{self._guild.id}" + (f"?{query}" if query else "")
return await self._node.send(method, query=uri, data=data)
async def _update_state(self, data: dict) -> None:
"""Updates the player's state based on the provided data."""
state: dict = data.get("state")
self._last_update = time.time() * 1000
self._is_connected = state.get("connected")
@@ -277,6 +304,7 @@ class Player(VoiceProtocol):
})
async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None):
"""Dispatches a voice update to the node."""
if {"sessionId", "event"} != self._voice_state.keys():
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update failed {voice_data}")
return
@@ -293,10 +321,12 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update to {state['event']['endpoint']} with data {data}")
async def on_voice_server_update(self, data: dict):
"""Handles a voice server update event."""
self._voice_state.update({"event": data})
await self._dispatch_voice_update(self._voice_state)
async def on_voice_state_update(self, data: dict):
"""Handles a voice state update event."""
self._voice_state.update({"sessionId": data.get("session_id")})
if not (channel_id := data.get("channel_id")):
@@ -312,6 +342,7 @@ class Player(VoiceProtocol):
await self._dispatch_voice_update({**self._voice_state, "event": data})
async def _dispatch_event(self, data: dict):
"""Dispatches an event based on the type of event data received."""
event_type = data.get("type")
event: VoicelinkEvent = getattr(events, event_type)(data, self)
@@ -326,6 +357,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched event {event_type}.")
async def do_next(self):
"""Processes the next track in the queue."""
if self._current or self.is_playing or not self.channel:
return
@@ -378,42 +410,45 @@ class Player(VoiceProtocol):
})
async def invoke_controller(self):
if self.updating or not self.channel:
"""Sends or updates the music controller message in the designated channel."""
if self._updating or not self.channel:
return
self.updating = True
self._updating = True
try:
embed, view = await self.build_embed(), InteractiveController(self)
try:
embed, view = self.build_embed(self.current), InteractiveController(self)
if not self.controller:
self.controller = await self.context.channel.send(embed=embed, view=view)
if request_channel_data := self.settings.get("music_request_channel"):
channel = self.bot.get_channel(request_channel_data.get("text_channel_id"))
if channel:
self.controller = channel.get_partial_message(request_channel_data.get("controller_msg_id"))
await self.controller.edit(embed=embed, view=view)
# Send a new controller message if none exists
if not self.controller:
self.controller = await self.context.channel.send(embed=embed, view=view)
elif not await self.is_position_fresh():
try:
await self.controller.delete()
except:
pass
except Exception as e:
self._logger.warning(
f"Failed to delete outdated controller in {self.guild.name}({self.guild.id}): {e}"
)
self.controller = await self.context.channel.send(embed=embed, view=view)
else:
await self.controller.edit(embed=embed, view=view)
except errors.Forbidden:
pass
except Exception as e:
self._logger.error(f"Something went wrong while sending music controller to {self.guild.name}({self.guild.id})", exc_info=e)
pass
self.updating = False
async def build_embed(self):
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
raw = controller.get("active" if self.current else "inactive", {})
return build_embed(raw, self._ph)
finally:
self._updating = False
async def is_position_fresh(self):
"""Checks if the current controller message is among the most recent messages."""
try:
async for message in self.context.channel.history(limit=5):
if message.id == self.controller.id:
@@ -424,19 +459,24 @@ class Player(VoiceProtocol):
return False
async def teardown(self):
await func.update_settings(
self.guild.id,
{"$set": {
"""Cleans up the player and associated resources."""
try:
await func.update_settings(self.guild.id, {"$set": {
"lastActice": (timeNow := round(time.time())),
"playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2)
}}
)
await self.update_voice_status(remove_status=True)
if self.is_ipc_connected:
await self.send_ws({"op": "playerClose"})
}})
if self.is_ipc_connected:
await self.send_ws({"op": "playerClose"})
except:
pass
try:
await self.controller.delete()
await self.update_voice_status(remove_status=True)
if self.controller and self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"):
await self.controller.edit(embed=self.build_embed(), view=None)
else:
await self.controller.delete()
except:
pass
@@ -464,6 +504,7 @@ class Player(VoiceProtocol):
return await self._node.get_tracks(query, requester=requester, search_type=search_type)
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False):
"""Connects the player to a voice channel."""
await self.guild.change_voice_state(channel=self.channel, self_deaf=True, self_mute=self_mute)
self._node._players[self.guild.id] = self
self._is_connected = True
@@ -538,6 +579,7 @@ class Player(VoiceProtocol):
return self._current
def _validate_time(self, track: Track, start_time: int, end_time: int) -> None:
"""Validates the start and end times for a track."""
if start_time or end_time:
if not end_time:
end_time = track.length
@@ -555,6 +597,7 @@ class Player(VoiceProtocol):
track.end_time = end_time
async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int:
"""Adds one or more tracks to the queue."""
tracks: List[Track] = []
_duplicate_tracks = [] if self.queue._allow_duplicate and duplicate else [track.uri for track in self.queue._queue]
raw_tracks = raw_tracks[0] if isinstance(raw_tracks, List) and len(raw_tracks) == 1 else raw_tracks
@@ -586,6 +629,7 @@ class Player(VoiceProtocol):
return len(tracks) if is_list else position
async def remove_track(self, index: int, index2: int = None, remove_target: Member = None, requester: Member = None) -> Dict[int, Track]:
"""Removes one or more tracks from the queue."""
removed_tracks = self.queue.remove(index, index2, remove_target)
if removed_tracks and self.is_ipc_connected:
await self.send_ws({
@@ -651,6 +695,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.")
async def swap_track(self, index1: int, index2: int, requester: Member = None) -> Tuple[Track, Track]:
"""Swaps two tracks in the queue at the specified indices."""
track1, track2 = self.queue.swap(index1, index2)
if self.is_ipc_connected:
await self.send_ws({
@@ -661,6 +706,7 @@ class Player(VoiceProtocol):
return track1, track2
async def move_track(self, index: int, new_index: int, requester: Member = None) -> Optional[Track]:
"""Moves a track from its current position to a new position in the queue."""
moved_track = self.queue.move(index, new_index)
if self.is_ipc_connected:
@@ -669,6 +715,7 @@ class Player(VoiceProtocol):
return moved_track
async def set_repeat(self, mode: LoopType = None, requester: Member = None) -> LoopType:
"""Sets the repeat mode for the queue."""
if not mode:
mode = self.queue._repeat.next()
@@ -684,6 +731,7 @@ class Player(VoiceProtocol):
return mode
async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = False) -> Filters:
"""Adds a filter to the player's audio stream."""
try:
self._filters.add_filter(filter=filter)
except FilterTagAlreadyInUse:
@@ -705,6 +753,7 @@ class Player(VoiceProtocol):
return self._filters
async def clear_queue(self, queue_type: str, requester: Member = None) -> None:
"""Clears the queue or the history of tracks."""
queue_type = queue_type.lower()
if queue_type == 'history':
self.queue.history_clear(self.is_playing)
@@ -735,6 +784,7 @@ class Player(VoiceProtocol):
return self._filters
async def reset_filter(self, *, requester: Member = None, fast_apply=False) -> None:
"""Resets all filters applied to the player's audio stream."""
if not self._filters:
raise FilterInvalidArgument("You must have filters applied first in order to use this method.")
@@ -752,7 +802,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed all filters.")
async def change_node(self, identifier: str = None) -> None:
"""Change node."""
"""Changes the audio processing node for the guild.."""
try:
node = NodePool.get_node(identifier=identifier)
except:
@@ -791,6 +841,7 @@ class Player(VoiceProtocol):
return False
async def update_voice_status(self, remove_status: bool = False) -> None:
"""Updates the voice status of the channel based on the specified template."""
template = self.settings.get("stage_announce_template", func.settings.voice_status_template)
if not template or not self.channel:
return
@@ -810,6 +861,7 @@ class Player(VoiceProtocol):
)
async def send_ws(self, payload, requester: Member = None):
"""Sends a WebSocket payload to the bot's IPC (Inter-Process Communication) system."""
payload['guild_id'] = str(self.guild.id)
if requester:
payload['requester_id'] = str(requester.id)