Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/playable node and Inactive Channel Check #303

Merged
merged 4 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
EvieePy marked this conversation as resolved.
Show resolved Hide resolved

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
Loading