4747)
4848from .filters import Filters
4949from .node import Pool
50- from .payloads import PlayerUpdateEventPayload , TrackEndEventPayload
50+ from .payloads import (
51+ PlayerUpdateEventPayload ,
52+ TrackEndEventPayload ,
53+ TrackStartEventPayload ,
54+ )
5155from .queue import Queue
5256from .tracks import Playable , Playlist
5357
@@ -139,15 +143,71 @@ def __init__(
139143
140144 self ._filters : Filters = Filters ()
141145
146+ # Needed for the inactivity checks...
147+ self ._inactivity_task : asyncio .Task [bool ] | None = None
148+ self ._inactivity_wait : int | None = self ._node ._inactive_player_timeout
149+
150+ def _inactivity_task_callback (self , task : asyncio .Task [bool ]) -> None :
151+ result : bool = task .result ()
152+ cancelled : bool = task .cancelled ()
153+
154+ if cancelled or result is False :
155+ logger .debug ("Disregarding Inactivity Check Task <%s> as it was previously cancelled." , task .get_name ())
156+ return
157+
158+ if result is not True :
159+ logger .debug ("Disregarding Inactivity Check Task <%s> as it received an unknown result." , task .get_name ())
160+ return
161+
162+ if not self ._guild :
163+ logger .debug ("Disregarding Inactivity Check Task <%s> as it has no guild." , task .get_name ())
164+ return
165+
166+ if self .playing :
167+ logger .debug (
168+ "Disregarding Inactivity Check Task <%s> as Player <%s> is playing." , task .get_name (), self ._guild .id
169+ )
170+ return
171+
172+ self .client .dispatch ("wavelink_inactive_player" , self )
173+ logger .debug ('Dispatched "on_wavelink_inactive_player" for Player <%s>.' , self ._guild .id )
174+
175+ async def _inactivity_runner (self , wait : int ) -> bool :
176+ try :
177+ await asyncio .sleep (wait )
178+ except asyncio .CancelledError :
179+ return False
180+
181+ return True
182+
183+ def _inactivity_cancel (self ) -> None :
184+ if self ._inactivity_task :
185+ try :
186+ self ._inactivity_task .cancel ()
187+ except Exception :
188+ pass
189+
190+ self ._inactivity_task = None
191+
192+ def _inactivity_start (self ) -> None :
193+ if self ._inactivity_wait is not None and self ._inactivity_wait > 0 :
194+ self ._inactivity_task = asyncio .create_task (self ._inactivity_runner (self ._inactivity_wait ))
195+ self ._inactivity_task .add_done_callback (self ._inactivity_task_callback )
196+
197+ async def _track_start (self , payload : TrackStartEventPayload ) -> None :
198+ self ._inactivity_cancel ()
199+
142200 async def _auto_play_event (self , payload : TrackEndEventPayload ) -> None :
143201 if self ._autoplay is AutoPlayMode .disabled :
202+ self ._inactivity_start ()
144203 return
145204
146205 if self ._error_count >= 3 :
147206 logger .warning (
148207 "AutoPlay was unable to continue as you have received too many consecutive errors."
149208 "Please check the error log on Lavalink."
150209 )
210+ self ._inactivity_start ()
151211 return
152212
153213 if payload .reason == "replaced" :
@@ -166,6 +226,7 @@ async def _auto_play_event(self, payload: TrackEndEventPayload) -> None:
166226
167227 if not isinstance (self .queue , Queue ) or not isinstance (self .auto_queue , Queue ): # type: ignore
168228 logger .warning (f'"Unable to use AutoPlay on Player for Guild "{ self .guild } " due to unsupported Queue.' )
229+ self ._inactivity_start ()
169230 return
170231
171232 if self .queue .mode is QueueMode .loop :
@@ -182,6 +243,10 @@ async def _auto_play_event(self, payload: TrackEndEventPayload) -> None:
182243 await self ._do_recommendation ()
183244
184245 async def _do_partial (self , * , history : bool = True ) -> None :
246+ # We still do the inactivity start here since if play fails and we have no more tracks...
247+ # we should eventually fire the inactivity event...
248+ self ._inactivity_start ()
249+
185250 if self ._current is None :
186251 try :
187252 track : Playable = self .queue .get ()
@@ -195,6 +260,10 @@ async def _do_recommendation(self):
195260 assert self .queue .history is not None and self .auto_queue .history is not None
196261
197262 if len (self .auto_queue ) > self ._auto_cutoff + 1 :
263+ # We still do the inactivity start here since if play fails and we have no more tracks...
264+ # we should eventually fire the inactivity event...
265+ self ._inactivity_start ()
266+
198267 track : Playable = self .auto_queue .get ()
199268 self .auto_queue .history .put (track )
200269
@@ -277,6 +346,7 @@ async def _search(query: str | None) -> T_a:
277346
278347 if not filtered_r :
279348 logger .debug (f'Player "{ self .guild .id } " could not load any songs via AutoPlay.' )
349+ self ._inactivity_start ()
280350 return
281351
282352 if not self ._current :
@@ -302,6 +372,58 @@ async def _search(query: str | None) -> T_a:
302372 random .shuffle (self .auto_queue ._queue )
303373 logger .debug (f'Player "{ self .guild .id } " added "{ added } " tracks to the auto_queue via AutoPlay.' )
304374
375+ # Probably don't need this here as it's likely to be cancelled instantly...
376+ self ._inactivity_start ()
377+
378+ @property
379+ def inactive_timeout (self ) -> int | None :
380+ """A property which returns the time as an ``int`` of seconds to wait before this player dispatches the
381+ :func:`on_wavelink_inactive_player` event.
382+
383+ This property could return ``None`` if no time has been set.
384+
385+ An inactive player is a player that has not been playing anything for the specified amount of seconds.
386+
387+ - Pausing the player while a song is playing will not activate this countdown.
388+ - The countdown starts when a track ends and cancels when a track starts.
389+ - The countdown will not trigger until a song is played for the first time or this property is reset.
390+ - The default countdown for all players is set on :class:`~wavelink.Node`.
391+
392+ This property can be set with a valid ``int`` of seconds to wait before dispatching the
393+ :func:`on_wavelink_inactive_player` event or ``None`` to remove the timeout.
394+
395+
396+ .. warning::
397+
398+ Setting this to a value of ``0`` or below is the equivalent of setting this property to ``None``.
399+
400+
401+ When this property is set, the timeout will reset, and all previously waiting countdowns are cancelled.
402+
403+ - See: :class:`~wavelink.Node`
404+ - See: :func:`on_wavelink_inactive_player`
405+
406+
407+ .. versionadded:: 3.2.0
408+ """
409+ return self ._inactivity_wait
410+
411+ @inactive_timeout .setter
412+ def inactive_timeout (self , value : int | None ) -> None :
413+ if not value or value <= 0 :
414+ self ._inactivity_wait = None
415+ self ._inactivity_cancel ()
416+ return
417+
418+ if value < 10 :
419+ logger .warn ('Setting "inactive_timeout" below 10 seconds may result in unwanted side effects.' )
420+
421+ self ._inactivity_wait = value
422+ self ._inactivity_cancel ()
423+
424+ if self .connected and not self .playing :
425+ self ._inactivity_start ()
426+
305427 @property
306428 def autoplay (self ) -> AutoPlayMode :
307429 """A property which returns the :class:`wavelink.AutoPlayMode` the player is currently in.
@@ -859,6 +981,7 @@ async def skip(self, *, force: bool = True) -> Playable | None:
859981 def _invalidate (self ) -> None :
860982 self ._connected = False
861983 self ._connection_event .clear ()
984+ self ._inactivity_cancel ()
862985
863986 try :
864987 self .cleanup ()
0 commit comments