Skip to content

Commit

Permalink
Merge pull request #143 from devoxin/dev
Browse files Browse the repository at this point in the history
5.3.0
  • Loading branch information
devoxin authored Mar 8, 2024
2 parents 210dd6a + 2bf388a commit 4ea2dc7
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 31 deletions.
8 changes: 8 additions & 0 deletions docs/lavalink.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ Client
.. autoclass:: Client
:members:

DataIO
------
.. autoclass:: DataReader
:members:

.. autoclass:: DataWriter
:members:

Errors
------
.. autoclass:: ClientError
Expand Down
8 changes: 6 additions & 2 deletions lavalink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
__author__ = 'Devoxin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2017-present Devoxin'
__version__ = '5.2.0'
__version__ = '5.3.0'


from typing import Type

from .abc import BasePlayer, DeferredAudioTrack, Source
from .client import Client
from .dataio import DataReader, DataWriter
from .errors import (AuthenticationError, ClientError, InvalidTrack, LoadError,
RequestError)
from .events import (Event, IncomingWebSocketMessage, NodeChangedEvent,
Expand All @@ -24,12 +27,13 @@
from .playermanager import PlayerManager
from .server import (AudioTrack, EndReason, LoadResult, LoadResultError,
LoadType, PlaylistInfo, Plugin, Severity)
from .source_decoders import DEFAULT_DECODER_MAPPING
from .stats import Penalty, Stats
from .utils import (decode_track, encode_track, format_time, parse_time,
timestamp_to_millis)


def listener(*events: Event):
def listener(*events: Type[Event]):
"""
Marks this function as an event listener for Lavalink.py.
This **must** be used on class methods, and you must ensure that you register
Expand Down
5 changes: 3 additions & 2 deletions lavalink/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ class DeferredAudioTrack(ABC, AudioTrack):
for example.
"""
@abstractmethod
async def load(self, client: 'Client'):
async def load(self, client: 'Client') -> Optional[str]:
"""|coro|
Retrieves a base64 string that's playable by Lavalink.
Expand All @@ -288,8 +288,9 @@ async def load(self, client: 'Client'):
Returns
-------
:class:`str`
Optional[:class:`str`]
A Lavalink-compatible base64-encoded string containing track metadata.
If a track string cannot be returned, you may return ``None`` or throw a :class:`LoadError`.
"""
raise NotImplementedError

Expand Down
10 changes: 5 additions & 5 deletions lavalink/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import random
from collections import defaultdict
from inspect import getmembers, ismethod
from typing import (Any, Callable, Dict, List, Optional, Sequence, Set, Tuple,
from typing import (Any, Callable, Dict, Generic, List, Optional, Sequence, Set, Tuple,
Type, TypeVar, Union)

import aiohttp
Expand All @@ -47,7 +47,7 @@
EventT = TypeVar('EventT', bound=Event)


class Client:
class Client(Generic[PlayerT]):
"""
Represents a Lavalink client used to manage nodes and connections.
Expand Down Expand Up @@ -102,7 +102,7 @@ def __init__(self, user_id: Union[int, str], player: Type[PlayerT] = DefaultPlay
self._user_id: int = int(user_id)
self._event_hooks = defaultdict(list)
self.node_manager: NodeManager = NodeManager(self, regions, connect_back)
self.player_manager: PlayerManager = PlayerManager(self, player)
self.player_manager: PlayerManager[PlayerT] = PlayerManager(self, player)
self.sources: Set[Source] = set()

@property
Expand All @@ -113,7 +113,7 @@ def nodes(self) -> List[Node]:
return self.node_manager.nodes

@property
def players(self) -> Dict[int, BasePlayer]:
def players(self) -> Dict[int, PlayerT]:
"""
Convenience shortcut for :attr:`PlayerManager.players`.
"""
Expand Down Expand Up @@ -207,7 +207,7 @@ def remove_event_hooks(self, *, events: Optional[Sequence[EventT]] = None, hooks
----------
events: Sequence[:class:`Event`]
The events to remove the hooks from. This parameter can be omitted,
and the events registered on the function via :meth:`listener` will be used instead, if applicable.
and the events registered on the function via :func:`listener` will be used instead, if applicable.
Otherwise, a default value of ``Generic`` is used instead.
hooks: Sequence[Callable]
A list of hook methods to remove.
Expand Down
20 changes: 15 additions & 5 deletions lavalink/dataio.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@

class DataReader:
def __init__(self, base64_str: str):
self._buf: BytesIO = BytesIO(b64decode(base64_str))
self._buf = BytesIO(b64decode(base64_str))

def _read(self, count):
@property
def remaining(self) -> int:
""" The amount of bytes left to be read. """
return self._buf.getbuffer().nbytes - self._buf.tell()

def _read(self, count: int):
return self._buf.read(count)

def read_byte(self) -> bytes:
Expand All @@ -55,8 +60,13 @@ def read_long(self) -> int:
result, = struct.unpack('>Q', self._read(8))
return result

def read_nullable_utf(self) -> Optional[str]:
return self.read_utf().decode() if self.read_boolean() else None
def read_nullable_utf(self, utfm: bool = False) -> Optional[str]:
exists = self.read_boolean()

if not exists:
return None

return self.read_utfm() if utfm else self.read_utf().decode()

def read_utf(self) -> bytes:
text_length = self.read_unsigned_short()
Expand Down Expand Up @@ -110,7 +120,7 @@ def write_utf(self, utf_string):
self.write_unsigned_short(byte_len)
self._write(utf)

def finish(self):
def finish(self) -> bytes:
with BytesIO() as track_buf:
byte_len = self._buf.getbuffer().nbytes
flags = byte_len | (1 << 30)
Expand Down
2 changes: 1 addition & 1 deletion lavalink/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ def update(self, *, smoothing: float):
------
:class:`ValueError`
"""
smoothing = float('smoothing')
smoothing = float(smoothing)

if smoothing <= 1:
raise ValueError('smoothing must be bigger than 1')
Expand Down
14 changes: 14 additions & 0 deletions lavalink/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,20 @@ def get_filter(self, _filter: Union[Type[FilterT], str]):

return self.filters.get(filter_name.lower(), None)

async def remove_filters(self, *filters: Union[Type[FilterT], str]):
"""|coro|
Removes multiple filters from the player, undoing any effects applied to the audio.
This is similar to :func:`remove_filter` but instead allows you to remove multiple filters with one call.
Parameters
----------
filters: Union[Type[:class:`Filter`], :class:`str`]
The filters to remove. Can be filter name, or filter class (**not** an instance of).
"""
for fltr in filters:
await self.remove_filter(fltr)

async def remove_filter(self, _filter: Union[Type[FilterT], str]):
"""|coro|
Expand Down
38 changes: 29 additions & 9 deletions lavalink/playermanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
SOFTWARE.
"""
import logging
from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Optional, Tuple,
Type, TypeVar)
from typing import (TYPE_CHECKING, Callable, Dict, Generic, Iterator, Optional,
Tuple, Type, TypeVar, Union, overload)

from .errors import ClientError
from .node import Node
Expand All @@ -35,9 +35,10 @@
_log = logging.getLogger(__name__)

PlayerT = TypeVar('PlayerT', bound=BasePlayer)
CustomPlayerT = TypeVar('CustomPlayerT', bound=BasePlayer)


class PlayerManager:
class PlayerManager(Generic[PlayerT]):
"""
Represents the player manager that contains all the players.
Expand All @@ -61,22 +62,22 @@ def __init__(self, client, player: Type[PlayerT]):

self.client: 'Client' = client
self._player_cls: Type[PlayerT] = player
self.players: Dict[int, BasePlayer] = {}
self.players: Dict[int, PlayerT] = {}

def __len__(self) -> int:
return len(self.players)

def __iter__(self) -> Iterator[Tuple[int, BasePlayer]]:
def __iter__(self) -> Iterator[Tuple[int, PlayerT]]:
""" Returns an iterator that yields a tuple of (guild_id, player). """
for guild_id, player in self.players.items():
yield guild_id, player

def values(self) -> Iterator[BasePlayer]:
def values(self) -> Iterator[PlayerT]:
""" Returns an iterator that yields only values. """
for player in self.players.values():
yield player

def find_all(self, predicate: Optional[Callable[[BasePlayer], bool]] = None):
def find_all(self, predicate: Optional[Callable[[PlayerT], bool]] = None):
"""
Returns a list of players that match the given predicate.
Expand All @@ -96,7 +97,7 @@ def find_all(self, predicate: Optional[Callable[[BasePlayer], bool]] = None):

return [p for p in self.players.values() if bool(predicate(p))]

def get(self, guild_id: int) -> Optional[BasePlayer]:
def get(self, guild_id: int) -> Optional[PlayerT]:
"""
Gets a player from cache.
Expand Down Expand Up @@ -126,13 +127,32 @@ def remove(self, guild_id: int):
player = self.players.pop(guild_id)
player.cleanup()

@overload
def create(self,
guild_id: int,
*,
region: Optional[str] = ...,
endpoint: Optional[str] = ...,
node: Optional[Node] = ...) -> PlayerT:
...

@overload
def create(self,
guild_id: int,
*,
region: Optional[str] = ...,
endpoint: Optional[str] = ...,
node: Optional[Node] = ...,
cls: Type[CustomPlayerT]) -> CustomPlayerT:
...

def create(self,
guild_id: int,
*,
region: Optional[str] = None,
endpoint: Optional[str] = None,
node: Optional[Node] = None,
cls: Optional[Type[PlayerT]] = None) -> BasePlayer:
cls: Optional[Type[CustomPlayerT]] = None) -> Union[CustomPlayerT, PlayerT]:
"""
Creates a player if one doesn't exist with the given information.
Expand Down
17 changes: 14 additions & 3 deletions lavalink/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class AudioTrack:
The track's uploader.
duration: :class:`int`
The duration of the track, in milliseconds.
stream: :class:`bool`
is_stream: :class:`bool`
Whether the track is a live-stream.
title: :class:`str`
The title of the track.
Expand All @@ -110,7 +110,7 @@ class AudioTrack:
extra: Dict[str, Any]
Any extra properties given to this AudioTrack will be stored here.
"""
__slots__ = ('raw', 'track', 'identifier', 'is_seekable', 'author', 'duration', 'stream', 'title', 'uri',
__slots__ = ('raw', 'track', 'identifier', 'is_seekable', 'author', 'duration', 'is_stream', 'title', 'uri',
'artwork_url', 'isrc', 'position', 'source_name', 'plugin_info', 'user_data', 'extra')

def __init__(self, data: dict, requester: int = 0, **extra):
Expand All @@ -127,7 +127,7 @@ def __init__(self, data: dict, requester: int = 0, **extra):
self.is_seekable: bool = info['isSeekable']
self.author: str = info['author']
self.duration: int = info['length']
self.stream: bool = info['isStream']
self.is_stream: bool = info['isStream']
self.title: str = info['title']
self.uri: str = info['uri']
self.artwork_url: Optional[str] = info.get('artworkUrl')
Expand All @@ -150,6 +150,17 @@ def __getitem__(self, name):
def from_dict(cls, mapping: dict):
return cls(mapping)

@property
def stream(self) -> bool:
"""
Property indicating whether this track is a stream.
.. deprecated:: 5.3.0
To be consistent with attribute naming, this property has been deprecated
in favour of ``is_stream``.
"""
return self.is_stream

@property
def requester(self) -> int:
return self.extra['requester']
Expand Down
61 changes: 61 additions & 0 deletions lavalink/source_decoders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
MIT License
Copyright (c) 2017-present Devoxin
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 typing import Any, Callable, Dict, Mapping

from .dataio import DataReader


def decode_probe_info(reader: DataReader) -> Mapping[str, Any]:
probe_info = reader.read_utf().decode()
return {'probe_info': probe_info}


def decode_lavasrc_fields(reader: DataReader) -> Mapping[str, Any]:
if reader.remaining <= 8: # 8 bytes (long) = position field
return {}

album_name = reader.read_nullable_utf()
album_url = reader.read_nullable_utf()
artist_url = reader.read_nullable_utf()
artist_artwork_url = reader.read_nullable_utf()
preview_url = reader.read_nullable_utf()
is_preview = reader.read_boolean()

return {
'album_name': album_name,
'album_url': album_url,
'artist_url': artist_url,
'artist_artwork_url': artist_artwork_url,
'preview_url': preview_url,
'is_preview': is_preview
}


DEFAULT_DECODER_MAPPING: Dict[str, Callable[[DataReader], Mapping[str, Any]]] = {
'http': decode_probe_info,
'local': decode_probe_info,
'deezer': decode_lavasrc_fields,
'spotify': decode_lavasrc_fields,
'applemusic': decode_lavasrc_fields
}
2 changes: 1 addition & 1 deletion lavalink/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ async def _listen(self):
try:
await self._handle_message(msg.json())
except Exception: # pylint: disable=W0718
_log.error('[Node:%s] Unexpected error occurred whilst processing websocket message', self._node.name)
_log.exception('[Node:%s] Unexpected error occurred whilst processing websocket message', self._node.name)
elif msg.type == aiohttp.WSMsgType.ERROR:
exc = self._ws.exception()
_log.error('[Node:%s] Exception in WebSocket!', self._node.name, exc_info=exc)
Expand Down
Loading

0 comments on commit 4ea2dc7

Please sign in to comment.