From 19618a1726927fa25cd0e139c09b881a2640c687 Mon Sep 17 00:00:00 2001 From: Choco Date: Sat, 21 Mar 2026 23:40:12 +0800 Subject: [PATCH] Add per-player inactive cleanup timer and config Adds configurable inactive cleanup timing and per-player timer logic so idle players disconnect or pause according to 24/7 and voice-channel state, replacing immediate teardown when the queue finishes. --- cogs/listeners.py | 15 ++++++++-- cogs/task.py | 48 ++++--------------------------- settings Example.json | 5 ++++ voicelink/config.py | 1 + voicelink/player.py | 67 +++++++++++++++++++++++++++++++++++++++---- 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index 44b2da3..391e1ce 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -173,9 +173,20 @@ class Listeners(commands.Cog): is_joined = False if is_joined and player.settings.get("24/7", False): - if player.is_paused and len([m for m in player.channel.members if not m.bot]) == 1: + if player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 1: await player.set_pause(False, member) - + + if not is_joined: + if not player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 0: + player._schedule_inactive_cleanup_timer() + + # if dj is not in the channel, find a new DJ + if player.dj not in player.channel.members: + for m in player.channel.members: + if not m.bot or not m.voice.self_deaf: + player.dj = m + break + if player.is_ipc_connected: await player._ipc_client.send({ "op": "updateGuild", diff --git a/cogs/task.py b/cogs/task.py index fd0f030..8975d5e 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -25,13 +25,13 @@ import voicelink import discord import function as func +from voicelink.config import Config from discord.ext import commands, tasks class Task(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.activity_update.start() - self.player_check.start() self.cache_cleaner.start() self.current_act = 0 @@ -39,15 +39,14 @@ class Task(commands.Cog): def cog_unload(self): self.activity_update.cancel() - self.player_check.cancel() self.cache_cleaner.cancel() - @tasks.loop(minutes=10.0) + @tasks.loop(seconds=Config().timer_settings.get("bot_activity_update", 600)) async def activity_update(self): await self.bot.wait_until_ready() try: - act_data = voicelink.Config().activity[(self.current_act + 1) % len(voicelink.Config().activity) - 1] + act_data = Config().activity[(self.current_act + 1) % len(Config().activity) - 1] act_original = self.bot.activity act_type = getattr(discord.ActivityType, act_data.get("type", "").lower(), discord.ActivityType.playing) act_name = self.placeholder.replace(act_data.get("name", "")) @@ -57,51 +56,14 @@ class Task(commands.Cog): if act_original.type != act_type or act_original.name != act_name: self.bot.activity = discord.Activity(type=act_type, name=act_name) await self.bot.change_presence(activity=self.bot.activity, status=status_type) - self.current_act = (self.current_act + 1) % len(voicelink.Config().activity) + self.current_act = (self.current_act + 1) % len(Config().activity) func.logger.info(f"Changed the bot status to {act_name}") except Exception as e: func.logger.error("Error occurred while changing the bot status!", exc_info=e) - @tasks.loop(minutes=5.0) - async def player_check(self): - for identifier, node in voicelink.NodePool._nodes.items(): - for guild_id, player in node._players.copy().items(): - try: - if not player.channel or not player.context or not player.guild: - await player.teardown() - continue - except: - await player.teardown() - continue - - try: - members = player.channel.members - if (not player.is_playing and player.queue.is_empty) or not any(False if member.bot or member.voice.self_deaf else True for member in members): - if not player.settings.get('24/7', False): - await player.teardown() - continue - else: - if not player.is_paused: - await player.set_pause(True) - else: - if not player.guild.me: - await player.teardown() - continue - elif not player.guild.me.voice: - await player.connect(timeout=0.0, reconnect=True) - - if player.dj not in members: - for m in members: - if not m.bot: - player.dj = m - break - - except Exception as e: - func.logger.error("Error occurred while checking the player!", exc_info=e) - - @tasks.loop(hours=12.0) + @tasks.loop(seconds=Config().timer_settings.get("cache_cleanup", 43200)) async def cache_cleaner(self): await voicelink.MongoDBHandler.cleanup_cache() diff --git a/settings Example.json b/settings Example.json index af423cb..8ac45ed 100644 --- a/settings Example.json +++ b/settings Example.json @@ -49,6 +49,11 @@ "secure": false, "enable": false }, + "timer_settings": { + "bot_activity_update": 600, + "inactive_player_cleanup": 600, + "cache_cleanup": 43200 + }, "playlist_settings": { "max_playlist": 5, "max_tracks_per_playlist": 500, diff --git a/voicelink/config.py b/voicelink/config.py index ff6de26..efe6214 100644 --- a/voicelink/config.py +++ b/voicelink/config.py @@ -98,6 +98,7 @@ class Config: self.lyrics_platform: str = settings.get("lyrics_platform", "A_ZLyrics").lower() self.ipc_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {}) self.playlist_settings: Dict[str, Union[str, int]] = settings.get("playlist_settings", {}) + self.timer_settings: Dict[str, int] = settings.get("timer_settings", {}) self.version: str = settings.get("version", "") self.initialized = True diff --git a/voicelink/player.py b/voicelink/player.py index c54626c..a60de2f 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -23,10 +23,9 @@ SOFTWARE. from __future__ import annotations -import time, logging +import time, logging, asyncio from math import ceil -from asyncio import sleep from random import shuffle, choice from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING @@ -155,6 +154,7 @@ class Player(VoiceProtocol): self._ph = PlayerPlaceholder(client, self) self._logger: Optional[logging.Logger] = self._node._logger + self._inactive_cleanup_task: Optional[asyncio.Task[None]] = None def __repr__(self): return ( @@ -238,10 +238,12 @@ class Player(VoiceProtocol): @property def autoplay(self) -> bool: + """Indicates whether the player is set to autoplay.""" return self.settings.get("autoplay", False) @property def data(self) -> dict: + """Returns a dictionary containing the player's data.""" return { "guild_id": self._guild.id, "channel_id": self.channel.id, @@ -405,7 +407,7 @@ class Player(VoiceProtocol): self._paused = False if self._track_is_stuck: - await sleep(10) + await asyncio.sleep(10) self._track_is_stuck = False if not self.guild.me.voice: @@ -423,12 +425,14 @@ class Player(VoiceProtocol): if not track: if self.autoplay and await self.get_recommendations(): return await self.do_next() + if self.queue.is_empty: + self._schedule_inactive_cleanup_timer() else: try: await self.play(track, start=track.position) except Exception as e: self._logger.error(f"Something went wrong while playing music in {self.guild.name}({self.guild.id})", exc_info=e) - await sleep(5) + await asyncio.sleep(5) return await self.do_next() if not track.requester.bot: @@ -515,6 +519,7 @@ class Player(VoiceProtocol): try: await self.update_voice_status(remove_status=True) + self._cancel_inactive_cleanup_timer() if self.controller: if self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"): await self.controller.edit(embed=self.build_embed(), view=None) @@ -630,6 +635,21 @@ class Player(VoiceProtocol): track.position = start_time track.end_time = end_time + def _cancel_inactive_cleanup_timer(self) -> None: + """Cancels the per-player inactivity cleanup timer (if any).""" + task: Optional[asyncio.Task] = getattr(self, "_inactive_cleanup_task", None) + if task and not task.done(): + task.cancel() + self._inactive_cleanup_task = None + + def _schedule_inactive_cleanup_timer(self) -> None: + """Schedules per-player cleanup after inactivity.""" + self._cancel_inactive_cleanup_timer() + seconds = Config().timer_settings.get("inactive_player_cleanup", 600) + self._inactive_cleanup_task = self.bot.loop.create_task( + self._inactive_cleanup_timer_worker(seconds) + ) + 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] = [] @@ -656,6 +676,8 @@ class Player(VoiceProtocol): finally: if tracks: + if self.channel.members and len([m for m in self.channel.members if not m.bot]) > 0: + self._cancel_inactive_cleanup_timer() if self.is_ipc_connected: await self.send_ws({"op": "addTrack", "tracks": [track.track_id for track in tracks], "position": -1 if is_list else position}, tracks[0].requester) @@ -900,4 +922,39 @@ class Player(VoiceProtocol): payload['guildId'] = str(self.guild.id) if requester: payload['requesterId'] = str(requester.id) - await self._ipc_client.send(payload) \ No newline at end of file + await self._ipc_client.send(payload) + + async def _inactive_cleanup_timer_worker(self, seconds: int) -> None: + try: + await asyncio.sleep(seconds) + if not self._guild or not self._node: + return + if self._guild.id not in self._node._players: + return + + members = self.channel.members if self.channel else [] + has_non_bot = any(not m.bot or not m.voice.self_deaf for m in members) + # Empty channel: always pause/teardown. If people remain: only when idle (not playing + empty queue). + if has_non_bot and (self.is_playing or not self.queue.is_empty): + return + + self._inactive_cleanup_task = None + + if self.settings.get("24/7", False): + if not self.is_paused: + await self.set_pause(True) + else: + await self.teardown() + except asyncio.CancelledError: + return + except Exception as e: + # Logger is best-effort; timer cleanup should never crash the loop. + try: + guild_id = self._guild.id if self._guild else "unknown" + except Exception: + guild_id = "unknown" + if self._logger: + self._logger.error( + f"Inactive cleanup timer failed for guild {guild_id}", + exc_info=e + ) \ No newline at end of file