Skip to content
This repository was archived by the owner on Apr 5, 2025. It is now read-only.

Commit 21ee454

Browse files
authored
Merge pull request #268 from PythonistaGuild/feature/inactivity-event
Implementation of the inactive player event.
2 parents c298d7c + a072e71 commit 21ee454

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

docs/wavelink.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,30 @@ An event listener in a cog.
8989

9090
.. versionadded:: 3.1.0
9191

92+
.. function:: on_wavelink_inactive_player(player: wavelink.Player)
93+
94+
Triggered when the :attr:`~wavelink.Player.inactive_timeout` countdown expires for the specific :class:`~wavelink.Player`.
95+
96+
97+
- See: :attr:`~wavelink.Player.inactive_timeout`
98+
- See: :class:`~wavelink.Node` for setting a default on all players.
99+
100+
101+
Examples
102+
--------
103+
104+
**Basic Usage:**
105+
106+
.. code:: python3
107+
108+
@commands.Cog.listener()
109+
async def on_wavelink_inactive_player(self, player: wavelink.Player) -> None:
110+
await player.channel.send(f"The player has been inactive for `{player.inactive_timeout}` seconds. Goodbye!")
111+
await player.disconnect()
112+
113+
114+
.. versionadded:: 3.2.0
115+
92116

93117
Types
94118
-----

wavelink/node.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ class Node:
119119
resume_timeout: Optional[int]
120120
The seconds this Node should configure Lavalink for resuming its current session in case of network issues.
121121
If this is ``0`` or below, resuming will be disabled. Defaults to ``60``.
122+
inactive_player_timeout: int | None
123+
Set the default for :attr:`wavelink.Player.inactive_timeout` on every player that connects to this node.
124+
Defaults to ``300``.
125+
126+
See also: :func:`on_wavelink_inactive_player`.
122127
"""
123128

124129
def __init__(
@@ -132,6 +137,7 @@ def __init__(
132137
retries: int | None = None,
133138
client: discord.Client | None = None,
134139
resume_timeout: int = 60,
140+
inactive_player_timeout: int | None = 300,
135141
) -> None:
136142
self._identifier = identifier or secrets.token_urlsafe(12)
137143
self._uri = uri.removesuffix("/")
@@ -153,6 +159,13 @@ def __init__(
153159

154160
self._websocket: Websocket | None = None
155161

162+
if inactive_player_timeout and inactive_player_timeout < 10:
163+
logger.warn('Setting "inactive_player_timeout" below 10 seconds may result in unwanted side effects.')
164+
165+
self._inactive_player_timeout = (
166+
inactive_player_timeout if inactive_player_timeout and inactive_player_timeout > 0 else None
167+
)
168+
156169
def __repr__(self) -> str:
157170
return f"Node(identifier={self.identifier}, uri={self.uri}, status={self.status}, players={len(self.players)})"
158171

wavelink/player.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@
4747
)
4848
from .filters import Filters
4949
from .node import Pool
50-
from .payloads import PlayerUpdateEventPayload, TrackEndEventPayload
50+
from .payloads import (
51+
PlayerUpdateEventPayload,
52+
TrackEndEventPayload,
53+
TrackStartEventPayload,
54+
)
5155
from .queue import Queue
5256
from .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()

wavelink/websocket.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ async def keep_alive(self) -> None:
197197
startpayload: TrackStartEventPayload = TrackStartEventPayload(player=player, track=track)
198198
self.dispatch("track_start", startpayload)
199199

200+
if player:
201+
asyncio.create_task(player._track_start(startpayload))
202+
200203
elif data["type"] == "TrackEndEvent":
201204
track: Playable = Playable(data["track"])
202205
reason: str = data["reason"]

0 commit comments

Comments
 (0)