From 1f9bb4f5880b25c46857b049c91bc163c0917015 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 20:12:35 +0300 Subject: [PATCH 01/16] Add compare meths for queue --- harmonize/connection/transport.py | 27 +++++++++++---------------- harmonize/player.py | 26 +++++++++----------------- harmonize/queue.py | 2 +- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/harmonize/connection/transport.py b/harmonize/connection/transport.py index a6df13c..6a3c4ab 100644 --- a/harmonize/connection/transport.py +++ b/harmonize/connection/transport.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from asyncio import sleep from typing import TYPE_CHECKING, Optional @@ -17,7 +18,7 @@ from harmonize.abstract.serializable import Serializable from harmonize.enums import NodeStatus, EndReason, Severity from harmonize.exceptions import AuthorizationError, NodeUnknownError, Forbidden, RequestError -from harmonize.objects import Stats +from harmonize.objects import Stats, Track if TYPE_CHECKING: from harmonize.connection.node import Node @@ -97,9 +98,8 @@ async def _connect_back(self) -> None: f"was success to successfully connect/reconnect to Lavalink V4 after " f'{retries} connection attempts.' ) - self.dispatch("node_ready", self._node) - await self._listen() + asyncio.run_coroutine_threadsafe(self._listen(), loop=asyncio.get_event_loop()) break if self._retries <= retries: @@ -125,7 +125,7 @@ async def connect(self) -> None: await self._connect_back() except Exception as e: - logger.warning(f"Connection timeout to Lavalink V4: {e}") + logger.warning(f"An error ({type(e).__name__}) was thrown when connecting: {e}") async def _handle_event(self, data: dict[any, any]) -> None: player = self._node.players.get(int(data['guildId'])) # type: ignore @@ -137,6 +137,11 @@ async def _handle_event(self, data: dict[any, any]) -> None: return + try: + await player.handle_event(data) + except Exception as e: + logger.error(f'Player {player.guild.id} threw an error whilst handling event : {e}') + if event_type == 'TrackStartEvent': self.dispatch( "track_start", @@ -144,12 +149,11 @@ async def _handle_event(self, data: dict[any, any]) -> None: player.queue.current ) elif event_type == 'TrackEndEvent': - end_reason = EndReason(data['reason']) self.dispatch( "track_end", player, - player.queue.history[0], - end_reason + Track.from_dict(data["track"]), + EndReason(data["reason"]) ) elif event_type == 'TrackExceptionEvent': exception = data['exception'] @@ -171,15 +175,6 @@ async def _handle_event(self, data: dict[any, any]) -> None: else: return self.dispatch("extra_event", event_type, player, data) - if player and event_type in ( - 'TrackStuckEvent', - 'TrackEndEvent' - ): - try: - await player.handle_event(EndReason(data['reason'])) - except Exception as e: - logger.error(f'Player {player.guild.id} threw an error whilst handling event : {e}') - async def _handle_message(self, data: dict[any, any] | list[any]) -> None: if not isinstance(data, dict) or 'op' not in data: return diff --git a/harmonize/player.py b/harmonize/player.py index 0464e3e..43a6446 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -150,7 +150,6 @@ async def on_voice_state_update(self, data: dict) -> None: if data['session_id'] != self._voice_state.get('sessionId'): self._voice_state.update(sessionId=data['session_id']) - await self._dispatch_voice_update() async def _dispatch_voice_update(self) -> None: @@ -158,35 +157,28 @@ async def _dispatch_voice_update(self) -> None: await self._node.update_player(guild_id=self.guild.id, voice_state=self._voice_state) self._connection_event.set() - async def handle_event(self, reason: EndReason) -> None: + async def handle_event(self, data: dict[any, any]) -> None: """|coro| - Handles an event triggered by the player, such as a track finishing or a load failure. - Note ---- - This function is required for autoplay please do not touch it for personal use + This function is used in the processing of events within the player Parameters ---------- - reason : :class:`harmonize.enums.EndReason` - The reason for the event. + data : dict[any, any] + The event data from lavalink. See Returns ------- None """ - if ( - reason.value == EndReason.FINISHED.value - or reason.value == EndReason.LOAD_FAILED.value - ): - try: + match data["type"]: + case "TrackStuckEvent": await self.play() - except RequestError as error: - logger.error( - 'Encountered a request error whilst ' - f'starting a new track on guild ({self.guild.id}) {error}' - ) + case "TrackEndEvent": + if data["reason"] in ('finished', 'loadFailed'): + await self.play() async def update_state(self, state: dict) -> None: """|coro| diff --git a/harmonize/queue.py b/harmonize/queue.py index 73af7c9..047a4cf 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -33,7 +33,7 @@ class Queue: Checks if two filters are not the same. - .. desribe:: x >= y + .. describe:: x >= y Checks if the first queue is greater or equal to the second queue. From 3a97f16616f5c51ac499c50ef6a6cf80c57adff1 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 21:08:25 +0300 Subject: [PATCH 02/16] Some trash --- harmonize/connection/transport.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/harmonize/connection/transport.py b/harmonize/connection/transport.py index 6a3c4ab..a84e55b 100644 --- a/harmonize/connection/transport.py +++ b/harmonize/connection/transport.py @@ -192,6 +192,8 @@ async def _handle_message(self, data: dict[any, any] | list[any]) -> None: if player := self._node.players.get(int(data['guildId'])): await player.update_state(data['state']) self.dispatch('player_update', player) + else: + await self._node.destroy_player(int(data['guildId'])) elif data["op"] == 'event': await self._handle_event(data) else: From 8d282c32c419681bac40b82b4c0f3f22609143ff Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 21:12:27 +0300 Subject: [PATCH 03/16] Some trash --- harmonize/player.py | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/harmonize/player.py b/harmonize/player.py index 43a6446..6e33d7f 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -159,6 +159,7 @@ async def _dispatch_voice_update(self) -> None: async def handle_event(self, data: dict[any, any]) -> None: """|coro| + Handles events received from the data source. Note ---- @@ -167,7 +168,7 @@ async def handle_event(self, data: dict[any, any]) -> None: Parameters ---------- data : dict[any, any] - The event data from lavalink. See + The event data from `Lavalink `_ Returns ------- diff --git a/pyproject.toml b/pyproject.toml index 004b206..36e1e79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ async-timeout = "^4.0.2" [project] name = "harmonize.py" -version = "1.0.1" +version = "1.1.0" authors = [ { name = "Krista", email = "contactchisato@gmail.com" }, ] From a0a7eec762fb8e38ced12b68ea4e90c5bbfb2c70 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 22:25:57 +0300 Subject: [PATCH 04/16] Some shit --- docs/abstract.rst | 3 +++ harmonize/abstract/__init__.py | 1 + harmonize/abstract/queue.py | 38 ++++++++++++++++++++++++++++++++++ harmonize/connection/node.py | 23 +++++++------------- harmonize/player.py | 30 ++++++++++++++++----------- harmonize/queue.py | 28 ++++++++++++++++++++++--- 6 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 harmonize/abstract/queue.py diff --git a/docs/abstract.rst b/docs/abstract.rst index 209b901..4a820cc 100644 --- a/docs/abstract.rst +++ b/docs/abstract.rst @@ -8,3 +8,6 @@ Abstract .. autoclass:: Serializable :members: + +.. autoclass:: BaseQueue + :members: diff --git a/harmonize/abstract/__init__.py b/harmonize/abstract/__init__.py index c8414b0..bd2040e 100644 --- a/harmonize/abstract/__init__.py +++ b/harmonize/abstract/__init__.py @@ -1,2 +1,3 @@ from .filter import Filter from .serializable import Serializable +from .queue import BaseQueue diff --git a/harmonize/abstract/queue.py b/harmonize/abstract/queue.py new file mode 100644 index 0000000..3cfaf2c --- /dev/null +++ b/harmonize/abstract/queue.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from harmonize.objects import Track + + +class BaseQueue: + """ + Base class for a queue that manages a list of tracks. + """ + + def load_next(self, track: Optional[Track]) -> any: + """ + Loads the next track in the queue. + + Parameters + ---------- + track : Optional[:class:`harmonize.objects.Track`] + The next track to load. Defaults to None. + + Returns + ------- + The next track in the queue, or None if there are no more tracks. + """ + ... + + @property + def current(self) -> Track: + """ + Returns the current track in the queue. + + Returns + ------- + The current track in the queue, or None if there is no current track. + """ + return ... diff --git a/harmonize/connection/node.py b/harmonize/connection/node.py index bfaa8c0..158511c 100644 --- a/harmonize/connection/node.py +++ b/harmonize/connection/node.py @@ -26,7 +26,7 @@ class Node: """Represents a lavalink node - + Operations ---------- .. describe:: x == y @@ -40,7 +40,7 @@ class Node: .. describe:: hash(x) Return the node's hash. - + Attributes ---------- identifier : str @@ -77,15 +77,6 @@ class Node: @classmethod def _load_cache(cls, capacity: int) -> None: - """ - Initializes the cache with the specified capacity. - - Args: - capacity (int): The capacity of the cache. - - Returns: - None - """ if cls.__cache is None: cls.__cache = LFUCache(capacity=capacity) @@ -497,7 +488,7 @@ async def get_player(self, guild_id: Union[str, int]) -> dict[str, any]: async def get_players(self) -> list[dict[str, any]]: """|coro| - + Retrieves a list of players associated with the session ID. Returns @@ -540,9 +531,9 @@ async def update_player( **kwargs ) -> Optional[dict[str, any]]: """|coro| - + Updates the state of a player with the given guild ID. - + Parameters ---------- guild_id : Union[str, int] @@ -572,12 +563,12 @@ async def update_player( If not specified, no additional user data will be associated. **kwargs Additional keyword arguments to pass to the request. - + Returns ------- Optional[dict[str, any]] The updated player information, or None if no update was made. - + Raises ------ InvalidSession diff --git a/harmonize/player.py b/harmonize/player.py index 6e33d7f..daf6607 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -9,7 +9,7 @@ from disnake import VoiceProtocol, Client, VoiceState from loguru import logger -from harmonize.abstract import Filter +from harmonize.abstract import Filter, BaseQueue from harmonize.connection import Pool from harmonize.enums import EndReason, LoopStatus from harmonize.exceptions import RequestError, InvalidChannelStateException @@ -99,7 +99,7 @@ def __init__(self, *args, **kwargs) -> None: self.last_position: int = 0 self.last_update: int = 0 - self._queue: Queue = Queue(self) + self._queue: BaseQueue = Queue(self) self._filters: dict[str, Filter] = {} super().__init__(*args, **kwargs) @@ -129,9 +129,15 @@ def volume(self) -> int: return self._volume @property - def queue(self) -> Queue: + def queue(self) -> BaseQueue: return self._queue + @queue.setter + def queue(self, value: BaseQueue) -> None: + if not isinstance(value, BaseQueue): + raise TypeError('Queue must be an instance of BaseQueue') + self._queue = value + @property def filters(self) -> list[Filter]: return list(self._filters.values()) @@ -292,7 +298,7 @@ async def _play_back( options['paused'] = pause - if track := await self._queue._go_to_next(track): + if track := await self._queue.load_next(track): options["encoded_track"] = track.encoded return await self._node.update_player( @@ -799,7 +805,14 @@ async def move_to( Moves the player to a specified voice channel. - Args: + Note + ---- + This method will clear the `_connection_event` event + and wait for the player to connect to the specified channel. + If the connection attempt times out or is cancelled, the player will be destroyed. + + Parameters + ---------- channel : VocalGuildChannel | None The voice channel to move the player to. If `None`, the player will remain in its current channel. timeout : Optional[float] @@ -820,13 +833,6 @@ async def move_to( Raises ------ InvalidChannelStateException: If the player tries to move without a valid guild or channel. - - Note - ----- - This method will clear the `_connection_event` event - and wait for the player to connect to the specified channel. - If the connection attempt times out or is cancelled, the player will be destroyed. - """ if not self.guild: raise InvalidChannelStateException("Player tried to move without a valid guild.") diff --git a/harmonize/queue.py b/harmonize/queue.py index 047a4cf..45247f9 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -3,6 +3,7 @@ from random import shuffle from typing import TYPE_CHECKING, Optional, overload, Iterator +from harmonize.abstract import BaseQueue from harmonize.enums import LoopStatus from harmonize.objects import MISSING @@ -15,7 +16,7 @@ ) -class Queue: +class Queue(BaseQueue): """ Represents a queue of tracks for a Discord voice player. @@ -211,7 +212,28 @@ def reverse(self) -> None: """ self._now.reverse() - async def _go_to_next(self, track: Optional[Track] = MISSING) -> Optional[Track]: + async def load_next(self, track: Optional[Track] = MISSING) -> Optional[Track]: + """ + Loads the next track in the queue and updates the current track. + + Parameters + ---------- + track : Optional[Track + The next track to load. Defaults to None. + + Returns + ------- + Optional[Track] + The previous current track if it was replaced, otherwise None. + + Note + ---- + - If the loop status is set to TRACK, the current track will be used as the next track if no other track is provided. + + - If the loop status is set to QUEUE, the current track will be added to the end of the queue. + + - If the queue is empty and no next track is provided, the player will stop. + """ self._listened_count += 1 old = self._current @@ -234,7 +256,7 @@ async def _go_to_next(self, track: Optional[Track] = MISSING) -> Optional[Track] self._history.insert(0, old) self._current = track - return track + return old def __repr__(self) -> str: return ( From 06e63efba15dc796def647ce73492155faa4ef5f Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 22:32:03 +0300 Subject: [PATCH 05/16] Tests --- harmonize/connection/transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/harmonize/connection/transport.py b/harmonize/connection/transport.py index a84e55b..9a370a2 100644 --- a/harmonize/connection/transport.py +++ b/harmonize/connection/transport.py @@ -192,8 +192,8 @@ async def _handle_message(self, data: dict[any, any] | list[any]) -> None: if player := self._node.players.get(int(data['guildId'])): await player.update_state(data['state']) self.dispatch('player_update', player) - else: - await self._node.destroy_player(int(data['guildId'])) + # else: + # await self._node.destroy_player(int(data['guildId'])) elif data["op"] == 'event': await self._handle_event(data) else: From 7e33ba9c8ea038d1608faba9d429be1295c70f51 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 22:45:39 +0300 Subject: [PATCH 06/16] Remove my shit --- docs/abstract.rst | 3 --- harmonize/abstract/__init__.py | 1 - harmonize/abstract/queue.py | 38 ---------------------------------- harmonize/player.py | 10 ++++----- harmonize/queue.py | 3 +-- 5 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 harmonize/abstract/queue.py diff --git a/docs/abstract.rst b/docs/abstract.rst index 4a820cc..209b901 100644 --- a/docs/abstract.rst +++ b/docs/abstract.rst @@ -8,6 +8,3 @@ Abstract .. autoclass:: Serializable :members: - -.. autoclass:: BaseQueue - :members: diff --git a/harmonize/abstract/__init__.py b/harmonize/abstract/__init__.py index bd2040e..c8414b0 100644 --- a/harmonize/abstract/__init__.py +++ b/harmonize/abstract/__init__.py @@ -1,3 +1,2 @@ from .filter import Filter from .serializable import Serializable -from .queue import BaseQueue diff --git a/harmonize/abstract/queue.py b/harmonize/abstract/queue.py deleted file mode 100644 index 3cfaf2c..0000000 --- a/harmonize/abstract/queue.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional - -if TYPE_CHECKING: - from harmonize.objects import Track - - -class BaseQueue: - """ - Base class for a queue that manages a list of tracks. - """ - - def load_next(self, track: Optional[Track]) -> any: - """ - Loads the next track in the queue. - - Parameters - ---------- - track : Optional[:class:`harmonize.objects.Track`] - The next track to load. Defaults to None. - - Returns - ------- - The next track in the queue, or None if there are no more tracks. - """ - ... - - @property - def current(self) -> Track: - """ - Returns the current track in the queue. - - Returns - ------- - The current track in the queue, or None if there is no current track. - """ - return ... diff --git a/harmonize/player.py b/harmonize/player.py index daf6607..a424dd0 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -99,7 +99,7 @@ def __init__(self, *args, **kwargs) -> None: self.last_position: int = 0 self.last_update: int = 0 - self._queue: BaseQueue = Queue(self) + self._queue: Queue = Queue(self) self._filters: dict[str, Filter] = {} super().__init__(*args, **kwargs) @@ -129,13 +129,13 @@ def volume(self) -> int: return self._volume @property - def queue(self) -> BaseQueue: + def queue(self) -> Queue: return self._queue @queue.setter - def queue(self, value: BaseQueue) -> None: - if not isinstance(value, BaseQueue): - raise TypeError('Queue must be an instance of BaseQueue') + def queue(self, value: Queue) -> None: + if not isinstance(value, Queue): + raise TypeError('Queue must be an instance of Queue') self._queue = value @property diff --git a/harmonize/queue.py b/harmonize/queue.py index 45247f9..3051810 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -3,7 +3,6 @@ from random import shuffle from typing import TYPE_CHECKING, Optional, overload, Iterator -from harmonize.abstract import BaseQueue from harmonize.enums import LoopStatus from harmonize.objects import MISSING @@ -16,7 +15,7 @@ ) -class Queue(BaseQueue): +class Queue: """ Represents a queue of tracks for a Discord voice player. From 24ef5ffc24fa1cae0fdcd61fa49e8b2c34addf8e Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 22:46:59 +0300 Subject: [PATCH 07/16] Imports --- harmonize/player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/harmonize/player.py b/harmonize/player.py index a424dd0..34cec62 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -9,10 +9,10 @@ from disnake import VoiceProtocol, Client, VoiceState from loguru import logger -from harmonize.abstract import Filter, BaseQueue +from harmonize.abstract import Filter from harmonize.connection import Pool -from harmonize.enums import EndReason, LoopStatus -from harmonize.exceptions import RequestError, InvalidChannelStateException +from harmonize.enums import LoopStatus +from harmonize.exceptions import InvalidChannelStateException from harmonize.objects import Track, MISSING from harmonize.queue import Queue From 4e960f885f5e6aee17047deed31905027756fd05 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 23:08:33 +0300 Subject: [PATCH 08/16] LUL, I BREAK IT --- harmonize/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index 3051810..3ebead6 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -255,7 +255,7 @@ async def load_next(self, track: Optional[Track] = MISSING) -> Optional[Track]: self._history.insert(0, old) self._current = track - return old + return track def __repr__(self) -> str: return ( From 2926da5953de2aa69358f57a4cc44e8bfd4bda11 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Tue, 17 Sep 2024 23:36:29 +0300 Subject: [PATCH 09/16] Fix docs --- docs/events.rst | 5 ++++- harmonize/connection/transport.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/events.rst b/docs/events.rst index fa07023..3641055 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -50,7 +50,10 @@ An event listener in a cog. Called when an audio WebSocket (to Discord) is closed. This can happen for various reasons (normal and abnormal), e.g. when using an expired voice server update. 4xxx codes are usually bad. - See the `Discord Docs `_. + + .. note:: + + See the `Discord Docs `_. .. function:: on_harmonize_extra_event(event_type: str, player: harmonize.Player, data: str) diff --git a/harmonize/connection/transport.py b/harmonize/connection/transport.py index 9a370a2..a219b36 100644 --- a/harmonize/connection/transport.py +++ b/harmonize/connection/transport.py @@ -171,6 +171,38 @@ async def _handle_event(self, data: dict[any, any]) -> None: assert player.queue.current is not None self.dispatch("track_stuck", player, player.queue.current, int(data['thresholdMs'])) elif event_type == 'WebSocketClosedEvent': + """ + +------+---------------------------+----------------------------------------------------------+ + | CODE | DESCRIPTION | EXPLANATION | + +======+===========================+==========================================================+ + | 4001 | Unknown opcode | You sent an invalid opcode. | + +------+---------------------------+----------------------------------------------------------+ + | 4002 | Failed to decode payload | You sent an invalid payload in your identifying to the | + | | | Gateway. | + +------+---------------------------+----------------------------------------------------------+ + | 4003 | Not authenticated | You sent a payload before identifying with the Gateway. | + +------+---------------------------+----------------------------------------------------------+ + | 4004 | Authentication failed | The token you sent in your identify payload is incorrect.| + +------+---------------------------+----------------------------------------------------------+ + | 4005 | Already authenticated | You sent more than one identify payload. Stahp. | + +------+---------------------------+----------------------------------------------------------+ + | 4006 | Session no longer valid | Your session is no longer valid. | + +------+---------------------------+----------------------------------------------------------+ + | 4009 | Session timeout | Your session has timed out. | + +------+---------------------------+----------------------------------------------------------+ + | 4011 | Server not found | We can't find the server you're trying to connect to. | + +------+---------------------------+----------------------------------------------------------+ + | 4012 | Unknown protocol | We didn't recognize the protocol you sent. | + +------+---------------------------+----------------------------------------------------------+ + | 4014 | Disconnected | Channel was deleted, you were kicked, voice server | + | | | changed, or the main gateway session was dropped. | + | | | Should not reconnect. | + +------+---------------------------+----------------------------------------------------------+ + | 4015 | Voice server crashed | The server crashed. Our bad! Try resuming. | + +------+---------------------------+----------------------------------------------------------+ + | 4016 | Unknown encryption mode | We didn't recognize your encryption. | + +------+---------------------------+----------------------------------------------------------+ + """ self.dispatch("discord_ws_closed", player, int(data['code']), data['reason'], bool(data['byRemote'])) else: return self.dispatch("extra_event", event_type, player, data) From 5c35d0b09bde132877026224fff9fc75de3c9848 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Thu, 19 Sep 2024 22:06:25 +0300 Subject: [PATCH 10/16] Changes --- docs/events.rst | 28 +++++++++++++++++++++++ harmonize/connection/transport.py | 37 ++++++++++++++++++++----------- harmonize/player.py | 19 ++++++++++------ harmonize/queue.py | 11 ++++++++- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/docs/events.rst b/docs/events.rst index 3641055..9163551 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -52,7 +52,35 @@ An event listener in a cog. 4xxx codes are usually bad. .. note:: + See the `Discord Docs `_. + +.. function:: on_harmonize_session_no_longer(player: harmonize.Player) + + Called when an audio WebSocket (to Discord) is closed with code 4006. + + .. note:: + See the `Discord Docs `_. + +.. function:: on_harmonize_session_timeout(player: harmonize.Player) + + Called when an audio WebSocket (to Discord) is closed with code 4009. + .. note:: + See the `Discord Docs `_. + +.. function:: on_harmonize_voice_modification(player: harmonize.Player) + + Called when an audio WebSocket (to Discord) is closed with code 4014. + E.g., changed the voice channel or kicked out of the channel + + .. note:: + See the `Discord Docs `_. + +.. function:: on_harmonize_voice_crashed(player: harmonize.Player) + + Called when an audio WebSocket (to Discord) is closed with code 4015. + + .. note:: See the `Discord Docs `_. .. function:: on_harmonize_extra_event(event_type: str, player: harmonize.Player, data: str) diff --git a/harmonize/connection/transport.py b/harmonize/connection/transport.py index a219b36..9239694 100644 --- a/harmonize/connection/transport.py +++ b/harmonize/connection/transport.py @@ -21,6 +21,7 @@ from harmonize.objects import Stats, Track if TYPE_CHECKING: + from harmonize import Player from harmonize.connection.node import Node __all__ = ( @@ -127,6 +128,17 @@ async def connect(self) -> None: except Exception as e: logger.warning(f"An error ({type(e).__name__}) was thrown when connecting: {e}") + def _handle_ws_closed_event(self, player: Player, data: dict[any, any]) -> None: + match int(data["code"]): + case 4006: + self.dispatch("session_no_longer", player) + case 4009: + self.dispatch("session_timeout", player) + case 4014: + self.dispatch("voice_modification", player) + case 4015: + self.dispatch("voice_crashed", player) + async def _handle_event(self, data: dict[any, any]) -> None: player = self._node.players.get(int(data['guildId'])) # type: ignore event_type = data['type'] @@ -177,33 +189,34 @@ async def _handle_event(self, data: dict[any, any]) -> None: +======+===========================+==========================================================+ | 4001 | Unknown opcode | You sent an invalid opcode. | +------+---------------------------+----------------------------------------------------------+ - | 4002 | Failed to decode payload | You sent an invalid payload in your identifying to the | + | 4002 | Failed to decode payload | You sent an invalid payload in your identifying to the | | | | Gateway. | +------+---------------------------+----------------------------------------------------------+ - | 4003 | Not authenticated | You sent a payload before identifying with the Gateway. | + | 4003 | Not authenticated | You sent a payload before identifying with the Gateway. | +------+---------------------------+----------------------------------------------------------+ - | 4004 | Authentication failed | The token you sent in your identify payload is incorrect.| + | 4004 | Authentication failed | The token you sent in your identify payload is incorrect.| +------+---------------------------+----------------------------------------------------------+ - | 4005 | Already authenticated | You sent more than one identify payload. Stahp. | + | 4005 | Already authenticated | You sent more than one identify payload. Stahp. | +------+---------------------------+----------------------------------------------------------+ - | 4006 | Session no longer valid | Your session is no longer valid. | + | 4006 | Session no longer valid | Your session is no longer valid. | +------+---------------------------+----------------------------------------------------------+ - | 4009 | Session timeout | Your session has timed out. | + | 4009 | Session timeout | Your session has timed out. | +------+---------------------------+----------------------------------------------------------+ - | 4011 | Server not found | We can't find the server you're trying to connect to. | + | 4011 | Server not found | We can't find the server you're trying to connect to. | +------+---------------------------+----------------------------------------------------------+ - | 4012 | Unknown protocol | We didn't recognize the protocol you sent. | + | 4012 | Unknown protocol | We didn't recognize the protocol you sent. | +------+---------------------------+----------------------------------------------------------+ - | 4014 | Disconnected | Channel was deleted, you were kicked, voice server | + | 4014 | Disconnected | Channel was deleted, you were kicked, voice server | | | | changed, or the main gateway session was dropped. | | | | Should not reconnect. | +------+---------------------------+----------------------------------------------------------+ - | 4015 | Voice server crashed | The server crashed. Our bad! Try resuming. | + | 4015 | Voice server crashed | The server crashed. Our bad! Try resuming. | +------+---------------------------+----------------------------------------------------------+ - | 4016 | Unknown encryption mode | We didn't recognize your encryption. | + | 4016 | Unknown encryption mode | We didn't recognize your encryption. | +------+---------------------------+----------------------------------------------------------+ """ self.dispatch("discord_ws_closed", player, int(data['code']), data['reason'], bool(data['byRemote'])) + self._handle_ws_closed_event(player, data) else: return self.dispatch("extra_event", event_type, player, data) @@ -224,8 +237,6 @@ async def _handle_message(self, data: dict[any, any] | list[any]) -> None: if player := self._node.players.get(int(data['guildId'])): await player.update_state(data['state']) self.dispatch('player_update', player) - # else: - # await self._node.destroy_player(int(data['guildId'])) elif data["op"] == 'event': await self._handle_event(data) else: diff --git a/harmonize/player.py b/harmonize/player.py index 34cec62..ef2495c 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -12,7 +12,7 @@ from harmonize.abstract import Filter from harmonize.connection import Pool from harmonize.enums import LoopStatus -from harmonize.exceptions import InvalidChannelStateException +from harmonize.exceptions import InvalidChannelStateException, RequestError from harmonize.objects import Track, MISSING from harmonize.queue import Queue @@ -36,9 +36,6 @@ class Player(VoiceProtocol): node : :class:`harmonize.connection.Node` The node the player is connected to. - connection_event : :class:`asyncio.Event` - An event triggered when the player's connection state changes. - voice_state : dict[str, any] The current voice state of the player. @@ -82,6 +79,7 @@ class Player(VoiceProtocol): def __call__(self, client: Client, channel: VocalGuildChannel) -> Player: super().__init__(client, channel) + self._guild = channel.guild return self def __init__(self, *args, **kwargs) -> None: @@ -151,17 +149,24 @@ async def on_voice_server_update(self, data: dict) -> None: await self._dispatch_voice_update() async def on_voice_state_update(self, data: dict) -> None: - if not data['channel_id']: + if not (channel := int(data["channel_id"])): return await self.disconnect(force=True) + self._connected = True + self.channel = self.client.get_channel(channel) # type: ignore + if data['session_id'] != self._voice_state.get('sessionId'): self._voice_state.update(sessionId=data['session_id']) await self._dispatch_voice_update() async def _dispatch_voice_update(self) -> None: if {'sessionId', 'endpoint', 'token'} == self._voice_state.keys(): - await self._node.update_player(guild_id=self.guild.id, voice_state=self._voice_state) - self._connection_event.set() + try: + await self._node.update_player(guild_id=self.guild.id, voice_state=self._voice_state) + except RequestError: + await self.disconnect(force=True) + else: + self._connection_event.set() async def handle_event(self, data: dict[any, any]) -> None: """|coro| diff --git a/harmonize/queue.py b/harmonize/queue.py index 3ebead6..8875c97 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -73,9 +73,18 @@ class Queue: Returns a string representation of the queue. + Note + ---- + The ``history`` and ``tracks`` attributes give the ORIGINAL OBJECT, you can change them at will. + Tip --- - The ``history`` and ``tracks`` attributes give the ORIGINAL OBJECT, you can change them at will + You can implement your own track queue and hook it to the player + + .. code-block:: python3 + + player = Player.connect_to_channel(voice) + player.queue = YourQueue() Attributes ---------- From 6f8ebf3acd0b191dff5aa525a93c266fcfefcb31 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Fri, 20 Sep 2024 21:19:21 +0300 Subject: [PATCH 11/16] Queue playlist support --- harmonize/player.py | 4 ++-- harmonize/queue.py | 40 +++++++++++++++++++++++++++++++++------- pyproject.toml | 2 +- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/harmonize/player.py b/harmonize/player.py index ef2495c..059b894 100644 --- a/harmonize/player.py +++ b/harmonize/player.py @@ -778,8 +778,8 @@ async def _destroy(self) -> None: except (AttributeError, KeyError): pass - del self._node.players[self.guild.id] - await self._node.destroy_player(self.guild.id) + if self.node.players.pop(self.guild.id, None): + await self._node.destroy_player(self.guild.id) async def disconnect(self, **kwargs: any) -> None: """|coro| diff --git a/harmonize/queue.py b/harmonize/queue.py index 8875c97..3ae5644 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -7,7 +7,7 @@ from harmonize.objects import MISSING if TYPE_CHECKING: - from harmonize.objects import Track + from harmonize.objects import Track, PlaylistInfo, LoadResult from harmonize import Player __all__ = ( @@ -108,8 +108,9 @@ def __init__(self, player: Player) -> None: self._loop: LoopStatus = LoopStatus(0) self._history: list[Track] = [] self._listened_count = 0 - self._now: list[Track] = [] + self._now: list[Track | dict[str, PlaylistInfo | list[Track]]] = [] self._current: Optional[Track] = None + self._current_playlist: Optional[PlaylistInfo] = None self._player: Player = player @property @@ -155,19 +156,27 @@ def set_loop(self, value: LoopStatus, /) -> None: self._loop = value @overload - def add(self, track: Track) -> None: + def add(self, search: LoadResult, /) -> None: ... @overload - def add(self, tracks: list[Track]) -> None: + def add(self, *, track: Track) -> None: ... - def add(self, **kwargs: Track | list[Track]) -> None: + @overload + def add(self, *, tracks: list[Track]) -> None: + ... + + def add(self, *args: LoadResult, **kwargs: Track | list[Track]) -> None: """ Adds a track or multiple tracks to the queue. Parameters ---------- + *args : :class:`harmonize.objects.LoadResult` + + A single :class:`harmonize.objects.LoadResult` object to add to the queue. + **kwargs : :class:`harmonize.objects.Track` | list[:class:`harmonize.objects.Track`] 'track' (:class:`harmonize.objects.Track`): A single track to add to the queue. @@ -182,6 +191,12 @@ def add(self, **kwargs: Track | list[Track]) -> None: ------- None """ + if load_result := list(args).pop(): + self._now.append({ + "playlist": load_result.playlist_info, + "tracks": load_result.tracks + }) + if 'track' in kwargs: self._now.append(kwargs.pop("track")) elif 'tracks' in kwargs: @@ -245,7 +260,7 @@ async def load_next(self, track: Optional[Track] = MISSING) -> Optional[Track]: self._listened_count += 1 old = self._current - if self.loop.value > 0 and self._current: + if self.loop != LoopStatus.OFF and self._current: match self.loop: case LoopStatus.TRACK: if track is MISSING: @@ -258,7 +273,18 @@ async def load_next(self, track: Optional[Track] = MISSING) -> Optional[Track]: await self._player.stop() return self._player.dispatch('queue_end', self._player) - track = self._now.pop(0) + if isinstance(self._now[0], dict): + data: dict[str, PlaylistInfo | list[Track]] = self._now[0].copy() + if not (tracks := data["tracks"]): + self._now.pop(0) + if not self._now: + await self._player.stop() + return self._player.dispatch('queue_end', self._player) + else: + self._current_playlist = data["playlist"] + track = tracks.pop(0) + else: + track = self._now.pop(0) if old: self._history.insert(0, old) diff --git a/pyproject.toml b/pyproject.toml index 36e1e79..b84d540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.poetry] name = "harmonize.py" -version = "1.0.1" +version = "1.1.0" authors = ["Krista"] description = "A robust and powerful, fully asynchronous Lavalink wrapper built for disnake in Python." readme = "README.md" From 8e6c4fae5d910c4d1ba0a025e41942df408295b5 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Fri, 20 Sep 2024 21:44:03 +0300 Subject: [PATCH 12/16] Fix --- harmonize/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index 3ae5644..d672865 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -302,7 +302,7 @@ def __repr__(self) -> str: ) def __len__(self) -> int: - return len(self._now) + return sum(len(obj["tracks"]) if isinstance(obj, dict) else 1 for obj in self._now) def __getitem__(self, index: int) -> Track: return self._now[index] From bca3d97263e88e12ad4e1fd3e1d238e71225113f Mon Sep 17 00:00:00 2001 From: krispeckt Date: Fri, 20 Sep 2024 21:45:54 +0300 Subject: [PATCH 13/16] Fix --- harmonize/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index d672865..fad4b06 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -192,7 +192,7 @@ def add(self, *args: LoadResult, **kwargs: Track | list[Track]) -> None: None """ if load_result := list(args).pop(): - self._now.append({ + return self._now.append({ "playlist": load_result.playlist_info, "tracks": load_result.tracks }) From dc9216726124794d80e1a8f04abbed3e0b65bcc5 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Fri, 20 Sep 2024 23:22:42 +0300 Subject: [PATCH 14/16] Fix --- harmonize/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index fad4b06..2ca56ce 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -191,7 +191,7 @@ def add(self, *args: LoadResult, **kwargs: Track | list[Track]) -> None: ------- None """ - if load_result := list(args).pop(): + if args and (load_result := list(args).pop()): return self._now.append({ "playlist": load_result.playlist_info, "tracks": load_result.tracks From 4e578bd6b16a3ce2f2d97cbd99c786497ea9f8a3 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Sat, 21 Sep 2024 00:03:47 +0300 Subject: [PATCH 15/16] Add new attrs --- harmonize/queue.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index 2ca56ce..6cfbdca 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -91,8 +91,11 @@ class Queue: current : Optional[:class:`harmonize.objects.Track`] The currently playing track in the queue. - tracks : list[:class:`harmonize.objects.Track`] - The list of tracks currently in the queue. + items : list[:class:`harmonize.objects.Track` | dict[str, :class:`harmonize.objects.PlaylistInfo` | list[:class:`harmonize.objects.Track`]] + The list of tracks or playlists currently in the queue. + + tracks : list[:class:`harmonize.objects.Track] + Only tracks in the queue. history : list[:class:`harmonize.objects.Track`] The list of tracks that have been played in the queue. @@ -122,7 +125,7 @@ def history(self) -> list[Track]: return self._history @property - def tracks(self) -> list[Track]: + def items(self) -> list[Track | dict[str, PlaylistInfo | list[Track]]]: return self._now @property @@ -133,6 +136,15 @@ def listened_count(self) -> int: def loop(self) -> LoopStatus: return self._loop + @property + def tracks(self) -> list[Track]: + def unpack(item: dict[str, PlaylistInfo | list[Track]] | Track) -> list[Track]: + if isinstance(item, dict) and "tracks" in item: + return item["tracks"] + return [item] + + return [track for sublist in map(unpack, self._now) for track in sublist] + def set_loop(self, value: LoopStatus, /) -> None: """ Sets the loop status of the queue. From fd67d7ea87be7a1f8ae8bceeb3c4e4e106ec6321 Mon Sep 17 00:00:00 2001 From: krispeckt Date: Sat, 21 Sep 2024 00:08:14 +0300 Subject: [PATCH 16/16] Remove note --- harmonize/queue.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/harmonize/queue.py b/harmonize/queue.py index 6cfbdca..5f9d1ca 100644 --- a/harmonize/queue.py +++ b/harmonize/queue.py @@ -73,10 +73,6 @@ class Queue: Returns a string representation of the queue. - Note - ---- - The ``history`` and ``tracks`` attributes give the ORIGINAL OBJECT, you can change them at will. - Tip --- You can implement your own track queue and hook it to the player