diff --git a/wavelink/node.py b/wavelink/node.py index df388b07..5c7fb1ab 100644 --- a/wavelink/node.py +++ b/wavelink/node.py @@ -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`. """ @@ -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("/") @@ -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)})" @@ -895,7 +901,7 @@ 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 @@ -903,6 +909,9 @@ async def fetch_tracks(cls, query: str, /) -> list[Playable] | Playlist: 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 ------- @@ -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. @@ -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"]) diff --git a/wavelink/player.py b/wavelink/player.py index 49e283a0..d961ccb7 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -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... @@ -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 @@ -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 [] @@ -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 @@ -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}} diff --git a/wavelink/tracks.py b/wavelink/tracks.py index c8571488..83b76d27 100644 --- a/wavelink/tracks.py +++ b/wavelink/tracks.py @@ -37,6 +37,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from .node import Node from .types.tracks import ( PlaylistInfoPayload, PlaylistPayload, @@ -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:: @@ -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 @@ -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: @@ -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