Skip to content

Commit

Permalink
Feature/playable node and Inactive Channel Check (#303)
Browse files Browse the repository at this point in the history
* Add inactive_channel check.

* Remove discord badge from README

* Allow a Node to be passed to Playable or fetch_tracks.
  • Loading branch information
EvieePy authored Jun 11, 2024
1 parent faee7c8 commit f548a3b
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 15 deletions.
20 changes: 17 additions & 3 deletions wavelink/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ class Node:
inactive_player_timeout: int | None
Set the default for :attr:`wavelink.Player.inactive_timeout` on every player that connects to this node.
Defaults to ``300``.
inactive_channel_tokens: int | None
Sets the default for :attr:`wavelink.Player.inactive_channel_tokens` on every player that connects to this node.
Defaults to ``3``.
See also: :func:`on_wavelink_inactive_player`.
"""
Expand All @@ -142,6 +145,7 @@ def __init__(
client: discord.Client | None = None,
resume_timeout: int = 60,
inactive_player_timeout: int | None = 300,
inactive_channel_tokens: int | None = 3,
) -> None:
self._identifier = identifier or secrets.token_urlsafe(12)
self._uri = uri.removesuffix("/")
Expand Down Expand Up @@ -170,6 +174,8 @@ def __init__(
inactive_player_timeout if inactive_player_timeout and inactive_player_timeout > 0 else None
)

self._inactive_channel_tokens = inactive_channel_tokens

def __repr__(self) -> str:
return f"Node(identifier={self.identifier}, uri={self.uri}, status={self.status}, players={len(self.players)})"

Expand Down Expand Up @@ -895,14 +901,17 @@ def get_node(cls, identifier: str | None = None, /) -> Node:
return sorted(nodes, key=lambda n: n._total_player_count or len(n.players))[0]

@classmethod
async def fetch_tracks(cls, query: str, /) -> list[Playable] | Playlist:
async def fetch_tracks(cls, query: str, /, *, node: Node | None = None) -> list[Playable] | Playlist:
"""Search for a list of :class:`~wavelink.Playable` or a :class:`~wavelink.Playlist`, with the given query.
Parameters
----------
query: str
The query to search tracks for. If this is not a URL based search you should provide the appropriate search
prefix, e.g. "ytsearch:Rick Roll"
node: :class:`~wavelink.Node` | None
An optional :class:`~wavelink.Node` to use when fetching tracks. Defaults to ``None``, which selects the
most appropriate :class:`~wavelink.Node` automatically.
Returns
-------
Expand All @@ -923,6 +932,11 @@ async def fetch_tracks(cls, query: str, /) -> list[Playable] | Playlist:
or an empty list if no results were found.
This method no longer accepts the ``cls`` parameter.
.. versionadded:: 3.4.0
Added the ``node`` Keyword-Only argument.
"""

# TODO: Documentation Extension for `.. positional-only::` marker.
Expand All @@ -934,8 +948,8 @@ async def fetch_tracks(cls, query: str, /) -> list[Playable] | Playlist:
if potential:
return potential

node: Node = cls.get_node()
resp: LoadedResponse = await node._fetch_tracks(encoded_query)
node_: Node = node or cls.get_node()
resp: LoadedResponse = await node_._fetch_tracks(encoded_query)

if resp["loadType"] == "track":
track = Playable(data=resp["data"])
Expand Down
75 changes: 66 additions & 9 deletions wavelink/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ def __init__(
self._auto_lock: asyncio.Lock = asyncio.Lock()
self._error_count: int = 0

self._inactive_channel_limit: int | None = self._node._inactive_channel_tokens
self._inactive_channel_count: int = self._inactive_channel_limit if self._inactive_channel_limit else 0

self._filters: Filters = Filters()

# Needed for the inactivity checks...
Expand Down Expand Up @@ -216,7 +219,21 @@ async def _track_start(self, payload: TrackStartEventPayload) -> None:
self._inactivity_cancel()

async def _auto_play_event(self, payload: TrackEndEventPayload) -> None:
if self._autoplay is AutoPlayMode.disabled:
if not self.channel:
return

members: int = len([m for m in self.channel.members if not m.bot])
self._inactive_channel_count = (
self._inactive_channel_count - 1 if not members else self._inactive_channel_limit or 0
)

if self._inactive_channel_limit and self._inactive_channel_count <= 0:
self._inactive_channel_count = self._inactive_channel_limit # Reset...

self._inactivity_cancel()
self.client.dispatch("wavelink_inactive_player", self)

elif self._autoplay is AutoPlayMode.disabled:
self._inactivity_start()
return

Expand Down Expand Up @@ -353,7 +370,7 @@ async def _search(query: str | None) -> T_a:
return []

try:
search: wavelink.Search = await Pool.fetch_tracks(query)
search: wavelink.Search = await Pool.fetch_tracks(query, node=self._node)
except (LavalinkLoadException, LavalinkException):
return []

Expand Down Expand Up @@ -403,6 +420,49 @@ async def _search(query: str | None) -> T_a:
logger.info('Player "%s" could not load any songs via AutoPlay.', self.guild.id)
self._inactivity_start()

@property
def inactive_channel_tokens(self) -> int | None:
"""A settable property which returns the token limit as an ``int`` of the amount of tracks to play before firing
the :func:`on_wavelink_inactive_player` event when a channel is inactive.
This property could return ``None`` if the check has been disabled.
A channel is considered inactive when no real members (Members other than bots) are in the connected voice
channel. On each consecutive track played without a real member in the channel, this token bucket will reduce
by ``1``. After hitting ``0``, the :func:`on_wavelink_inactive_player` event will be fired and the token bucket
will reset to the set value. The default value for this property is ``3``.
This property can be set with any valid ``int`` or ``None``. If this property is set to ``<= 0`` or ``None``,
the check will be disabled.
Setting this property to ``1`` will fire the :func:`on_wavelink_inactive_player` event at the end of every track
if no real members are in the channel and you have not disconnected the player.
If this check successfully fires the :func:`on_wavelink_inactive_player` event, it will cancel any waiting
:attr:`inactive_timeout` checks until a new track is played.
The default for every player can be set on :class:`~wavelink.Node`.
- See: :class:`~wavelink.Node`
- See: :func:`on_wavelink_inactive_player`
.. warning::
Setting this property will reset the bucket.
.. versionadded:: 3.4.0
"""
return self._inactive_channel_limit

@inactive_channel_tokens.setter
def inactive_channel_tokens(self, value: int | None) -> None:
if not value or value <= 0:
self._inactive_channel_limit = None
return

self._inactive_channel_limit = value
self._inactive_channel_count = value

@property
def inactive_timeout(self) -> int | None:
"""A property which returns the time as an ``int`` of seconds to wait before this player dispatches the
Expand Down Expand Up @@ -616,14 +676,11 @@ async def _dispatch_voice_update(self) -> None:
assert self.guild is not None
data: VoiceState = self._voice_state["voice"]

try:
session_id: str = data["session_id"]
token: str = data["token"]
except KeyError:
return

session_id: str | None = data.get("session_id", None)
token: str | None = data.get("token", None)
endpoint: str | None = data.get("endpoint", None)
if not endpoint:

if not session_id or not token or not endpoint:
return

request: RequestPayload = {"voice": {"sessionId": session_id, "token": token, "endpoint": endpoint}}
Expand Down
12 changes: 9 additions & 3 deletions wavelink/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
if TYPE_CHECKING:
from collections.abc import Iterator

from .node import Node
from .types.tracks import (
PlaylistInfoPayload,
PlaylistPayload,
Expand Down Expand Up @@ -323,7 +324,9 @@ def raw_data(self) -> TrackPayload:
return self._raw_data

@classmethod
async def search(cls, query: str, /, *, source: TrackSource | str | None = TrackSource.YouTubeMusic) -> Search:
async def search(
cls, query: str, /, *, source: TrackSource | str | None = TrackSource.YouTubeMusic, node: Node | None = None
) -> Search:
"""Search for a list of :class:`~wavelink.Playable` or a :class:`~wavelink.Playlist`, with the given query.
.. note::
Expand Down Expand Up @@ -355,6 +358,9 @@ async def search(cls, query: str, /, *, source: TrackSource | str | None = Track
LavaSrc Spotify based search.
Defaults to :attr:`wavelink.TrackSource.YouTubeMusic` which is equivalent to "ytmsearch:".
node: :class:`~wavelink.Node` | None
An optional :class:`~wavelink.Node` to use when searching for tracks. Defaults to ``None``, which uses
the :class:`~wavelink.Pool`'s automatic node selection.
Returns
Expand Down Expand Up @@ -410,7 +416,7 @@ async def search(cls, query: str, /, *, source: TrackSource | str | None = Track
check = yarl.URL(query)

if check.host:
tracks: Search = await wavelink.Pool.fetch_tracks(query)
tracks: Search = await wavelink.Pool.fetch_tracks(query, node=node)
return tracks

if not prefix:
Expand All @@ -419,7 +425,7 @@ async def search(cls, query: str, /, *, source: TrackSource | str | None = Track
assert not isinstance(prefix, TrackSource)
term: str = f"{prefix.removesuffix(':')}:{query}"

tracks: Search = await wavelink.Pool.fetch_tracks(term)
tracks: Search = await wavelink.Pool.fetch_tracks(term, node=node)
return tracks


Expand Down

0 comments on commit f548a3b

Please sign in to comment.