Skip to content

Commit

Permalink
feat: Implement clustering
Browse files Browse the repository at this point in the history
  • Loading branch information
angelobreuer committed Jul 10, 2023
1 parent 0659f7c commit 1dee038
Show file tree
Hide file tree
Showing 80 changed files with 2,063 additions and 669 deletions.
74 changes: 74 additions & 0 deletions samples/Lavalink4NET.Samples.Cluster/DiscordClientHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Lavalink4NET.DiscordNet.ExampleBot;

using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Hosting;

internal sealed class DiscordClientHost : IHostedService
{
private readonly DiscordSocketClient _discordSocketClient;
private readonly InteractionService _interactionService;
private readonly IServiceProvider _serviceProvider;

public DiscordClientHost(
DiscordSocketClient discordSocketClient,
InteractionService interactionService,
IServiceProvider serviceProvider)
{
ArgumentNullException.ThrowIfNull(discordSocketClient);
ArgumentNullException.ThrowIfNull(interactionService);
ArgumentNullException.ThrowIfNull(serviceProvider);

_discordSocketClient = discordSocketClient;
_interactionService = interactionService;
_serviceProvider = serviceProvider;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
_discordSocketClient.InteractionCreated += InteractionCreated;
_discordSocketClient.Ready += ClientReady;

// Put bot token here
await _discordSocketClient
.LoginAsync(TokenType.Bot, "")
.ConfigureAwait(false);

await _discordSocketClient
.StartAsync()
.ConfigureAwait(false);
}

public async Task StopAsync(CancellationToken cancellationToken)
{
_discordSocketClient.InteractionCreated -= InteractionCreated;
_discordSocketClient.Ready -= ClientReady;

await _discordSocketClient
.StopAsync()
.ConfigureAwait(false);
}

private Task InteractionCreated(SocketInteraction interaction)
{
var interactionContext = new SocketInteractionContext(_discordSocketClient, interaction);
return _interactionService!.ExecuteCommandAsync(interactionContext, _serviceProvider);
}

private async Task ClientReady()
{
await _interactionService
.AddModulesAsync(Assembly.GetExecutingAssembly(), _serviceProvider)
.ConfigureAwait(false);

// Put your guild id to test here
await _interactionService
.RegisterCommandsToGuildAsync(0)
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net.Interactions" Version="3.10.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />

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

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

using System;
using System.Threading.Tasks;
using Discord.Interactions;
using Lavalink4NET.Clients;
using Lavalink4NET.DiscordNet;
using Lavalink4NET.Players;
using Lavalink4NET.Players.Queued;
using Lavalink4NET.Rest.Entities.Tracks;

/// <summary>
/// Presents some of the main features of the Lavalink4NET-Library.
/// </summary>
[RequireContext(ContextType.Guild)]
public sealed class MusicModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly IAudioService _audioService;

public MusicModule(IAudioService audioService)
{
ArgumentNullException.ThrowIfNull(audioService);

_audioService = audioService;
}

[SlashCommand("play", description: "Plays music", runMode: RunMode.Async)]
public async Task Play(string query)
{
await DeferAsync().ConfigureAwait(false);

var player = await GetPlayerAsync(connectToVoiceChannel: true).ConfigureAwait(false);

if (player is null)
{
return;
}

var track = await _audioService.Tracks
.LoadTrackAsync(query, TrackSearchMode.YouTube)
.ConfigureAwait(false);

if (track is null)
{
await FollowupAsync("😖 No results.").ConfigureAwait(false);
return;
}

var position = await player.PlayAsync(track).ConfigureAwait(false);

if (position is 0)
{
await FollowupAsync($"🔈 Playing: {track.Uri}").ConfigureAwait(false);
}
else
{
await FollowupAsync($"🔈 Added to queue: {track.Uri}").ConfigureAwait(false);
}
}

private async ValueTask<QueuedLavalinkPlayer?> GetPlayerAsync(bool connectToVoiceChannel = true)
{
var joinOptions = new PlayerJoinOptions(ConnectToVoiceChannel: connectToVoiceChannel);

var result = await _audioService.Players
.GetOrJoinAsync(Context, playerFactory: PlayerFactory.Queued, joinOptions)
.ConfigureAwait(false);

if (!result.IsSuccess)
{
var errorMessage = result.Status switch
{
PlayerJoinStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
PlayerJoinStatus.BotNotConnected => "The bot is currently not connected.",
_ => "Unknown error.",
};

await FollowupAsync(errorMessage).ConfigureAwait(false);
return null;
}

return result.Player;
}
}
31 changes: 31 additions & 0 deletions samples/Lavalink4NET.Samples.Cluster/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections.Immutable;
using Discord.Interactions;
using Discord.WebSocket;
using Lavalink4NET.Cluster.Extensions;
using Lavalink4NET.Cluster.Nodes;
using Lavalink4NET.DiscordNet;
using Lavalink4NET.DiscordNet.ExampleBot;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = new HostApplicationBuilder(args);

// Discord
builder.Services.AddSingleton<DiscordSocketClient>();
builder.Services.AddSingleton<InteractionService>();
builder.Services.AddHostedService<DiscordClientHost>();

// Lavalink
builder.Services.AddLavalinkCluster<DiscordClientWrapper>();
builder.Services.AddLogging(x => x.AddConsole().SetMinimumLevel(LogLevel.Trace));

builder.Services.ConfigureLavalinkCluster(x =>
{
x.Nodes = ImmutableArray.Create(
new LavalinkClusterNodeOptions { BaseAddress = new Uri("http://localhost:2333/"), },
new LavalinkClusterNodeOptions { BaseAddress = new Uri("http://localhost:2334/"), },
new LavalinkClusterNodeOptions { BaseAddress = new Uri("http://localhost:2335/"), });
});

builder.Build().Run();
142 changes: 142 additions & 0 deletions src/Lavalink4NET.Cluster/ClusterAudioService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
namespace Lavalink4NET.Cluster;

using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Lavalink4NET.Clients;
using Lavalink4NET.Cluster.Nodes;
using Lavalink4NET.Cluster.Rest;
using Lavalink4NET.Integrations;
using Lavalink4NET.Players;
using Lavalink4NET.Rest;
using Lavalink4NET.Socket;
using Lavalink4NET.Tracks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

internal sealed class ClusterAudioService : AudioServiceBase, IClusterAudioService
{
private readonly object _syncRoot;
private readonly ClusterAudioServiceOptions _options;
private readonly LavalinkNodeServiceContext _serviceContext;
private readonly CancellationTokenSource _shutdownCancellationTokenSource;
private readonly CancellationToken _shutdownCancellationToken;
private readonly ILavalinkApiClientFactory _lavalinkApiClientFactory;
private readonly ILoggerFactory _loggerFactory;

public ClusterAudioService(
IDiscordClientWrapper discordClient,
ILavalinkSocketFactory socketFactory,
ILavalinkApiClientProvider lavalinkApiClientProvider,
ILavalinkApiClientFactory lavalinkApiClientFactory,
IIntegrationManager integrations,
IPlayerManager players,
ITrackManager tracks,
IOptions<ClusterAudioServiceOptions> options,
ILoggerFactory loggerFactory)
: base(lavalinkApiClientProvider, integrations, players, tracks)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(discordClient);
ArgumentNullException.ThrowIfNull(loggerFactory);

_syncRoot = new object();
_options = options.Value;

_serviceContext = new LavalinkNodeServiceContext(
ClientWrapper: discordClient,
LavalinkSocketFactory: socketFactory,
IntegrationManager: Integrations,
PlayerManager: Players,
NodeListener: this);

Nodes = ImmutableArray<ILavalinkNode>.Empty;
_lavalinkApiClientFactory = lavalinkApiClientFactory;
_loggerFactory = loggerFactory;

_shutdownCancellationTokenSource = new CancellationTokenSource();
_shutdownCancellationToken = _shutdownCancellationTokenSource.Token;
}

public ImmutableArray<ILavalinkNode> Nodes { get; private set; }

public ValueTask<ILavalinkNode> AddAsync(IOptions<LavalinkClusterNodeOptions> options, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(options);

var nodeLogger = _loggerFactory.CreateLogger<LavalinkNode>();
var apiClient = _lavalinkApiClientFactory.Create(options);

var node = new LavalinkClusterNode(
cluster: this,
apiClient: apiClient,
serviceContext: _serviceContext,
options: options,
shutdownCancellationToken: _shutdownCancellationToken,
logger: nodeLogger);

lock (_syncRoot)
{
Nodes = Nodes.Add(node);
}

return ValueTask.FromResult<ILavalinkNode>(node);
}

public async ValueTask<bool> RemoveAsync(ILavalinkNode node, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(node);

lock (_syncRoot)
{
var previousNodes = Nodes;
Nodes = previousNodes.Remove(node);

if (previousNodes == Nodes)
{
return false;
}
}

await node
.StopAsync(cancellationToken)
.ConfigureAwait(false);

return true;
}

public override void Dispose()
{
_shutdownCancellationTokenSource.Dispose();
}

public override async ValueTask StartAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

foreach (var nodeOptions in _options.Nodes)
{
await AddAsync(Options.Create(nodeOptions), cancellationToken).ConfigureAwait(false);
}
}

