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

Commit fe1c753

Browse files
committed
Implementation of the inactive player event.
1 parent eb98c0a commit fe1c753

File tree

4 files changed

+162
-2
lines changed

4 files changed

+162
-2
lines changed

docs/wavelink.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,29 @@ 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+
See: :attr:`~wavelink.Player.inactive_timeout`
97+
See: :class:`~wavelink.Node` for setting a default on all players.
98+
99+
100+
Examples
101+
--------
102+
103+
**Basic Usage:**
104+
105+
.. code:: python3
106+
107+
@commands.Cog.listener()
108+
async def on_wavelink_inactive_player(self, player: wavelink.Player) -> None:
109+
await player.channel.send(f"The player has been inactive for `{player.inactive_timeout}` seconds. Goodbye!")
110+
await player.disconnect()
111+
112+
113+
.. versionadded:: 3.2.0
114+
92115

93116
Types
94117
-----

wavelink/node.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ 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``. See also: :func:`on_wavelink_inactive_player`.
122125
"""
123126

124127
def __init__(
@@ -132,6 +135,7 @@ def __init__(
132135
retries: int | None = None,
133136
client: discord.Client | None = None,
134137
resume_timeout: int = 60,
138+
inactive_player_timeout: int | None = 300,
135139
) -> None:
136140
self._identifier = identifier or secrets.token_urlsafe(12)
137141
self._uri = uri.removesuffix("/")
@@ -153,6 +157,13 @@ def __init__(
153157

154158
self._websocket: Websocket | None = None
155159

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

wavelink/player.py

Lines changed: 125 additions & 2 deletions
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,57 @@ 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+
.. wanring::
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+
.. versionadded:: 3.2.0
407+
"""
408+
return self._inactivity_wait
409+
410+
@inactive_timeout.setter
411+
def inactive_timeout(self, value: int | None) -> None:
412+
if not value or value <= 0:
413+
self._inactivity_wait = None
414+
self._inactivity_cancel()
415+
return
416+
417+
if value and value < 10:
418+
logger.warn('Setting "inactive_timeout" below 10 seconds may result in unwanted side effects.')
419+
420+
self._inactivity_wait = value
421+
self._inactivity_cancel()
422+
423+
if self.connected and not self.playing:
424+
self._inactivity_start()
425+
305426
@property
306427
def autoplay(self) -> AutoPlayMode:
307428
"""A property which returns the :class:`wavelink.AutoPlayMode` the player is currently in.
@@ -513,7 +634,8 @@ async def connect(
513634

514635
if not self._guild:
515636
self._guild = self.channel.guild
516-
self.node._players[self._guild.id] = self
637+
638+
self.node._players[self._guild.id] = self
517639

518640
assert self.guild is not None
519641
await self.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf)
@@ -858,6 +980,7 @@ async def skip(self, *, force: bool = True) -> Playable | None:
858980
def _invalidate(self) -> None:
859981
self._connected = False
860982
self._connection_event.clear()
983+
self._inactivity_cancel()
861984

862985
try:
863986
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)