Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/angelobreuer/Lavalink4NET in…
Browse files Browse the repository at this point in the history
…to dev
  • Loading branch information
angelobreuer committed Mar 4, 2024
2 parents edd964e + 43c2c00 commit d5c2c98
Show file tree
Hide file tree
Showing 14 changed files with 871 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: dotnet restore src/Lavalink4NET.sln

- name: Build
run: dotnet build src/Lavalink4NET.sln --no-restore --configuration ${{ matrix.configuration }} /property:Version=4.0.12-beta.7
run: dotnet build src/Lavalink4NET.sln --no-restore --configuration ${{ matrix.configuration }} /property:Version=4.0.13

- name: Run tests
working-directory: ci
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- Center badges -->
<p align="center"><b>High performance Lavalink wrapper for .NET</b></p>

[Lavalink4NET](https://github.com/angelobreuer/Lavalink4NET) is a [Lavalink](https://github.com/freyacodes/Lavalink) wrapper with node clustering, caching and custom players for .NET with support for [Discord.Net](https://github.com/RogueException/Discord.Net), [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus/) and [Remora](https://github.com/Remora/Remora.Discord).
[Lavalink4NET](https://github.com/angelobreuer/Lavalink4NET) is a [Lavalink](https://github.com/freyacodes/Lavalink) wrapper with node clustering, caching and custom players for .NET with support for [Discord.Net](https://github.com/RogueException/Discord.Net), [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus/), [Remora](https://github.com/Remora/Remora.Discord), and [NetCord](https://github.com/KubaZ2/NetCord).

[![Lavalink4NET Support Server Banner](https://discordapp.com/api/guilds/894533462428635146/embed.png?style=banner3)](https://discord.gg/cD4qTmnqRg)

Expand Down Expand Up @@ -60,6 +60,8 @@ Lavalink4NET offers high flexibility and extensibility by providing an isolated

- [**Lavalink4NET.Remora.Discord**](https://www.nuget.org/packages/Lavalink4NET.Remora.Discord/)&nbsp;&nbsp;&nbsp;![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Remora.Discord.svg?style=flat-square)<br>Add powerful audio playback to your Remora-based discord bots with this integration for Lavalink4NET. Suitable for end users developing with Remora.

- [**Lavalink4NET.NetCord**](https://www.nuget.org/packages/Lavalink4NET.NetCord/)&nbsp;&nbsp;&nbsp;![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.NetCord.svg?style=flat-square)<br>Add powerful audio playback to your NetCord-based discord bots with this integration for Lavalink4NET. Suitable for end users developing with NetCord.

#### _Clustering_

- [**Lavalink4NET.Cluster**](https://www.nuget.org/packages/Lavalink4NET.Cluster/)&nbsp;&nbsp;&nbsp;![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Cluster.svg?style=flat-square)<br>Scale and improve performance by using multiple Lavalink nodes with this cluster support module. Ideal for handling high-demand music streaming applications.
Expand Down Expand Up @@ -118,7 +120,7 @@ var audioService = app.Services.GetRequiredService<IAudioService>();
// Play a track
var playerOptions = new LavalinkPlayerOptions
{
InitialTrack = new TrackReference("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
InitialTrack = new TrackQueueItem("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
};

await audioService.Players
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>ExampleBot</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="NetCord" Version="1.0.0-alpha.270" />
<PackageReference Include="NetCord.Hosting" Version="1.0.0-alpha.40" />
<PackageReference Include="NetCord.Hosting.Services" Version="1.0.0-alpha.49" />
<PackageReference Include="NetCord.Services" Version="1.0.0-alpha.171" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj" />
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions samples/Lavalink4NET.Samples.NetCord/MusicModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace ExampleBot;

using Lavalink4NET;
using Lavalink4NET.NetCord;
using Lavalink4NET.Players;
using Lavalink4NET.Rest.Entities.Tracks;
using NetCord.Services.ApplicationCommands;

public class MusicModule(IAudioService audioService) : ApplicationCommandModule<SlashCommandContext>
{
[SlashCommand("play", "Plays a track!")]
public async Task<string> PlayAsync([SlashCommandParameter(Name = "query", Description = "The query to search for")] string query)
{
var retrieveOptions = new PlayerRetrieveOptions(ChannelBehavior: PlayerChannelBehavior.Join);

var result = await audioService.Players
.RetrieveAsync(Context, playerFactory: PlayerFactory.Queued, retrieveOptions);

if (!result.IsSuccess)
{
return GetErrorMessage(result.Status);
}

var player = result.Player;

var track = await audioService.Tracks
.LoadTrackAsync(query, TrackSearchMode.YouTube);

if (track is null)
{
return "No tracks found.";
}

await player.PlayAsync(track);

return $"Now playing: {track.Title}";
}

private static string GetErrorMessage(PlayerRetrieveStatus retrieveStatus) => retrieveStatus switch
{
PlayerRetrieveStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
PlayerRetrieveStatus.BotNotConnected => "The bot is currently not connected.",
_ => "Unknown error.",
};
}
18 changes: 18 additions & 0 deletions samples/Lavalink4NET.Samples.NetCord/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Lavalink4NET.NetCord;
using Microsoft.Extensions.Hosting;
using NetCord;
using NetCord.Hosting.Gateway;
using NetCord.Hosting.Services;
using NetCord.Hosting.Services.ApplicationCommands;
using NetCord.Services.ApplicationCommands;

var builder = Host.CreateDefaultBuilder(args)
.UseDiscordGateway()
.UseLavalink()
.UseApplicationCommands<SlashCommandInteraction, SlashCommandContext>();

var host = builder.Build()
.AddModules(typeof(Program).Assembly)
.UseGatewayEventHandlers();

host.Run();
62 changes: 62 additions & 0 deletions src/Lavalink4NET.NetCord/DiscordClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;
using Lavalink4NET.Clients.Events;
using Lavalink4NET.Events;

public sealed class DiscordClientWrapper : IDiscordClientWrapper
{
private readonly IDiscordClientWrapper _client;

public DiscordClientWrapper(GatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = new GatewayClientWrapper(client);
}

public DiscordClientWrapper(ShardedGatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = new ShardedGatewayClientWrapper(client);
}

public event AsyncEventHandler<VoiceServerUpdatedEventArgs>? VoiceServerUpdated
{
add => _client.VoiceServerUpdated += value;
remove => _client.VoiceServerUpdated -= value;
}

public event AsyncEventHandler<VoiceStateUpdatedEventArgs>? VoiceStateUpdated
{
add => _client.VoiceStateUpdated += value;
remove => _client.VoiceStateUpdated -= value;
}

public ValueTask<ImmutableArray<ulong>> GetChannelUsersAsync(ulong guildId, ulong voiceChannelId, bool includeBots = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.GetChannelUsersAsync(guildId, voiceChannelId, includeBots, cancellationToken);
}

public ValueTask SendVoiceUpdateAsync(ulong guildId, ulong? voiceChannelId, bool selfDeaf = false, bool selfMute = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.SendVoiceUpdateAsync(guildId, voiceChannelId, selfDeaf, selfMute, cancellationToken);
}

public ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.WaitForReadyAsync(cancellationToken);
}
}
49 changes: 49 additions & 0 deletions src/Lavalink4NET.NetCord/GatewayClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;

internal sealed class GatewayClientWrapper : GatewayClientWrapperBase, IDiscordClientWrapper, IDisposable
{
private readonly GatewayClient _client;

public GatewayClientWrapper(GatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = client;

_client.VoiceStateUpdate += HandleVoiceStateUpdateAsync;
_client.VoiceServerUpdate += HandleVoiceServerUpdateAsync;
}

public void Dispose()
{
_client.VoiceStateUpdate -= HandleVoiceStateUpdateAsync;
_client.VoiceServerUpdate -= HandleVoiceServerUpdateAsync;
}

public override async ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

await _client.ReadyAsync
.WaitAsync(cancellationToken)
.ConfigureAwait(false);

var shardCount = _client.Shard?.Count ?? 1;

return new ClientInformation("NetCord", _client.Id, shardCount);
}

protected override GatewayClient GetClient(ulong guildId) => _client;

protected override bool TryGetGuild(ulong guildId, [MaybeNullWhen(false)] out Guild guild)
{
return _client.Cache.Guilds.TryGetValue(guildId, out guild);
}
}
104 changes: 104 additions & 0 deletions src/Lavalink4NET.NetCord/GatewayClientWrapperBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;
using Lavalink4NET.Clients.Events;
using Lavalink4NET.Events;

internal abstract class GatewayClientWrapperBase : IDiscordClientWrapper
{
public event AsyncEventHandler<VoiceServerUpdatedEventArgs>? VoiceServerUpdated;

public event AsyncEventHandler<VoiceStateUpdatedEventArgs>? VoiceStateUpdated;

public ValueTask<ImmutableArray<ulong>> GetChannelUsersAsync(ulong guildId, ulong voiceChannelId, bool includeBots = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (!TryGetGuild(guildId, out var guild))
{
return new ValueTask<ImmutableArray<ulong>>([]);
}

var currentUserId = GetClient(guildId).Id;

var voiceStates = guild.VoiceStates
.Where(x => x.Value.ChannelId == voiceChannelId)
.Where(x => x.Value.UserId != currentUserId);

if (!includeBots)
{
voiceStates = voiceStates.Where(x => x.Value.User is not { IsBot: true, });
}

var userIds = voiceStates.Select(x => x.Value.UserId).ToImmutableArray();
return new ValueTask<ImmutableArray<ulong>>(userIds);
}

public async ValueTask SendVoiceUpdateAsync(ulong guildId, ulong? voiceChannelId, bool selfDeaf = false, bool selfMute = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

var voiceStateProperties = new VoiceStateProperties(guildId, voiceChannelId) { SelfDeaf = selfDeaf, SelfMute = selfMute, };

await GetClient(guildId)
.UpdateVoiceStateAsync(voiceStateProperties)
.ConfigureAwait(false);
}

protected abstract bool TryGetGuild(ulong guildId, [MaybeNullWhen(false)] out Guild guild);

protected abstract GatewayClient GetClient(ulong guildId);

protected ValueTask HandleVoiceServerUpdateAsync(VoiceServerUpdateEventArgs eventArgs)
{
ArgumentNullException.ThrowIfNull(eventArgs);

if (eventArgs.Endpoint is null)
{
return default;
}

var voiceServerUpdatedEventArgs = new VoiceServerUpdatedEventArgs(
guildId: eventArgs.GuildId,
voiceServer: new VoiceServer(eventArgs.Token, eventArgs.Endpoint));

return VoiceServerUpdated.InvokeAsync(this, voiceServerUpdatedEventArgs);
}

protected async ValueTask HandleVoiceStateUpdateAsync(global::NetCord.Gateway.VoiceState eventArgs)
{
ArgumentNullException.ThrowIfNull(eventArgs);

// Retrieve previous voice state from cache
var previousVoiceState = TryGetGuild(eventArgs.GuildId, out var guild)
&& guild.VoiceStates.TryGetValue(eventArgs.UserId, out var previousVoiceStateData)
? new Clients.VoiceState(VoiceChannelId: previousVoiceStateData.ChannelId, SessionId: previousVoiceStateData.SessionId)
: default;

var currentUserId = GetClient(eventArgs.GuildId).Id;

var updatedVoiceState = new Clients.VoiceState(
VoiceChannelId: eventArgs.ChannelId,
SessionId: eventArgs.SessionId);

var voiceStateUpdatedEventArgs = new VoiceStateUpdatedEventArgs(
eventArgs.GuildId,
eventArgs.UserId,
eventArgs.UserId == currentUserId,
updatedVoiceState,
previousVoiceState);

await VoiceStateUpdated
.InvokeAsync(this, voiceStateUpdatedEventArgs)
.ConfigureAwait(false);
}

public abstract ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default);
}
14 changes: 14 additions & 0 deletions src/Lavalink4NET.NetCord/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Lavalink4NET.NetCord;

using System;
using Microsoft.Extensions.Hosting;

public static class HostBuilderExtensions
{
public static IHostBuilder UseLavalink(this IHostBuilder hostBuilder)
{
ArgumentNullException.ThrowIfNull(hostBuilder);

return hostBuilder.ConfigureServices(static (_, services) => services.AddLavalink());
}
}
23 changes: 23 additions & 0 deletions src/Lavalink4NET.NetCord/Lavalink4NET.NetCord.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>

<!-- Package Description -->
<Description>High performance Lavalink wrapper for .NET | Add powerful audio playback to your NetCord-based applications with this integration for Lavalink4NET. Suitable for end users developing with NetCord.</Description>
<PackageTags>lavalink,lavalink-wrapper,discord,discord-music,discord-music-bot,netcord</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NetCord" Version="1.0.0-alpha.270" />
<PackageReference Include="NetCord.Services" Version="1.0.0-alpha.171" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Lavalink4NET\Lavalink4NET.csproj" />
</ItemGroup>

<Import Project="../Lavalink4NET.targets" />
</Project>
Loading

0 comments on commit d5c2c98

Please sign in to comment.