public override async ValueTask StopAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

_shutdownCancellationTokenSource.Cancel();

var nodes = Nodes;

foreach (var node in nodes)
{
await RemoveAsync(node, cancellationToken).ConfigureAwait(false);
}
}

public override ValueTask WaitForReadyAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException(); // TODO
}
}
15 changes: 15 additions & 0 deletions src/Lavalink4NET.Cluster/ClusterAudioServiceOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Lavalink4NET.Cluster;

using System;
using System.Collections.Immutable;
using Lavalink4NET.Cluster.Nodes;
using Microsoft.Extensions.Options;

public sealed record class ClusterAudioServiceOptions
{
public string HttpClientName { get; set; } = Options.DefaultName;

public TimeSpan ReadyTimeout { get; set; } = TimeSpan.FromSeconds(10);

public ImmutableArray<LavalinkClusterNodeOptions> Nodes { get; set; } = ImmutableArray.Create(new LavalinkClusterNodeOptions());
}
10 changes: 10 additions & 0 deletions src/Lavalink4NET.Cluster/ClusterNodeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Lavalink4NET.Cluster;

public sealed record class ClusterNodeOptions
{
public string Passphrase { get; set; } = "youshallnotpass";

public Uri? WebSocketUri { get; set; }


}
Loading

0 comments on commit 1dee038

Please sign in to comment.