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:
@@ -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",
|
||||
|
||||
48
cogs/task.py
48
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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user