From 895c37bd805e5252c202a2007bf731abb645ccc9 Mon Sep 17 00:00:00 2001 From: EvieePy Date: Sun, 3 Dec 2023 10:20:13 +1000 Subject: [PATCH 1/6] Update README --- README.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 103 ----------------------------------------------------- 2 files changed, 91 insertions(+), 103 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 00000000..5e2d423a --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +
+ + +![Logo](https://raw.githubusercontent.com/PythonistaGuild/Wavelink/master/logo.png) + +![Python Version](https://img.shields.io/pypi/pyversions/Wavelink) +[![PyPI - Version](https://img.shields.io/pypi/v/Wavelink)](https://pypi.org/project/wavelink/) +[![Github License](https://img.shields.io/github/license/PythonistaGuild/Wavelink)](LICENSE) +[![Lavalink Version](https://img.shields.io/badge/Lavalink-v4.0%2B-blue?color=%23FB7713)](https://lavalink.dev) +![Lavalink Plugins](https://img.shields.io/badge/Lavalink_Plugins-Native_Support-blue?color=%2373D673) +[![Discord](https://img.shields.io/discord/490948346773635102?logo=discord&logoColor=%23FFF&label=Pythonista&labelColor=%235865F2&color=%232B2D31)](https://discord.gg/RAKc3HF) + + +
+ + +Wavelink is a robust and powerful Lavalink wrapper for [Discord.py](https://github.com/Rapptz/discord.py) +Wavelink features a fully asynchronous API that's intuitive and easy to use. + + +# Migrating from Version 2 to Version 3: + +[Migrating Guide](https://wavelink.dev/en/latest/migrating.html) + + +### Features + +- Full asynchronous design. +- Lavalink v4+ Supported with REST API. +- discord.py v2.0.0+ Support. +- Advanced AutoPlay and track recommendations for continuous play. +- Object orientated design with stateful objects and payloads. +- Fully annotated and complies with Pyright strict typing. + + +## Getting Started + +**See Examples:** [Examples](https://github.com/PythonistaGuild/Wavelink/tree/main/examples) + +**Lavalink:** [GitHub](https://github.com/lavalink-devs/Lavalink/releases), [Webpage](https://lavalink.dev) + + +## Documentation + +[Official Documentation](https://wavelink.dev/en/latest) + +## Support + +For support using WaveLink, please join the official [Support Server](https://discord.gg/RAKc3HF) on +[Discord](https://discordapp.com) + +[![Discord Banner](https://discordapp.com/api/guilds/490948346773635102/widget.png?style=banner2)](https://discord.gg/RAKc3HF) + + +## Installation + +**WaveLink 3 requires Python 3.10+** + +**Windows** + + +```sh +py -3.10 -m pip install -U wavelink +``` + +**Linux** + +```sh +python3.10 -m pip install -U wavelink +``` + +**Virtual Environments** + +```sh +pip install -U wavelink +``` + + +## Lavalink + +Wavelink **3** requires **Lavalink v4**. +See: [Lavalink](https://github.com/lavalink-devs/Lavalink/releases) + +For spotify support, simply install and use [LavaSrc](https://github.com/topi314/LavaSrc) with your `wavelink.Playable` + + +### Notes + +- Wavelink **3** is compatible with Lavalink **v4+**. +- Wavelink has built in support for Lavalink Plugins including LavaSrc and SponsorBlock. +- Wavelink is fully typed in compliance with Pyright Strict, though some nuances remain between discord.py and wavelink. diff --git a/README.rst b/README.rst deleted file mode 100644 index 2c39b902..00000000 --- a/README.rst +++ /dev/null @@ -1,103 +0,0 @@ -.. image:: https://raw.githubusercontent.com/PythonistaGuild/Wavelink/master/logo.png - - -.. image:: https://img.shields.io/badge/Python-3.10%20%7C%203.11-blue.svg - :target: https://www.python.org - - -.. image:: https://img.shields.io/github/license/PythonistaGuild/Wavelink.svg - :target: LICENSE - - -.. image:: https://img.shields.io/discord/490948346773635102?color=%237289DA&label=Pythonista&logo=discord&logoColor=white - :target: https://discord.gg/RAKc3HF - - -.. image:: https://img.shields.io/pypi/dm/Wavelink?color=black - :target: https://pypi.org/project/Wavelink - :alt: PyPI - Downloads - - -.. image:: https://img.shields.io/maintenance/yes/2023?color=pink&style=for-the-badge - :target: https://github.com/PythonistaGuild/Wavelink/commits/main - :alt: Maintenance - - - -Wavelink is a robust and powerful Lavalink wrapper for `Discord.py `_. -Wavelink features a fully asynchronous API that's intuitive and easy to use. - - -Migrating from Version 2 to Version 3: -###################################### - -`Migrating Guide `_ - - -**Features:** - -- Full asynchronous design. -- Lavalink v4+ Supported with REST API. -- discord.py v2.0.0+ Support. -- Advanced AutoPlay and track recommendations for continuous play. -- Object orientated design with stateful objects and payloads. -- Fully annotated and complies with Pyright strict typing. - - -Documentation -------------- -`Official Documentation `_ - -Support -------- -For support using WaveLink, please join the official `support server -`_ on `Discord `_. - -.. image:: https://discordapp.com/api/guilds/490948346773635102/widget.png?style=banner2 - :target: https://discord.gg/RAKc3HF - - -Installation ------------- -**WaveLink 3 requires Python 3.10+** - -**Windows** - -.. code:: sh - - py -3.10 -m pip install -U wavelink - -**Linux** - -.. code:: sh - - python3.10 -m pip install -U wavelink - -**Virtual Environments** - -.. code:: sh - - pip install -U wavelink - - -Getting Started ---------------- - -**See Examples:** `Examples `_ - - -Lavalink --------- - -Wavelink **3** requires **Lavalink v4**. -See: `Lavalink `_ - -For spotify support, simply install and use `LavaSrc `_ with your `wavelink.Playable` - - -Notes ------ - -- Wavelink **3** is compatible with Lavalink **v4+**. -- Wavelink has built in support for Lavalink Plugins including LavaSrc and SponsorBlock. -- Wavelink is fully typed in compliance with Pyright Strict, though some nuances remain between discord.py and wavelink. From 2b447a3c4e66196fedcb42f1cdb8adf6cdc9ded2 Mon Sep 17 00:00:00 2001 From: EvieePy Date: Sun, 3 Dec 2023 10:22:50 +1000 Subject: [PATCH 2/6] Update classifiers and .rst to .md --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 93c48007..3d6e3a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ ] dynamic = ["dependencies"] description = "A robust and powerful, fully asynchronous Lavalink wrapper built for discord.py in Python." -readme = "README.rst" +readme = "README.md" requires-python = ">=3.10" classifiers = [ "License :: OSI Approved :: MIT License", @@ -19,6 +19,7 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", From 2cf793d79eb3d517eb8cd652c6e6743fc0562a7b Mon Sep 17 00:00:00 2001 From: Mysty Date: Fri, 8 Dec 2023 02:38:45 +1000 Subject: [PATCH 3/6] Add public node endpoints with payloads. (#260) * Update response payload type. * Add new fetch endpoint payloads * Add public endpoints * Update docs for payloads. * Bump version * Add warning to docs. * Add some version ifo and migrating. * versionadd should be versionadded * Run black, --- docs/migrating.rst | 5 + docs/wavelink.rst | 40 +++++++ pyproject.toml | 2 +- wavelink/__init__.py | 2 +- wavelink/node.py | 146 +++++++++++++++++++++++ wavelink/payloads.py | 234 ++++++++++++++++++++++++++++++++++++- wavelink/types/response.py | 10 +- 7 files changed, 430 insertions(+), 9 deletions(-) diff --git a/docs/migrating.rst b/docs/migrating.rst index e2c7a09b..b445fe46 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -80,6 +80,11 @@ Added - :meth:`wavelink.Node.send` - :class:`wavelink.Search` - LFU (Least Frequently Used) Cache for request caching. +- :meth:`wavelink.Node.fetch_info` +- :meth:`wavelink.Node.fetch_stats` +- :meth:`wavelink.Node.fetch_version` +- :meth:`wavelink.Node.fetch_player_info` +- :meth:`wavelink.Node.fetch_players` Connecting diff --git a/docs/wavelink.rst b/docs/wavelink.rst index caca9279..15018dee 100644 --- a/docs/wavelink.rst +++ b/docs/wavelink.rst @@ -150,6 +150,46 @@ Payloads .. autoclass:: StatsEventFrames :members: +.. attributetable:: StatsResponsePayload + +.. autoclass:: StatsResponsePayload + :members: + +.. attributetable:: PlayerStatePayload + +.. autoclass:: PlayerStatePayload + :members: + +.. attributetable:: VoiceStatePayload + +.. autoclass:: VoiceStatePayload + :members: + +.. attributetable:: PlayerResponsePayload + +.. autoclass:: PlayerResponsePayload + :members: + +.. attributetable:: GitResponsePayload + +.. autoclass:: GitResponsePayload + :members: + +.. attributetable:: VersionResponsePayload + +.. autoclass:: VersionResponsePayload + :members: + +.. attributetable:: PluginResponsePayload + +.. autoclass:: PluginResponsePayload + :members: + +.. attributetable:: InfoResponsePayload + +.. autoclass:: InfoResponsePayload + :members: + Enums ----- diff --git a/pyproject.toml b/pyproject.toml index 3d6e3a1e..6531dcae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wavelink" -version = "3.0.0" +version = "3.1.0" authors = [ { name="PythonistaGuild, EvieePy", email="evieepy@gmail.com" }, ] diff --git a/wavelink/__init__.py b/wavelink/__init__.py index a92a252e..b97ebf3a 100644 --- a/wavelink/__init__.py +++ b/wavelink/__init__.py @@ -25,7 +25,7 @@ __author__ = "PythonistaGuild, EvieePy" __license__ = "MIT" __copyright__ = "Copyright 2019-Present (c) PythonistaGuild, EvieePy" -__version__ = "3.0.0" +__version__ = "3.1.0" from .enums import * diff --git a/wavelink/node.py b/wavelink/node.py index bfe79cd7..58533780 100644 --- a/wavelink/node.py +++ b/wavelink/node.py @@ -44,6 +44,7 @@ NodeException, ) from .lfu import LFUCache +from .payloads import * from .tracks import Playable, Playlist from .websocket import Websocket @@ -338,6 +339,9 @@ async def send( NodeException An error occured while making this request to Lavalink, and Lavalink was unable to send any error information. + + + .. versionadded:: 3.0.0 """ clean_path: str = path.removesuffix("/") uri: str = f"{self.uri}/{clean_path}" @@ -391,6 +395,36 @@ async def _fetch_players(self) -> list[PlayerResponse]: raise LavalinkException(data=exc_data) + async def fetch_players(self) -> list[PlayerResponsePayload]: + """Method to fetch the player information Lavalink holds for every connected player on this node. + + .. warning:: + + This payload is not the same as the :class:`wavelink.Player` class. This is the data received from + Lavalink about the players. + + + Returns + ------- + list[:class:`PlayerResponsePayload`] + A list of :class:`PlayerResponsePayload` representing each player connected to this node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: list[PlayerResponse] = await self._fetch_players() + + payload: list[PlayerResponsePayload] = [PlayerResponsePayload(p) for p in data] + return payload + async def _fetch_player(self, guild_id: int, /) -> PlayerResponse: uri: str = f"{self.uri}/v4/sessions/{self.session_id}/players/{guild_id}" @@ -408,6 +442,48 @@ async def _fetch_player(self, guild_id: int, /) -> PlayerResponse: raise LavalinkException(data=exc_data) + async def fetch_player_info(self, guild_id: int, /) -> PlayerResponsePayload | None: + """Method to fetch the player information Lavalink holds for the specific guild. + + .. warning:: + + This payload is not the same as the :class:`wavelink.Player` class. This is the data received from + Lavalink about the player. See: :meth:`~wavelink.Node.get_player` + + + Parameters + ---------- + guild_id: int + The ID of the guild you want to fetch info for. + + Returns + ------- + :class:`PlayerResponsePayload` | None + The :class:`PlayerResponsePayload` representing the player info for the guild ID connected to this node. + Could be ``None`` if no player is found with the given guild ID. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + try: + data: PlayerResponse = await self._fetch_player(guild_id) + except LavalinkException as e: + if e.status == 404: + return None + + raise e + + payload: PlayerResponsePayload = PlayerResponsePayload(data) + return payload + async def _update_player(self, guild_id: int, /, *, data: Request, replace: bool = False) -> PlayerResponse: no_replace: bool = not replace @@ -499,6 +575,30 @@ async def _fetch_info(self) -> InfoResponse: raise LavalinkException(data=exc_data) + async def fetch_info(self) -> InfoResponsePayload: + """Method to fetch this Lavalink Nodes info response data. + + Returns + ------- + :class:`InfoResponsePayload` + The :class:`InfoResponsePayload` associated with this Node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: InfoResponse = await self._fetch_info() + + payload: InfoResponsePayload = InfoResponsePayload(data) + return payload + async def _fetch_stats(self) -> StatsResponse: uri: str = f"{self.uri}/v4/stats" @@ -516,6 +616,30 @@ async def _fetch_stats(self) -> StatsResponse: raise LavalinkException(data=exc_data) + async def fetch_stats(self) -> StatsResponsePayload: + """Method to fetch this Lavalink Nodes stats response data. + + Returns + ------- + :class:`StatsResponsePayload` + The :class:`StatsResponsePayload` associated with this Node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: StatsResponse = await self._fetch_stats() + + payload: StatsResponsePayload = StatsResponsePayload(data) + return payload + async def _fetch_version(self) -> str: uri: str = f"{self.uri}/version" @@ -531,6 +655,28 @@ async def _fetch_version(self) -> str: raise LavalinkException(data=exc_data) + async def fetch_version(self) -> str: + """Method to fetch this Lavalink version string. + + Returns + ------- + str + The version string associated with this Lavalink node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: str = await self._fetch_version() + return data + def get_player(self, guild_id: int, /) -> Player | None: """Return a :class:`~wavelink.Player` associated with the provided :attr:`discord.Guild.id`. diff --git a/wavelink/payloads.py b/wavelink/payloads.py index 272c3cda..2edc46f2 100644 --- a/wavelink/payloads.py +++ b/wavelink/payloads.py @@ -23,16 +23,20 @@ """ from __future__ import annotations +import datetime from typing import TYPE_CHECKING, cast import wavelink from .enums import DiscordVoiceCloseType +from .filters import Filters +from .tracks import Playable if TYPE_CHECKING: from .node import Node from .player import Player - from .tracks import Playable + from .types.filters import * + from .types.response import * from .types.state import PlayerState from .types.stats import CPUStats, FrameStats, MemoryStats from .types.websocket import StatsOP, TrackExceptionPayload @@ -50,6 +54,14 @@ "StatsEventMemory", "StatsEventCPU", "StatsEventFrames", + "StatsResponsePayload", + "GitResponsePayload", + "VersionResponsePayload", + "PluginResponsePayload", + "InfoResponsePayload", + "PlayerStatePayload", + "VoiceStatePayload", + "PlayerResponsePayload", ) @@ -281,8 +293,8 @@ class StatsEventPayload: See Also: :class:`wavelink.StatsEventMemory` cpu: :class:`wavelink.StatsEventCPU` See Also: :class:`wavelink.StatsEventCPU` - frames: :class:`wavelink.StatsEventFrames` - See Also: :class:`wavelink.StatsEventFrames` + frames: :class:`wavelink.StatsEventFrames` | None + See Also: :class:`wavelink.StatsEventFrames`. This could be ``None``. """ def __init__(self, data: StatsOP) -> None: @@ -294,5 +306,217 @@ def __init__(self, data: StatsOP) -> None: self.cpu: StatsEventCPU = StatsEventCPU(data=data["cpu"]) self.frames: StatsEventFrames | None = None - if data["frameStats"]: - self.frames = StatsEventFrames(data=data["frameStats"]) + if frames := data.get("frameStats", None): + self.frames = StatsEventFrames(frames) + + +class StatsResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_stats` + + Attributes + ---------- + players: int + The amount of players connected to the node (Lavalink). + playing: int + The amount of players playing a track. + uptime: int + The uptime of the node in milliseconds. + memory: :class:`wavelink.StatsEventMemory` + See Also: :class:`wavelink.StatsEventMemory` + cpu: :class:`wavelink.StatsEventCPU` + See Also: :class:`wavelink.StatsEventCPU` + frames: :class:`wavelink.StatsEventFrames` | None + See Also: :class:`wavelink.StatsEventFrames`. This could be ``None``. + """ + + def __init__(self, data: StatsResponse) -> None: + self.players: int = data["players"] + self.playing: int = data["playingPlayers"] + self.uptime: int = data["uptime"] + + self.memory: StatsEventMemory = StatsEventMemory(data=data["memory"]) + self.cpu: StatsEventCPU = StatsEventCPU(data=data["cpu"]) + self.frames: StatsEventFrames | None = None + + if frames := data.get("frameStats", None): + self.frames = StatsEventFrames(frames) + + +class PlayerStatePayload: + """Represents the PlayerState information received via :meth:`~wavelink.Node.fetch_player_info` or + :meth:`~wavelink.Node.fetch_players` + + Attributes + ---------- + time: int + Unix timestamp in milliseconds received from Lavalink. + position: int + The position of the track in milliseconds received from Lavalink. + connected: bool + Whether Lavalink is connected to the voice gateway. + ping: int + The ping of the node to the Discord voice server in milliseconds (-1 if not connected). + """ + + def __init__(self, data: PlayerState) -> None: + self.time: int = data["time"] + self.position: int = data["position"] + self.connected: bool = data["connected"] + self.ping: int = data["ping"] + + +class VoiceStatePayload: + """Represents the VoiceState information received via :meth:`~wavelink.Node.fetch_player_info` or + :meth:`~wavelink.Node.fetch_players`. This is the voice state information received via Discord and sent to your + Lavalink node. + + Attributes + ---------- + token: str | None + The Discord voice token authenticated with. This is not the same as your bots token. Could be ``None``. + endpoint: str | None + The Discord voice endpoint connected to. Could be ``None``. + session_id: str | None + The Discord voice session ID autheticated with. Could be ``None``. + """ + + def __init__(self, data: VoiceStateResponse) -> None: + self.token: str | None = data.get("token") + self.endpoint: str | None = data.get("endpoint") + self.session_id: str | None = data.get("sessionId") + + +class PlayerResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_player_info` or :meth:`~wavelink.Node.fetch_players` + + Attributes + ---------- + guild_id: int + The guild ID as an int that this player is connected to. + track: :class:`wavelink.Playable` | None + The current track playing on Lavalink. Could be ``None`` if no track is playing. + volume: int + The current volume of the player. + paused: bool + A bool indicating whether the player is paused. + state: :class:`wavelink.PlayerStatePayload` + The current state of the player. See: :class:`wavelink.PlayerStatePayload`. + voice_state: :class:`wavelink.VoiceStatePayload` + The voice state infomration received via Discord and sent to Lavalink. See: :class:`wavelink.VoiceStatePayload`. + filters: :class:`wavelink.Filters` + The :class:`wavelink.Filters` currently associated with this player. + """ + + def __init__(self, data: PlayerResponse) -> None: + self.guild_id: int = int(data["guildId"]) + self.track: Playable | None = None + + if track := data.get("track"): + self.track = Playable(track) + + self.volume: int = data["volume"] + self.paused: bool = data["paused"] + self.state: PlayerStatePayload = PlayerStatePayload(data["state"]) + self.voice_state: VoiceStatePayload = VoiceStatePayload(data["voice"]) + self.filters: Filters = Filters(data=data["filters"]) + + +class GitResponsePayload: + """Represents Git information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + branch: str + The branch this Lavalink server was built on. + commit: str + The commit this Lavalink server was built on. + commit_time: :class:`datetime.datetime` + The timestamp for when the commit was created. + """ + + def __init__(self, data: GitPayload) -> None: + self.branch: str = data["branch"] + self.commit: str = data["commit"] + self.commit_time: datetime.datetime = datetime.datetime.fromtimestamp( + data["commitTime"] / 1000, tz=datetime.timezone.utc + ) + + +class VersionResponsePayload: + """Represents Version information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + semver: str + The full version string of this Lavalink server. + major: int + The major version of this Lavalink server. + minor: int + The minor version of this Lavalink server. + patch: int + The patch version of this Lavalink server. + pre_release: str + The pre-release version according to semver as a ``.`` separated list of identifiers. + build: str | None + The build metadata according to semver as a ``.`` separated list of identifiers. Could be ``None``. + """ + + def __init__(self, data: VersionPayload) -> None: + self.semver: str = data["semver"] + self.major: int = data["major"] + self.minor: int = data["minor"] + self.patch: int = data["patch"] + self.pre_release: str | None = data.get("preRelease") + self.build: str | None = data.get("build") + + +class PluginResponsePayload: + """Represents Plugin information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + name: str + The plugin name. + version: str + The plugin version. + """ + + def __init__(self, data: PluginPayload) -> None: + self.name: str = data["name"] + self.version: str = data["version"] + + +class InfoResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_info` + + Attributes + ---------- + version: :class:`VersionResponsePayload` + The version info payload for this Lavalink node in the :class:`VersionResponsePayload` object. + build_time: :class:`datetime.datetime` + The timestamp when this Lavalink jar was built. + git: :class:`GitResponsePayload` + The git info payload for this Lavalink node in the :class:`GitResponsePayload` object. + jvm: str + The JVM version this Lavalink node runs on. + lavaplayer: str + The Lavaplayer version being used by this Lavalink node. + source_managers: list[str] + The enabled source managers for this node. + filters: list[str] + The enabled filters for this node. + plugins: list[:class:`PluginResponsePayload`] + The enabled plugins for this node. + """ + + def __init__(self, data: InfoResponse) -> None: + self.version: VersionResponsePayload = VersionResponsePayload(data["version"]) + self.build_time: datetime.datetime = datetime.datetime.fromtimestamp( + data["buildTime"] / 1000, tz=datetime.timezone.utc + ) + self.git: GitResponsePayload = GitResponsePayload(data["git"]) + self.jvm: str = data["jvm"] + self.lavaplayer: str = data["lavaplayer"] + self.source_managers: list[str] = data["sourceManagers"] + self.filters: list[str] = data["filters"] + self.plugins: list[PluginResponsePayload] = [PluginResponsePayload(p) for p in data["plugins"]] diff --git a/wavelink/types/response.py b/wavelink/types/response.py index a2e73ba6..4ef88e21 100644 --- a/wavelink/types/response.py +++ b/wavelink/types/response.py @@ -27,7 +27,7 @@ from typing_extensions import Never, NotRequired from .filters import FilterPayload - from .state import PlayerState, VoiceState + from .state import PlayerState from .stats import CPUStats, FrameStats, MemoryStats from .tracks import PlaylistPayload, TrackPayload @@ -47,13 +47,19 @@ class LoadedErrorPayload(TypedDict): cause: str +class VoiceStateResponse(TypedDict, total=False): + token: str + endpoint: str | None + sessionId: str + + class PlayerResponse(TypedDict): guildId: str track: NotRequired[TrackPayload] volume: int paused: bool state: PlayerState - voice: VoiceState + voice: VoiceStateResponse filters: FilterPayload From a9ed696a77dba3f504a75ea93a22cb297522c8f5 Mon Sep 17 00:00:00 2001 From: Mysty Date: Fri, 8 Dec 2023 21:38:33 +1000 Subject: [PATCH 4/6] Add extras for userData in Lavalink (#261) * Add extras for userData in Lavalink * Add migrating docs --- docs/migrating.rst | 1 + docs/wavelink.rst | 9 ++++++ wavelink/__init__.py | 1 + wavelink/player.py | 4 +-- wavelink/tracks.py | 47 +++++++++++++++++++++++++++++ wavelink/types/request.py | 9 +++++- wavelink/types/tracks.py | 1 + wavelink/utils.py | 62 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 wavelink/utils.py diff --git a/docs/migrating.rst b/docs/migrating.rst index b445fe46..989f77c3 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -85,6 +85,7 @@ Added - :meth:`wavelink.Node.fetch_version` - :meth:`wavelink.Node.fetch_player_info` - :meth:`wavelink.Node.fetch_players` +- :attr:`wavelink.Playable.extras` Connecting diff --git a/docs/wavelink.rst b/docs/wavelink.rst index 15018dee..e4c25d9b 100644 --- a/docs/wavelink.rst +++ b/docs/wavelink.rst @@ -347,6 +347,15 @@ Filters :members: +Utils +----- + +.. attributetable:: ExtrasNamespace + +.. autoclass:: ExtrasNamespace + + + Exceptions ---------- diff --git a/wavelink/__init__.py b/wavelink/__init__.py index b97ebf3a..40cfde86 100644 --- a/wavelink/__init__.py +++ b/wavelink/__init__.py @@ -38,3 +38,4 @@ from .player import Player as Player from .queue import * from .tracks import * +from .utils import ExtrasNamespace as ExtrasNamespace diff --git a/wavelink/player.py b/wavelink/player.py index 6d0fb59a..88035f0f 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -667,7 +667,7 @@ async def play( self._filters = filters request: RequestPayload = { - "encodedTrack": track.encoded, + "track": {"encoded": track.encoded, "userData": dict(track.extras)}, "volume": vol, "position": start, "endTime": end, @@ -850,7 +850,7 @@ async def skip(self, *, force: bool = True) -> Playable | None: if force: self.queue._loaded = None - request: RequestPayload = {"encodedTrack": None} + request: RequestPayload = {"track": {"encoded": None}} await self.node._update_player(self.guild.id, data=request, replace=True) return old diff --git a/wavelink/tracks.py b/wavelink/tracks.py index 42283a86..8638fa2d 100644 --- a/wavelink/tracks.py +++ b/wavelink/tracks.py @@ -31,6 +31,7 @@ import wavelink from .enums import TrackSource +from .utils import ExtrasNamespace if TYPE_CHECKING: from .types.tracks import ( @@ -134,6 +135,8 @@ def __init__(self, data: TrackPayload, *, playlist: PlaylistInfo | None = None) self._playlist = playlist self._recommended: bool = False + self._extras: ExtrasNamespace = ExtrasNamespace(data.get("userData", {})) + def __hash__(self) -> int: return hash(self.encoded) @@ -247,6 +250,50 @@ def recommended(self) -> bool: """Property returning a bool indicating whether this track was recommended via AutoPlay.""" return self._recommended + @property + def extras(self) -> ExtrasNamespace: + """Property returning a :class:`~wavelink.ExtrasNamespace` of extras for this :class:`Playable`. + + You can set this property with a :class:`dict` of valid :class:`str` keys to any valid ``JSON`` value, + or a :class:`~wavelink.ExtrasNamespace`. + + If a dict is passed, it will be converted into an :class:`~wavelink.ExtrasNamespace`, + which can be converted back to a dict with dict(...). Additionally, you can also use list or tuple on + :class:`~wavelink.ExtrasNamespace`. + + The extras dict will be sent to Lavalink as the ``userData`` field. + + + .. warning:: + + This is only available when using Lavalink 4+ (**Non BETA**) versions. + + + Examples + -------- + + .. code:: python + + track: wavelink.Playable = wavelink.Playable.search("QUERY") + track.extras = {"requester_id": 1234567890} + + # later... + print(track.extras.requester_id) + # or + print(dict(track.extras)["requester_id"]) + + + .. versionadded:: 3.1.0 + """ + return self._extras + + @extras.setter + def extras(self, __value: ExtrasNamespace | dict[str, Any]) -> None: + if isinstance(__value, ExtrasNamespace): + self._extras = __value + else: + self._extras = ExtrasNamespace(__value) + @classmethod async def search(cls, query: str, /, *, source: TrackSource | str | None = TrackSource.YouTubeMusic) -> Search: """Search for a list of :class:`~wavelink.Playable` or a :class:`~wavelink.Playlist`, with the given query. diff --git a/wavelink/types/request.py b/wavelink/types/request.py index 5325eb5f..8a4b9775 100644 --- a/wavelink/types/request.py +++ b/wavelink/types/request.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, TypeAlias, TypedDict +from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict if TYPE_CHECKING: from typing_extensions import NotRequired @@ -37,6 +37,12 @@ class VoiceRequest(TypedDict): sessionId: str +class TrackRequest(TypedDict, total=False): + encoded: str | None + identifier: str + userData: dict[str, Any] + + class _BaseRequest(TypedDict, total=False): voice: VoiceRequest position: int @@ -44,6 +50,7 @@ class _BaseRequest(TypedDict, total=False): volume: int paused: bool filters: FilterPayload + track: TrackRequest class EncodedTrackRequest(_BaseRequest): diff --git a/wavelink/types/tracks.py b/wavelink/types/tracks.py index b4c6fdda..4825abaf 100644 --- a/wavelink/types/tracks.py +++ b/wavelink/types/tracks.py @@ -50,6 +50,7 @@ class TrackPayload(TypedDict): encoded: str info: TrackInfoPayload pluginInfo: dict[Any, Any] + userData: dict[str, Any] class PlaylistPayload(TypedDict): diff --git a/wavelink/utils.py b/wavelink/utils.py new file mode 100644 index 00000000..a3c85f32 --- /dev/null +++ b/wavelink/utils.py @@ -0,0 +1,62 @@ +""" +MIT License + +Copyright (c) 2019-Current PythonistaGuild, EvieePy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from types import SimpleNamespace +from typing import Any, Iterator + +__all__ = ( + "Namespace", + "ExtrasNamespace", +) + + +class Namespace(SimpleNamespace): + def __iter__(self) -> Iterator[tuple[str, Any]]: + return iter(self.__dict__.items()) + + +class ExtrasNamespace(Namespace): + """A subclass of :class:`types.SimpleNameSpace`. + + You can construct this namespace with a :class:`dict` of `str` keys and `Any` value, or with keyword pairs or + with a mix of both. + + You can access a dict version of this namespace by calling `dict()` on an instance. + + + Examples + -------- + + .. code:: python + + ns: ExtrasNamespace = ExtrasNamespace({"hello": "world!"}, stuff=1) + + # Later... + print(ns.hello) + print(ns.stuff) + print(dict(ns)) + """ + + def __init__(self, __dict: dict[str, Any] = {}, /, **kwargs: Any) -> None: + updated = __dict | kwargs + super().__init__(**updated) From 21de582202bebc5f65b171b28334c5216899f899 Mon Sep 17 00:00:00 2001 From: EvieePy Date: Sat, 9 Dec 2023 15:53:56 +1000 Subject: [PATCH 5/6] Add extra_event for unknown plugin events --- docs/wavelink.rst | 14 ++++++++++++++ wavelink/payloads.py | 28 ++++++++++++++++++++++++++++ wavelink/websocket.py | 3 ++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/wavelink.rst b/docs/wavelink.rst index e4c25d9b..939c2c87 100644 --- a/docs/wavelink.rst +++ b/docs/wavelink.rst @@ -77,6 +77,15 @@ An event listener in a cog. Called when a node has been closed and cleaned-up. The second parameter ``disconnected`` is a list of :class:`wavelink.Player` that were connected on this Node and are now disconnected. +.. function:: on_wavelink_extra_event(payload: wavelink.ExtraEventPayload) + + Called when an ``Unknown`` and/or ``Unhandled`` event is recevied via Lavalink. This is most likely due to + a plugin like SponsorBlock sending custom event data. The payload includes the raw data sent from Lavalink. + + .. note:: + + Please see the documentation for your Lavalink plugins to determine what data they send. + Types ----- @@ -190,6 +199,11 @@ Payloads .. autoclass:: InfoResponsePayload :members: +.. attributetable:: ExtraEventPayload + +.. autoclass:: ExtraEventPayload + :members: + Enums ----- diff --git a/wavelink/payloads.py b/wavelink/payloads.py index 2edc46f2..cef40d23 100644 --- a/wavelink/payloads.py +++ b/wavelink/payloads.py @@ -62,6 +62,7 @@ "PlayerStatePayload", "VoiceStatePayload", "PlayerResponsePayload", + "ExtraEventPayload", ) @@ -520,3 +521,30 @@ def __init__(self, data: InfoResponse) -> None: self.source_managers: list[str] = data["sourceManagers"] self.filters: list[str] = data["filters"] self.plugins: list[PluginResponsePayload] = [PluginResponsePayload(p) for p in data["plugins"]] + + +class ExtraEventPayload: + """Payload received in the :func:`on_wavelink_extra_event` event. + + This payload is created when an ``Unknown`` and ``Unhandled`` event is received from Lavalink, most likely via + a plugin. + + .. note:: + + See the appropriate documentation of the plugin for the data sent with these events. + + + Attributes + ---------- + node: :class:`~wavelink.Node` + The node that the event pertains to. + player: :class:`~wavelink.Player` | None + The player associated with this event. Could be None. + data: dict[str, Any] + The raw data sent from Lavalink for this event. + """ + + def __init__(self, *, node: Node, player: Player | None, data: dict[str, Any]) -> None: + self.node = node + self.player = player + self.data = data diff --git a/wavelink/websocket.py b/wavelink/websocket.py index 96222563..c13b5e8f 100644 --- a/wavelink/websocket.py +++ b/wavelink/websocket.py @@ -239,7 +239,8 @@ async def keep_alive(self) -> None: self.dispatch("websocket_closed", wcpayload) else: - logger.debug(f"Received unknown event type from Lavalink '{data['type']}'. Disregarding.") + other_payload: ExtraEventPayload = ExtraEventPayload(node=self.node, player=player, data=data) + self.dispatch("extra_event", other_payload) else: logger.debug(f"'Received an unknown OP from Lavalink '{data['op']}'. Disregarding.") From 59efffa71b76e51c905a27e10150a205547d224f Mon Sep 17 00:00:00 2001 From: EvieePy Date: Sat, 9 Dec 2023 15:57:59 +1000 Subject: [PATCH 6/6] Added some additional documentation --- docs/migrating.rst | 1 + docs/wavelink.rst | 3 +++ wavelink/payloads.py | 3 +++ 3 files changed, 7 insertions(+) diff --git a/docs/migrating.rst b/docs/migrating.rst index 989f77c3..3bc7ad07 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -86,6 +86,7 @@ Added - :meth:`wavelink.Node.fetch_player_info` - :meth:`wavelink.Node.fetch_players` - :attr:`wavelink.Playable.extras` +- :func:`wavelink.on_wavelink_extra_event` Connecting diff --git a/docs/wavelink.rst b/docs/wavelink.rst index 939c2c87..16dbe552 100644 --- a/docs/wavelink.rst +++ b/docs/wavelink.rst @@ -85,6 +85,9 @@ An event listener in a cog. .. note:: Please see the documentation for your Lavalink plugins to determine what data they send. + + + .. versionadded:: 3.1.0 Types diff --git a/wavelink/payloads.py b/wavelink/payloads.py index cef40d23..e869e37a 100644 --- a/wavelink/payloads.py +++ b/wavelink/payloads.py @@ -542,6 +542,9 @@ class ExtraEventPayload: The player associated with this event. Could be None. data: dict[str, Any] The raw data sent from Lavalink for this event. + + + .. versionadded:: 3.1.0 """ def __init__(self, *, node: Node, player: Player | None, data: dict[str, Any]) -> None: