Skip to content

Commit

Permalink
Merge pull request #5 from interactions-py/unstable
Browse files Browse the repository at this point in the history
  • Loading branch information
Damego authored Sep 10, 2022
2 parents 09ad92e + dde46af commit 9d0fc4e
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 45 deletions.
106 changes: 86 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,108 @@

## Usage

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

Main file:
```python
import interactions
from interactions.ext.lavalink import VoiceState, VoiceClient

client = VoiceClient(...)

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

@client.event()
async def on_voice_state_update(before: VoiceState, after: VoiceState):
client.start()
```

Extension file: `exts/music.py`
```python
import interactions
from interactions.ext.lavalink import VoiceClient, VoiceState, listener, Player
import lavalink


class Music(interactions.Extension):
def __init__(self, client):
self.client: VoiceClient = client

@listener()
async def on_track_start(self, event: lavalink.TrackStartEvent):
"""
Fires when track starts
"""
print("STARTED", event.track)

@interactions.extension_listener()
async def on_start(self):
self.client.lavalink_client.add_node("127.0.0.1", 43421, "your_password", "eu")

@interactions.extension_listener()
async def on_voice_state_update(self, before: VoiceState, after: VoiceState):
"""
Disconnect if bot is alone
"""
if before and not after.joined:
voice_states = self.client.get_channel_voice_states(before.channel_id)
if len(voice_states) == 1 and voice_states[0].user_id == self.client.me.id:
await self.client.disconnect(before.guild_id)

@interactions.extension_command()
@interactions.option()
async def play(self, ctx: interactions.CommandContext, query: str):
await ctx.defer()

# NOTE: ctx.author.voice can be None if you ran a bot after joining the voice channel
voice: VoiceState = ctx.author.voice
if not voice or not voice.joined:
return await ctx.send("You're not connected to the voice channel!")

player: Player # Typehint player variable to see their methods
if (player := ctx.guild.player) is None:
player = await voice.connect()

tracks = await player.search_youtube(query)
track = tracks[0]
player.add(requester=int(ctx.author.id), track=track)

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

@interactions.extension_command()
async def leave(self, ctx: interactions.CommandContext):
await self.client.disconnect(ctx.guild_id)
```

## Events
To listen lavalink event you have to use `@listener` decorator.

```python
import lavalink
from interactions.ext.lavalink import listener


# NOTE: Works only in extensions.
class MusicExt(Extension):
...

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

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

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

client.start()
```

Example with using `Extension` [here](https://github.com/Damego/interactions-lavalink/tree/main/examples)

## New methods/properties for interactions.py library

`Member.voice` - returns current member's `VoiceState`. It can be `None` if not cached.
Expand Down
22 changes: 15 additions & 7 deletions examples/exts/music.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import interactions
from interactions.ext.lavalink import VoiceClient, VoiceState
from lavalink import AudioTrack
from interactions.ext.lavalink import Player, VoiceClient, VoiceState


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

# NOTE: ctx.author.voice can be None if you runned a bot after joining the voice channel
player = await self.client.connect(ctx.author.voice.guild_id, ctx.author.voice.channel_id)
# NOTE: ctx.author.voice can be None if you ran a bot after joining the voice channel
voice: VoiceState = ctx.author.voice
if not voice or not voice.joined:
return await ctx.send("You're not connected to the voice channel!")

player: Player # Typehint player variable to see their methods
if (player := ctx.guild.player) is None:
player = await voice.connect()

results = await player.node.get_tracks(f"ytsearch:{query}")
track = AudioTrack(results["tracks"][0], int(ctx.author.id))
tracks = await player.search_youtube(query)
track = tracks[0]
player.add(requester=int(ctx.author.id), track=track)
await player.play()

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

@interactions.extension_command()
Expand Down
2 changes: 1 addition & 1 deletion interactions/ext/lavalink/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from interactions.ext.version import Version, VersionAuthor

__all__ = ["version", "base"]
__version__ = "0.0.3"
__version__ = "0.1.0"

version = Version(
version=__version__, author=VersionAuthor(name="Damego", email="[email protected]")
Expand Down
55 changes: 45 additions & 10 deletions interactions/ext/lavalink/client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from inspect import getmembers
from typing import Dict, List, Optional, Union

from lavalink import Client as LavalinkClient
from lavalink import DefaultPlayer

from interactions import Client, Snowflake
from interactions import Client, LibraryException, Snowflake

from .models import VoiceState
from .player import Player
from .websocket import VoiceWebSocketClient

__all__ = ["VoiceClient"]
__all__ = ["VoiceClient", "listener"]


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

self._websocket = VoiceWebSocketClient(token, self._intents)
self.lavalink_client = LavalinkClient(int(self.me.id))
self.lavalink_client = LavalinkClient(int(self.me.id), player=Player)

self._websocket._dispatch.register(
self.__raw_voice_state_update, "on_raw_voice_state_update"
Expand All @@ -25,7 +26,7 @@ def __init__(self, token: str, **kwargs):
self.__raw_voice_server_update, "on_raw_voice_server_update"
)

self._websocket._bot_var = self
self._websocket._http._bot_var = self
self._http._bot_var = self

async def __raw_voice_state_update(self, data: dict):
Expand All @@ -42,7 +43,7 @@ async def connect(
channel_id: Union[Snowflake, int, str],
self_deaf: bool = False,
self_mute: bool = False,
) -> DefaultPlayer:
) -> Player:
"""
Connects to voice channel and creates player.
Expand All @@ -55,26 +56,35 @@ async def connect(
:param self_mute: Whether bot is self muted
:type self_mute: bool
:return: Created guild player.
:rtype: DefaultPlayer
:rtype: Player
"""
# Discord will fire INVALID_SESSION if channel_id is None
if guild_id is None:
raise LibraryException(message="Missed requirement argument: guild_id")
if channel_id is None:
raise LibraryException(message="Missed requirement argument: channel_id")

await self._websocket.connect_voice_channel(guild_id, channel_id, self_deaf, self_mute)
player = self.lavalink_client.player_manager.get(int(guild_id))
if player is None:
player = self.lavalink_client.player_manager.create(int(guild_id))
return player

async def disconnect(self, guild_id: Union[Snowflake, int]):
if guild_id is None:
raise LibraryException(message="Missed requirement argument: guild_id")

await self._websocket.disconnect_voice_channel(int(guild_id))
await self.lavalink_client.player_manager.destroy(int(guild_id))

def get_player(self, guild_id: Union[Snowflake, int]) -> DefaultPlayer:
def get_player(self, guild_id: Union[Snowflake, int]) -> Player:
"""
Returns current player in guild.
:param guild_id: The guild id
:type guild_id: Union[Snowflake, int]
:return: Guild player
:rtype: DefaultPlayer
:rtype: Player
"""
return self.lavalink_client.player_manager.get(int(guild_id))

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

def get_guild_voice_states(self, guild_id: Union[Snowflake, int]):
def get_guild_voice_states(self, guild_id: Union[Snowflake, int]) -> Optional[List[VoiceState]]:
"""
Returns guild voice states.
Expand Down Expand Up @@ -131,3 +141,28 @@ def get_channel_voice_states(
for voice_state in self.voice_states.values()
if voice_state.channel_id == _channel_id
]

def __register_lavalink_listeners(self):
for extension in self._extensions.values():
for name, func in getmembers(extension):
if hasattr(func, "__lavalink__"):
name = func.__lavalink__[3:]
event_name = "".join(word.capitalize() for word in name.split("_")) + "Event"
if event_name not in self.lavalink_client._event_hooks:
self.lavalink_client._event_hooks[event_name] = []
self.lavalink_client._event_hooks[event_name].append(func)

async def _ready(self) -> None:
self.__register_lavalink_listeners()
await super()._ready()


def listener(func=None, *, name: str = None):
def wrapper(func):
_name = name or func.__name__
func.__lavalink__ = _name
return func

if func is not None:
return wrapper(func)
return wrapper
10 changes: 10 additions & 0 deletions interactions/ext/lavalink/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from interactions import Channel, Guild, Member

from .models import VoiceState
from .player import Player


@property
Expand Down Expand Up @@ -43,4 +44,13 @@ def guild_voice_states(self) -> Optional[List[VoiceState]]:
]


@property
def player(self) -> Optional[Player]:
"""
Returns player of the guild.
"""
return self._client._bot_var.lavalink_client.player_manager.get(int(self.id))


Guild.voice_states = guild_voice_states
Guild.player = player
19 changes: 17 additions & 2 deletions interactions/ext/lavalink/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from datetime import datetime
from typing import Optional
from typing import TYPE_CHECKING, Optional

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

from interactions import Channel, Guild, Member, Snowflake
from interactions import Channel, Guild, LibraryException, Member, Snowflake

if TYPE_CHECKING:
from .player import Player

__all__ = ["VoiceState", "VoiceServer"]

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

async def connect(self, self_deaf: bool = False, self_mute: bool = False) -> "Player":
if not self.channel_id:
raise LibraryException(message="User not connected to the voice channel!")

await self._client._bot_var._websocket.connect_voice_channel(
self.guild_id, self.channel_id, self_deaf, self_mute
)
player = self._client._bot_var.lavalink_client.player_manager.get(int(self.guild_id))
if player is None:
player = self._client._bot_var.lavalink_client.player_manager.create(int(self.guild_id))
return player


@define()
class VoiceServer(ClientSerializerMixin):
Expand Down
19 changes: 19 additions & 0 deletions interactions/ext/lavalink/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import List

from lavalink import AudioTrack, DefaultPlayer

__all__ = ["Player"]


class Player(DefaultPlayer):
async def search_youtube(self, query: str) -> List[AudioTrack]:
res = await self.node.get_tracks(f"ytsearch: {query}")
return res.tracks

async def search_soundcloud(self, query: str) -> List[AudioTrack]:
res = await self.node.get_tracks(f"scsearch: {query}")
return res.tracks

async def get_tracks(self, url: str) -> List[AudioTrack]:
res = await self.node.get_tracks(url)
return res.tracks
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
discord-py-interactions>=4.3.0
discord-py-interactions>=4.3.0, <4.3.2
lavalink~=4.0.1
5 changes: 1 addition & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@
author_email=AUTHOR_EMAIL,
description=DESCRIPTION,
include_package_data=True,
install_requires=[
"discord-py-interactions>=4.3.0",
"lavalink~=4.0.1"
],
install_requires=["discord-py-interactions>=4.3.0, <4.3.2", "lavalink~=4.0.1"],
license="GPL-3.0 License",
long_description=README,
long_description_content_type="text/markdown",
Expand Down

0 comments on commit 9d0fc4e

Please sign in to comment.