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.
This commit is contained in:
Choco
2026-03-21 23:40:12 +08:00
parent 3492846699
commit 19618a1726
5 changed files with 86 additions and 50 deletions

View File

@@ -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",

View File

@@ -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()

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
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
)