Skip to content

Commit 9d0fc4e

Browse files
authored
Merge pull request #5 from interactions-py/unstable
2 parents 09ad92e + dde46af commit 9d0fc4e

File tree

9 files changed

+195
-45
lines changed

9 files changed

+195
-45
lines changed

README.md

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,108 @@
99

1010
## Usage
1111

12-
Run lavalink via `java -jar Lavalink.jar` in same folder with `application.yml` file.
12+
Run lavalink via `java -jar Lavalink.jar` in same folder with `application.yml` file.
1313
Create bot like example and run it.
1414

15+
Main file:
1516
```python
1617
import interactions
1718
from interactions.ext.lavalink import VoiceState, VoiceClient
1819

1920
client = VoiceClient(...)
2021

21-
@client.event()
22-
async def on_start():
23-
client.lavalink_client.add_node("127.0.0.1", 43421, "your_password", "eu") # Copy host, port and password from `application.yml`
22+
client.load("exts.music")
2423

25-
@client.event()
26-
async def on_voice_state_update(before: VoiceState, after: VoiceState):
24+
client.start()
25+
```
26+
27+
Extension file: `exts/music.py`
28+
```python
29+
import interactions
30+
from interactions.ext.lavalink import VoiceClient, VoiceState, listener, Player
31+
import lavalink
32+
33+
34+
class Music(interactions.Extension):
35+
def __init__(self, client):
36+
self.client: VoiceClient = client
37+
38+
@listener()
39+
async def on_track_start(self, event: lavalink.TrackStartEvent):
40+
"""
41+
Fires when track starts
42+
"""
43+
print("STARTED", event.track)
44+
45+
@interactions.extension_listener()
46+
async def on_start(self):
47+
self.client.lavalink_client.add_node("127.0.0.1", 43421, "your_password", "eu")
48+
49+
@interactions.extension_listener()
50+
async def on_voice_state_update(self, before: VoiceState, after: VoiceState):
51+
"""
52+
Disconnect if bot is alone
53+
"""
54+
if before and not after.joined:
55+
voice_states = self.client.get_channel_voice_states(before.channel_id)
56+
if len(voice_states) == 1 and voice_states[0].user_id == self.client.me.id:
57+
await self.client.disconnect(before.guild_id)
58+
59+
@interactions.extension_command()
60+
@interactions.option()
61+
async def play(self, ctx: interactions.CommandContext, query: str):
62+
await ctx.defer()
63+
64+
# NOTE: ctx.author.voice can be None if you ran a bot after joining the voice channel
65+
voice: VoiceState = ctx.author.voice
66+
if not voice or not voice.joined:
67+
return await ctx.send("You're not connected to the voice channel!")
68+
69+
player: Player # Typehint player variable to see their methods
70+
if (player := ctx.guild.player) is None:
71+
player = await voice.connect()
72+
73+
tracks = await player.search_youtube(query)
74+
track = tracks[0]
75+
player.add(requester=int(ctx.author.id), track=track)
76+
77+
if player.is_playing:
78+
return await ctx.send(f"Added to queue: `{track.title}`")
79+
await player.play()
80+
await ctx.send(f"Now playing: `{track.title}`")
81+
82+
@interactions.extension_command()
83+
async def leave(self, ctx: interactions.CommandContext):
84+
await self.client.disconnect(ctx.guild_id)
85+
```
86+
87+
## Events
88+
To listen lavalink event you have to use `@listener` decorator.
89+
90+
```python
91+
import lavalink
92+
from interactions.ext.lavalink import listener
93+
94+
95+
# NOTE: Works only in extensions.
96+
class MusicExt(Extension):
2797
...
2898

29-
@client.command()
30-
@interactions.option()
31-
async def play(ctx: interactions.CommandContext, query: str):
32-
await ctx.defer()
33-
# NOTE: ctx.author.voice can be None if you runned a bot after joining the voice channel
34-
player = await self.client.connect(ctx.author.voice.guild_id, ctx.author.voice.channel_id)
99+
# There are most useful events for you. You can use other events if you want it.
100+
@listener()
101+
async def on_track_start(self, event: lavalink.TrackStartEvent):
102+
"""Fires when track starts"""
35103

36-
results = await player.node.get_tracks(f"ytsearch:{query}")
37-
track = AudioTrack(results["tracks"][0], int(ctx.author.id))
38-
player.add(requester=int(ctx.author.id), track=track)
39-
await player.play()
104+
@listener()
105+
async def on_track_end(self, event: lavalink.TrackEndEvent):
106+
"""Fires when track ends"""
40107

41-
await ctx.send(f"Now playing: `{track.title}`")
108+
@listener()
109+
async def on_queue_end(self, event: lavalink.QueueEndEvent):
110+
"""Fires when queue ends"""
42111

43-
client.start()
44112
```
45113

46-
Example with using `Extension` [here](https://github.com/Damego/interactions-lavalink/tree/main/examples)
47-
48114
## New methods/properties for interactions.py library
49115

50116
`Member.voice` - returns current member's `VoiceState`. It can be `None` if not cached.

examples/exts/music.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import interactions
2-
from interactions.ext.lavalink import VoiceClient, VoiceState
3-
from lavalink import AudioTrack
2+
from interactions.ext.lavalink import Player, VoiceClient, VoiceState
3+
44

55
class Music(interactions.Extension):
66
def __init__(self, client):
@@ -25,14 +25,22 @@ async def on_voice_state_update(self, before: VoiceState, after: VoiceState):
2525
async def play(self, ctx: interactions.CommandContext, query: str):
2626
await ctx.defer()
2727

28-
# NOTE: ctx.author.voice can be None if you runned a bot after joining the voice channel
29-
player = await self.client.connect(ctx.author.voice.guild_id, ctx.author.voice.channel_id)
28+
# NOTE: ctx.author.voice can be None if you ran a bot after joining the voice channel
29+
voice: VoiceState = ctx.author.voice
30+
if not voice or not voice.joined:
31+
return await ctx.send("You're not connected to the voice channel!")
32+
33+
player: Player # Typehint player variable to see their methods
34+
if (player := ctx.guild.player) is None:
35+
player = await voice.connect()
3036

31-
results = await player.node.get_tracks(f"ytsearch:{query}")
32-
track = AudioTrack(results["tracks"][0], int(ctx.author.id))
37+
tracks = await player.search_youtube(query)
38+
track = tracks[0]
3339
player.add(requester=int(ctx.author.id), track=track)
34-
await player.play()
3540

41+
if player.is_playing:
42+
return await ctx.send(f"Added to queue: `{track.title}`")
43+
await player.play()
3644
await ctx.send(f"Now playing: `{track.title}`")
3745

3846
@interactions.extension_command()

interactions/ext/lavalink/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from interactions.ext.version import Version, VersionAuthor
33

44
__all__ = ["version", "base"]
5-
__version__ = "0.0.3"
5+
__version__ = "0.1.0"
66

77
version = Version(
88
version=__version__, author=VersionAuthor(name="Damego", email="[email protected]")

interactions/ext/lavalink/client.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1+
from inspect import getmembers
12
from typing import Dict, List, Optional, Union
23

34
from lavalink import Client as LavalinkClient
4-
from lavalink import DefaultPlayer
55

6-
from interactions import Client, Snowflake
6+
from interactions import Client, LibraryException, Snowflake
77

88
from .models import VoiceState
9+
from .player import Player
910
from .websocket import VoiceWebSocketClient
1011

11-
__all__ = ["VoiceClient"]
12+
__all__ = ["VoiceClient", "listener"]
1213

1314

1415
class VoiceClient(Client):
1516
def __init__(self, token: str, **kwargs):
1617
super().__init__(token, **kwargs)
1718

1819
self._websocket = VoiceWebSocketClient(token, self._intents)
19-
self.lavalink_client = LavalinkClient(int(self.me.id))
20+
self.lavalink_client = LavalinkClient(int(self.me.id), player=Player)
2021

2122
self._websocket._dispatch.register(
2223
self.__raw_voice_state_update, "on_raw_voice_state_update"
@@ -25,7 +26,7 @@ def __init__(self, token: str, **kwargs):
2526
self.__raw_voice_server_update, "on_raw_voice_server_update"
2627
)
2728

28-
self._websocket._bot_var = self
29+
self._websocket._http._bot_var = self
2930
self._http._bot_var = self
3031

3132
async def __raw_voice_state_update(self, data: dict):
@@ -42,7 +43,7 @@ async def connect(
4243
channel_id: Union[Snowflake, int, str],
4344
self_deaf: bool = False,
4445
self_mute: bool = False,
45-
) -> DefaultPlayer:
46+
) -> Player:
4647
"""
4748
Connects to voice channel and creates player.
4849
@@ -55,26 +56,35 @@ async def connect(
5556
:param self_mute: Whether bot is self muted
5657
:type self_mute: bool
5758
:return: Created guild player.
58-
:rtype: DefaultPlayer
59+
:rtype: Player
5960
"""
61+
# Discord will fire INVALID_SESSION if channel_id is None
62+
if guild_id is None:
63+
raise LibraryException(message="Missed requirement argument: guild_id")
64+
if channel_id is None:
65+
raise LibraryException(message="Missed requirement argument: channel_id")
66+
6067
await self._websocket.connect_voice_channel(guild_id, channel_id, self_deaf, self_mute)
6168
player = self.lavalink_client.player_manager.get(int(guild_id))
6269
if player is None:
6370
player = self.lavalink_client.player_manager.create(int(guild_id))
6471
return player
6572

6673
async def disconnect(self, guild_id: Union[Snowflake, int]):
74+
if guild_id is None:
75+
raise LibraryException(message="Missed requirement argument: guild_id")
76+
6777
await self._websocket.disconnect_voice_channel(int(guild_id))
6878
await self.lavalink_client.player_manager.destroy(int(guild_id))
6979

70-
def get_player(self, guild_id: Union[Snowflake, int]) -> DefaultPlayer:
80+
def get_player(self, guild_id: Union[Snowflake, int]) -> Player:
7181
"""
7282
Returns current player in guild.
7383
7484
:param guild_id: The guild id
7585
:type guild_id: Union[Snowflake, int]
7686
:return: Guild player
77-
:rtype: DefaultPlayer
87+
:rtype: Player
7888
"""
7989
return self.lavalink_client.player_manager.get(int(guild_id))
8090

@@ -96,7 +106,7 @@ def get_user_voice_state(self, user_id: Union[Snowflake, int]) -> Optional[Voice
96106
_user_id = Snowflake(user_id) if isinstance(user_id, int) else user_id
97107
return self._http.cache[VoiceState].get(_user_id)
98108

99-
def get_guild_voice_states(self, guild_id: Union[Snowflake, int]):
109+
def get_guild_voice_states(self, guild_id: Union[Snowflake, int]) -> Optional[List[VoiceState]]:
100110
"""
101111
Returns guild voice states.
102112
@@ -131,3 +141,28 @@ def get_channel_voice_states(
131141
for voice_state in self.voice_states.values()
132142
if voice_state.channel_id == _channel_id
133143
]
144+
145+
def __register_lavalink_listeners(self):
146+
for extension in self._extensions.values():
147+
for name, func in getmembers(extension):
148+
if hasattr(func, "__lavalink__"):
149+
name = func.__lavalink__[3:]
150+
event_name = "".join(word.capitalize() for word in name.split("_")) + "Event"
151+
if event_name not in self.lavalink_client._event_hooks:
152+
self.lavalink_client._event_hooks[event_name] = []
153+
self.lavalink_client._event_hooks[event_name].append(func)
154+
155+
async def _ready(self) -> None:
156+
self.__register_lavalink_listeners()
157+
await super()._ready()
158+
159+
160+
def listener(func=None, *, name: str = None):
161+
def wrapper(func):
162+
_name = name or func.__name__
163+
func.__lavalink__ = _name
164+
return func
165+
166+
if func is not None:
167+
return wrapper(func)
168+
return wrapper

interactions/ext/lavalink/helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from interactions import Channel, Guild, Member
44

55
from .models import VoiceState
6+
from .player import Player
67

78

89
@property
@@ -43,4 +44,13 @@ def guild_voice_states(self) -> Optional[List[VoiceState]]:
4344
]
4445

4546

47+
@property
48+
def player(self) -> Optional[Player]:
49+
"""
50+
Returns player of the guild.
51+
"""
52+
return self._client._bot_var.lavalink_client.player_manager.get(int(self.id))
53+
54+
4655
Guild.voice_states = guild_voice_states
56+
Guild.player = player

interactions/ext/lavalink/models.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from datetime import datetime
2-
from typing import Optional
2+
from typing import TYPE_CHECKING, Optional
33

44
from interactions.api.models.attrs_utils import ClientSerializerMixin, define, field
55

6-
from interactions import Channel, Guild, Member, Snowflake
6+
from interactions import Channel, Guild, LibraryException, Member, Snowflake
7+
8+
if TYPE_CHECKING:
9+
from .player import Player
710

811
__all__ = ["VoiceState", "VoiceServer"]
912

@@ -134,6 +137,18 @@ async def get_guild(self) -> Guild:
134137
return guild
135138
return Guild(**await self._client.get_guild(int(self.guild_id)), _client=self._client)
136139

140+
async def connect(self, self_deaf: bool = False, self_mute: bool = False) -> "Player":
141+
if not self.channel_id:
142+
raise LibraryException(message="User not connected to the voice channel!")
143+
144+
await self._client._bot_var._websocket.connect_voice_channel(
145+
self.guild_id, self.channel_id, self_deaf, self_mute
146+
)
147+
player = self._client._bot_var.lavalink_client.player_manager.get(int(self.guild_id))
148+
if player is None:
149+
player = self._client._bot_var.lavalink_client.player_manager.create(int(self.guild_id))
150+
return player
151+
137152

138153
@define()
139154
class VoiceServer(ClientSerializerMixin):
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import List
2+
3+
from lavalink import AudioTrack, DefaultPlayer
4+
5+
__all__ = ["Player"]
6+
7+
8+
class Player(DefaultPlayer):
9+
async def search_youtube(self, query: str) -> List[AudioTrack]:
10+
res = await self.node.get_tracks(f"ytsearch: {query}")
11+
return res.tracks
12+
13+
async def search_soundcloud(self, query: str) -> List[AudioTrack]:
14+
res = await self.node.get_tracks(f"scsearch: {query}")
15+
return res.tracks
16+
17+
async def get_tracks(self, url: str) -> List[AudioTrack]:
18+
res = await self.node.get_tracks(url)
19+
return res.tracks

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
discord-py-interactions>=4.3.0
1+
discord-py-interactions>=4.3.0, <4.3.2
22
lavalink~=4.0.1

setup.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@
2727
author_email=AUTHOR_EMAIL,
2828
description=DESCRIPTION,
2929
include_package_data=True,
30-
install_requires=[
31-
"discord-py-interactions>=4.3.0",
32-
"lavalink~=4.0.1"
33-
],
30+
install_requires=["discord-py-interactions>=4.3.0, <4.3.2", "lavalink~=4.0.1"],
3431
license="GPL-3.0 License",
3532
long_description=README,
3633
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)