mirror of
https://github.com/Myxelium/Lunaris2.0.git
synced 2026-04-13 08:00:37 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b79e56d3a1 | |||
| fa19f8d938 | |||
| ac869c43da | |||
| e2fdd9a2d7 | |||
| 98761fc91d | |||
| 373d482906 | |||
|
|
e044f2f91b | ||
| 4855d37d76 | |||
| 4ba01ed72b |
38
.github/workflows/dotnet.yml
vendored
38
.github/workflows/dotnet.yml
vendored
@@ -14,24 +14,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # required for github-action-get-previous-tag
|
fetch-depth: 0 # required for github-action-get-previous-tag
|
||||||
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '8.0.x'
|
|
||||||
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore ./Bot/Lunaris2.csproj
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build ./Bot/Lunaris2.csproj --no-restore -c Release -o ./out
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
|
|
||||||
|
|
||||||
- name: Zip the build
|
|
||||||
run: 7z a -tzip ./out/Lunaris.zip ./out/*
|
|
||||||
|
|
||||||
- name: Get previous tag
|
- name: Get previous tag
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
|
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
|
||||||
@@ -44,6 +27,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: ${{ steps.previoustag.outputs.tag }}
|
version: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore ./Bot/Lunaris2.csproj
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build ./Bot/Lunaris2.csproj --no-restore -c Release /p:AssemblyVersion=${{ steps.previoustag.outputs.tag }} -o ./out
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
|
||||||
|
|
||||||
|
- name: Zip the build
|
||||||
|
run: 7z a -tzip ./out/Lunaris.zip ./out/*
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@@ -63,5 +63,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./out/Lunaris.zip
|
asset_path: ./out/Lunaris.zip
|
||||||
asset_name: Lunaris.zip
|
asset_name: Lunaris_${{steps.semver.outputs.patch}}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
|||||||
.vs
|
.vs
|
||||||
bin
|
bin
|
||||||
appsettings.json
|
appsettings.json
|
||||||
|
ollama
|
||||||
|
plugins
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -19,13 +21,41 @@ public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotifi
|
|||||||
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await BotMentioned(notification, cancellationToken);
|
await BotMentioned(notification, cancellationToken);
|
||||||
|
await Statistics(notification, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Statistics(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (notification.Message.Content.Contains("!LunarisStats"))
|
||||||
|
{
|
||||||
|
var servers = _client.Guilds.Select(guild => guild.Name);
|
||||||
|
var channels = _client.Guilds
|
||||||
|
.SelectMany(guild => guild.VoiceChannels)
|
||||||
|
.Where(channel => channel.Users.Any(user => user.IsBot));
|
||||||
|
|
||||||
|
var table = new StringBuilder();
|
||||||
|
var serverColumnWidth = 25; // Width for server column
|
||||||
|
var channelColumnWidth = 25; // Width for channel column
|
||||||
|
table.AppendLine($"{"Servers".PadRight(serverColumnWidth - 1)}|{"Channels".PadRight(channelColumnWidth - 1)}");
|
||||||
|
table.AppendLine($"{new string('-', serverColumnWidth - 1)}|{new string('-', channelColumnWidth - 1)}");
|
||||||
|
foreach (var (server, channel) in servers.Zip(channels))
|
||||||
|
{
|
||||||
|
table.AppendLine($"{server.PadRight(serverColumnWidth - 1)}|{channel.Name.PadRight(channelColumnWidth - 1)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Lunaris Statistics")
|
||||||
|
.WithDescription(table.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await notification.Message.Channel.SendMessageAsync(embed: embed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
|
if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
|
||||||
{
|
{
|
||||||
// The bot was mentioned
|
|
||||||
const string pattern = "<.*?>";
|
const string pattern = "<.*?>";
|
||||||
const string replacement = "";
|
const string replacement = "";
|
||||||
var regex = new Regex(pattern);
|
var regex = new Regex(pattern);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.ClearQueueCommand;
|
||||||
|
|
||||||
|
public record ClearQueueCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class DisconnectHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<ClearQueueCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(ClearQueueCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await player.Queue.ClearAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await context.SendMessageAsync("Cleared queue. No songs are queued.", client).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,6 @@ public class DisconnectHandler(DiscordSocketClient client, IAudioService audioSe
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
|
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await context.RespondAsync("Disconnected.").ConfigureAwait(false);
|
await context.SendMessageAsync("Disconnected.", client).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Net;
|
||||||
using Discord;
|
using Discord;
|
||||||
|
using Discord.Net;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
@@ -38,15 +40,15 @@ public static class MessageModule
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
|
public static async Task RemoveMessages(this SocketSlashCommand context, DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
var guildId = context.GetGuild(client).Id;
|
var guildId = context.GetGuild(client).Id;
|
||||||
|
|
||||||
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
{
|
{
|
||||||
if (value.Count <= 0)
|
if (value.Count <= 0)
|
||||||
return guildId;
|
return;
|
||||||
|
|
||||||
foreach (var messageId in value)
|
foreach (var messageId in value)
|
||||||
{
|
{
|
||||||
@@ -57,8 +59,43 @@ public static class MessageModule
|
|||||||
|
|
||||||
value.Clear();
|
value.Clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
var guildId = context.GetGuild(client).Id;
|
||||||
|
|
||||||
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
|
{
|
||||||
|
if (value.Count <= 0)
|
||||||
|
return guildId;
|
||||||
|
|
||||||
|
// Create a copy of the list to avoid modifying it during iteration
|
||||||
|
var messagesToDelete = new List<ulong>(value);
|
||||||
|
|
||||||
|
foreach (var messageId in messagesToDelete)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
||||||
|
if (messageToDelete != null)
|
||||||
|
{
|
||||||
|
await messageToDelete.DeleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.HttpCode != HttpStatusCode.NotFound)
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the list after we're done with the iteration
|
||||||
|
value.Clear();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If the guildId does not exist, add it to the dictionary
|
||||||
GuildMessageIds.Add(guildId, new List<ulong>());
|
GuildMessageIds.Add(guildId, new List<ulong>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET.Players.Queued;
|
||||||
using Lavalink4NET.Tracks;
|
using Lavalink4NET.Tracks;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
@@ -11,30 +12,38 @@ public class MusicEmbed
|
|||||||
string title,
|
string title,
|
||||||
string length,
|
string length,
|
||||||
string artist,
|
string artist,
|
||||||
string queuedBy)
|
string queuedBy,
|
||||||
|
string? nextSong = null)
|
||||||
{
|
{
|
||||||
|
var getNextSong = nextSong is not null ? $"\nNext Song: {nextSong}" : string.Empty;
|
||||||
|
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.WithAuthor("Lunaris", "https://media.tenor.com/GqAwMt01UXgAAAAi/cd.gif")
|
.WithAuthor("Lunaris", "https://media.tenor.com/GqAwMt01UXgAAAAi/cd.gif")
|
||||||
.WithTitle(title)
|
.WithTitle(title)
|
||||||
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}")
|
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}{getNextSong}")
|
||||||
.WithColor(Color.Magenta)
|
.WithColor(Color.Magenta)
|
||||||
.WithThumbnailUrl(imageUrl)
|
.WithThumbnailUrl(imageUrl)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task NowPlayingEmbed(
|
public async Task NowPlayingEmbed(
|
||||||
LavalinkTrack player,
|
LavalinkTrack track,
|
||||||
SocketSlashCommand context,
|
SocketSlashCommand context,
|
||||||
DiscordSocketClient client)
|
DiscordSocketClient client,
|
||||||
|
ITrackQueue? queue = null)
|
||||||
{
|
{
|
||||||
var artwork = player.ArtworkUri;
|
var duration = TimeSpan.Parse(track.Duration.ToString());
|
||||||
|
|
||||||
|
var artwork = track.ArtworkUri;
|
||||||
|
var nextSong = queue?.Count > 1 ? queue[1].Track?.Title : null;
|
||||||
var embed = SendMusicEmbed(
|
var embed = SendMusicEmbed(
|
||||||
artwork.ToString(),
|
artwork.ToString(),
|
||||||
player.Title,
|
track.Title,
|
||||||
player.Duration.ToString(),
|
duration.ToString(@"hh\:mm\:ss"),
|
||||||
player.Author,
|
track.Author,
|
||||||
context.User.Username);
|
context.User.Username,
|
||||||
|
nextSong);
|
||||||
|
|
||||||
await context.SendMessageAsync(embed, client);
|
await context.SendMessageAsync(embed, client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
Bot/Handler/MusicPlayer/PlayCommand/NormalizationFilter.cs
Normal file
30
Bot/Handler/MusicPlayer/PlayCommand/NormalizationFilter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Lavalink4NET.Filters;
|
||||||
|
using Lavalink4NET.Protocol.Models.Filters;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
|
public class NormalizationFilter : IFilterOptions
|
||||||
|
{
|
||||||
|
private double MaxAmplitude { get; set; }
|
||||||
|
private bool Adaptive { get; set; }
|
||||||
|
|
||||||
|
public NormalizationFilter(double maxAmplitude, bool adaptive)
|
||||||
|
{
|
||||||
|
MaxAmplitude = maxAmplitude;
|
||||||
|
Adaptive = adaptive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDefault => MaxAmplitude == 1.0 && !Adaptive;
|
||||||
|
|
||||||
|
public void Apply(ref PlayerFilterMapModel filterMap)
|
||||||
|
{
|
||||||
|
filterMap.AdditionalFilters ??= new Dictionary<string, JsonElement>();
|
||||||
|
var normalizationFilter = new
|
||||||
|
{
|
||||||
|
maxAmplitude = MaxAmplitude,
|
||||||
|
adaptive = Adaptive
|
||||||
|
};
|
||||||
|
filterMap.AdditionalFilters["normalization"] = JsonSerializer.SerializeToElement(normalizationFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Lavalink4NET;
|
using Lavalink4NET;
|
||||||
using Lavalink4NET.Events.Players;
|
using Lavalink4NET.Events.Players;
|
||||||
|
using Lavalink4NET.Integrations.SponsorBlock;
|
||||||
|
using Lavalink4NET.Integrations.SponsorBlock.Extensions;
|
||||||
using Lavalink4NET.Players.Queued;
|
using Lavalink4NET.Players.Queued;
|
||||||
using Lavalink4NET.Rest.Entities.Tracks;
|
using Lavalink4NET.Rest.Entities.Tracks;
|
||||||
using System.Threading;
|
using Lavalink4NET.Tracks;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
@@ -17,6 +20,9 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IAudioService _audioService;
|
private readonly IAudioService _audioService;
|
||||||
private SocketSlashCommand _context;
|
private SocketSlashCommand _context;
|
||||||
|
private const int MaxTrackDuration = 30;
|
||||||
|
private LavalinkTrack? _previousTrack;
|
||||||
|
private static readonly HashSet<ulong> SubscribedGuilds = new();
|
||||||
|
|
||||||
public PlayHandler(
|
public PlayHandler(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
@@ -26,18 +32,37 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
_client = client;
|
_client = client;
|
||||||
_musicEmbed = musicEmbed;
|
_musicEmbed = musicEmbed;
|
||||||
_audioService = audioService;
|
_audioService = audioService;
|
||||||
_audioService.TrackStarted += OnTrackStarted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnTrackEnded(object sender, TrackEndedEventArgs eventargs)
|
||||||
|
{
|
||||||
|
// Reset the previous track when the track ends.
|
||||||
|
_previousTrack = null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
|
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
|
||||||
{
|
{
|
||||||
var player = sender as QueuedLavalinkPlayer;
|
var player = sender as QueuedLavalinkPlayer;
|
||||||
var track = player?.CurrentTrack;
|
|
||||||
|
|
||||||
if (track != null)
|
if (player?.CurrentTrack is null)
|
||||||
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
|
{
|
||||||
|
return; // No track is currently playing.
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTrack = player.CurrentTrack;
|
||||||
|
|
||||||
|
// Check if the current track is the same as the previous one
|
||||||
|
if (_previousTrack?.Identifier == currentTrack.Identifier)
|
||||||
|
{
|
||||||
|
// The same track is playing, so we don't need to create a new embed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track has changed, update the previous track and send the embed
|
||||||
|
_previousTrack = currentTrack;
|
||||||
|
await _musicEmbed.NowPlayingEmbed(currentTrack, _context, _client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
new Thread(PlayMusic).Start();
|
new Thread(PlayMusic).Start();
|
||||||
@@ -45,55 +70,127 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
|
|
||||||
async void PlayMusic()
|
async void PlayMusic()
|
||||||
{
|
{
|
||||||
var context = command.Message;
|
try
|
||||||
_context = context;
|
|
||||||
|
|
||||||
if ((context.User as SocketGuildUser)?.VoiceChannel == null)
|
|
||||||
{
|
{
|
||||||
await context.SendMessageAsync("You must be in a voice channel to use this command.", _client);
|
RegisterTrackStartedEventListerner(command);
|
||||||
return;
|
|
||||||
}
|
await _audioService.StartAsync(cancellationToken);
|
||||||
|
|
||||||
await _audioService.StartAsync(cancellationToken);
|
var context = command.Message;
|
||||||
|
_context = context;
|
||||||
var searchQuery = context.GetOptionValueByName(Option.Input);
|
|
||||||
|
if ((context.User as SocketGuildUser)?.VoiceChannel == null)
|
||||||
if (string.IsNullOrWhiteSpace(searchQuery))
|
|
||||||
{
|
|
||||||
await context.SendMessageAsync("Please provide search terms.", _client);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
|
||||||
|
|
||||||
if (player is null) return;
|
|
||||||
|
|
||||||
var trackLoadOptions = new TrackLoadOptions { SearchMode = TrackSearchMode.YouTube, };
|
|
||||||
|
|
||||||
var track = await _audioService.Tracks.LoadTrackAsync(searchQuery, trackLoadOptions, cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
if (track is null) await context.SendMessageAsync("😖 No results.", _client);
|
|
||||||
|
|
||||||
if (player.CurrentTrack is null)
|
|
||||||
{
|
|
||||||
await player.PlayAsync(track, cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(track, context, _client);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (track != null)
|
|
||||||
{
|
{
|
||||||
var queueTracks = new[] { new TrackQueueItem(track) };
|
await context.SendMessageAsync("You must be in a voice channel to use this command.", _client);
|
||||||
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
return;
|
||||||
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
|
}
|
||||||
|
|
||||||
|
var searchQuery = context.GetOptionValueByName(Option.Input);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(searchQuery))
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Please provide search terms.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SendMessageAsync("📻 Searching...", _client);
|
||||||
|
|
||||||
|
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ApplyFilters(cancellationToken, player);
|
||||||
|
await ConfigureSponsorBlock(cancellationToken, player);
|
||||||
|
|
||||||
|
var trackLoadOptions = new TrackLoadOptions
|
||||||
|
{
|
||||||
|
SearchMode = TrackSearchMode.YouTube,
|
||||||
|
};
|
||||||
|
|
||||||
|
var trackCollection = await _audioService.Tracks.LoadTracksAsync(searchQuery, trackLoadOptions, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Check if the result is a playlist or just a single track from the search result
|
||||||
|
if (trackCollection.IsPlaylist)
|
||||||
|
{
|
||||||
|
// If it's a playlist, check if it's a free-text input.
|
||||||
|
if (!Uri.IsWellFormedUriString(searchQuery, UriKind.Absolute))
|
||||||
|
{
|
||||||
|
// Free text was used (not a direct URL to a playlist), let's prevent queueing the whole playlist.
|
||||||
|
// Queue only the first track of the search result
|
||||||
|
// var firstTrack = trackCollection.Tracks.FirstOrDefault();
|
||||||
|
if (trackCollection.Track != null)
|
||||||
|
{
|
||||||
|
await player.PlayAsync(trackCollection.Track, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
await _musicEmbed.NowPlayingEmbed(trackCollection.Track, _context, _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("No tracks found.", _client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// It's a playlist and a URL was used, so we can queue all tracks as usual
|
||||||
|
var queueTracks = trackCollection.Tracks
|
||||||
|
.Skip(1)
|
||||||
|
.Select(t => new TrackQueueItem(t))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await player.PlayAsync(trackCollection.Tracks.First(), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
||||||
|
await context.SendMessageAsync($"Queued playlist {trackCollection.Playlist?.Name} with {queueTracks.Count} tracks.", _client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await context.SendMessageAsync($"Couldn't read song information", _client);
|
// It's just a single track or a search result.
|
||||||
|
var track = trackCollection.Tracks.FirstOrDefault();
|
||||||
|
|
||||||
|
if (track != null)
|
||||||
|
{
|
||||||
|
await player.PlayAsync(track, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("No tracks found.", _client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
throw new Exception("Error occured in the Play handler!", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterTrackStartedEventListerner(PlayCommand command)
|
||||||
|
{
|
||||||
|
if (SubscribedGuilds.Contains((ulong)command.Message.GuildId!))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_audioService.TrackStarted += OnTrackStarted;
|
||||||
|
_audioService.TrackEnded += OnTrackEnded;
|
||||||
|
SubscribedGuilds.Add((ulong)command.Message.GuildId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ApplyFilters(CancellationToken cancellationToken, QueuedLavalinkPlayer player)
|
||||||
|
{
|
||||||
|
var normalizationFilter = new NormalizationFilter(0.5, true);
|
||||||
|
player.Filters.SetFilter(normalizationFilter);
|
||||||
|
await player.Filters.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ConfigureSponsorBlock(CancellationToken cancellationToken, QueuedLavalinkPlayer player)
|
||||||
|
{
|
||||||
|
var categories = ImmutableArray.Create(
|
||||||
|
SegmentCategory.Intro,
|
||||||
|
SegmentCategory.Sponsor,
|
||||||
|
SegmentCategory.SelfPromotion,
|
||||||
|
SegmentCategory.Outro,
|
||||||
|
SegmentCategory.Filler);
|
||||||
|
|
||||||
|
await player.UpdateSponsorBlockCategoriesAsync(categories, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,4 +32,4 @@ There is also OnTrackEnd, when it get called an attempt is made to play the next
|
|||||||
| `player` | `LavaPlayer` | An instance of the `LavaPlayer` class, representing a music player connected to a specific voice channel. Used to play, pause, skip, and queue tracks. |
|
| `player` | `LavaPlayer` | An instance of the `LavaPlayer` class, representing a music player connected to a specific voice channel. Used to play, pause, skip, and queue tracks. |
|
||||||
| `guildMessageIds` | `Dictionary<ulong, List<ulong>>` | A dictionary that maps guild IDs to lists of message IDs. Used to keep track of messages sent by the bot in each guild, allowing the bot to delete its old messages when it sends new ones. |
|
| `guildMessageIds` | `Dictionary<ulong, List<ulong>>` | A dictionary that maps guild IDs to lists of message IDs. Used to keep track of messages sent by the bot in each guild, allowing the bot to delete its old messages when it sends new ones. |
|
||||||
| `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. |
|
| `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. |
|
||||||
| `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. |
|
| `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. |
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Lunaris2.Handler.MusicPlayer.ClearQueueCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
@@ -32,6 +33,9 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
|
|||||||
case Command.Skip.Name:
|
case Command.Skip.Name:
|
||||||
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
|
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
|
||||||
break;
|
break;
|
||||||
|
case Command.Clear.Name:
|
||||||
|
await mediator.Send(new ClearQueueCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,26 +8,31 @@
|
|||||||
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
|
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Lavalink4net 4.0.25 seems to break the Message Module-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Discord.Net" Version="3.15.3" />
|
<PackageReference Include="Discord.Net" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Commands" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Commands" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Core" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Interactions" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Interactions" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Rest" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Rest" Version="3.16.0" />
|
||||||
<PackageReference Include="Lavalink4NET" Version="4.0.20" />
|
<PackageReference Include="Lavalink4NET" Version="4.0.25" />
|
||||||
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.20" />
|
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.25" />
|
||||||
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.20" />
|
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.25" />
|
||||||
<PackageReference Include="MediatR" Version="12.4.0" />
|
<PackageReference Include="Lavalink4NET.Integrations.Lavasrc" Version="4.0.25" />
|
||||||
|
<PackageReference Include="Lavalink4NET.Integrations.SponsorBlock" Version="4.0.25" />
|
||||||
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
<PackageReference Include="OllamaSharp" Version="1.1.10" />
|
<PackageReference Include="OllamaSharp" Version="1.1.10" />
|
||||||
<PackageReference Include="Victoria" Version="6.0.23.324" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Include="../docker-compose.yml" pack="true" PackagePath="." />
|
||||||
|
<None Include="../application.yml" pack="true" PackagePath="." />
|
||||||
|
<None Include="../start-services.sh" pack="true" PackagePath="." />
|
||||||
<None Update="appsettings.json">
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Discord.Interactions;
|
|||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.Handler.ChatCommand;
|
using Lunaris2.Handler.ChatCommand;
|
||||||
using Lavalink4NET.Extensions;
|
using Lavalink4NET.Extensions;
|
||||||
|
using Lavalink4NET.Integrations.SponsorBlock.Extensions;
|
||||||
using Lunaris2.Handler.MusicPlayer;
|
using Lunaris2.Handler.MusicPlayer;
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
using Lunaris2.Service;
|
using Lunaris2.Service;
|
||||||
@@ -11,7 +12,6 @@ using Lunaris2.SlashCommand;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Victoria.Node;
|
|
||||||
|
|
||||||
namespace Lunaris2;
|
namespace Lunaris2;
|
||||||
|
|
||||||
@@ -23,7 +23,10 @@ public class Program
|
|||||||
{
|
{
|
||||||
Console.WriteLine(eventArgs.ExceptionObject);
|
Console.WriteLine(eventArgs.ExceptionObject);
|
||||||
};
|
};
|
||||||
CreateHostBuilder(args).Build().Run();
|
var app = CreateHostBuilder(args).Build();
|
||||||
|
|
||||||
|
app.UseSponsorBlock();
|
||||||
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
@@ -53,7 +56,6 @@ public class Program
|
|||||||
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
||||||
options.Label = "Node";
|
options.Label = "Node";
|
||||||
})
|
})
|
||||||
.AddSingleton<LavaNode>()
|
|
||||||
.AddSingleton<MusicEmbed>()
|
.AddSingleton<MusicEmbed>()
|
||||||
.AddSingleton<ChatSettings>()
|
.AddSingleton<ChatSettings>()
|
||||||
.AddSingleton(client)
|
.AddSingleton(client)
|
||||||
|
|||||||
@@ -1,59 +1,71 @@
|
|||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace Lunaris2.Service;
|
namespace Lunaris2.Service
|
||||||
|
|
||||||
public class VoiceChannelMonitorService
|
|
||||||
{
|
{
|
||||||
private readonly DiscordSocketClient _client;
|
public class VoiceChannelMonitorService
|
||||||
private readonly Dictionary<ulong, Timer> _timers = new();
|
|
||||||
|
|
||||||
public VoiceChannelMonitorService(DiscordSocketClient client)
|
|
||||||
{
|
{
|
||||||
_client = client;
|
private readonly DiscordSocketClient _client;
|
||||||
}
|
private readonly Dictionary<ulong, Timer> _timers = new();
|
||||||
|
|
||||||
public void StartMonitoring()
|
public VoiceChannelMonitorService(DiscordSocketClient client)
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
while (true)
|
_client = client;
|
||||||
{
|
}
|
||||||
await CheckVoiceChannels();
|
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CheckVoiceChannels()
|
public void StartMonitoring()
|
||||||
{
|
|
||||||
foreach (var guild in _client.Guilds)
|
|
||||||
{
|
{
|
||||||
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc => vc.ConnectedUsers.Count == 1);
|
Task.Run(async () =>
|
||||||
if (voiceChannel != null)
|
|
||||||
{
|
{
|
||||||
if (!_timers.ContainsKey(voiceChannel.Id))
|
while (true)
|
||||||
{
|
{
|
||||||
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null, TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan);
|
await CheckVoiceChannels();
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1)); // Monitor every minute
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckVoiceChannels()
|
||||||
|
{
|
||||||
|
foreach (var guild in _client.Guilds)
|
||||||
|
{
|
||||||
|
// Find voice channels where only the bot is left
|
||||||
|
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc =>
|
||||||
|
vc.ConnectedUsers.Count == 1 &&
|
||||||
|
vc.Users.Any(u => u.Id == _client.CurrentUser.Id));
|
||||||
|
|
||||||
|
if (voiceChannel != null)
|
||||||
|
{
|
||||||
|
// If timer not set for this channel, start one
|
||||||
|
if (!_timers.ContainsKey(voiceChannel.Id))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Bot is alone in channel {voiceChannel.Name}, starting timer...");
|
||||||
|
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null,
|
||||||
|
TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan); // Set delay before leaving
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Clean up timer if channel is no longer active
|
||||||
|
var timersToDispose = _timers.Where(t => guild.VoiceChannels.All(vc => vc.Id != t.Key)).ToList();
|
||||||
|
foreach (var timer in timersToDispose)
|
||||||
|
{
|
||||||
|
await timer.Value.DisposeAsync();
|
||||||
|
_timers.Remove(timer.Key);
|
||||||
|
Console.WriteLine($"Disposed timer for inactive voice channel ID: {timer.Key}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
private async Task LeaveChannel(SocketVoiceChannel voiceChannel)
|
||||||
|
{
|
||||||
|
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
|
||||||
{
|
{
|
||||||
if (voiceChannel == null || !_timers.ContainsKey(voiceChannel.Id))
|
Console.WriteLine($"Leaving channel {voiceChannel.Name} due to inactivity...");
|
||||||
continue;
|
await voiceChannel.DisconnectAsync();
|
||||||
|
|
||||||
await _timers[voiceChannel.Id].DisposeAsync();
|
await _timers[voiceChannel.Id].DisposeAsync();
|
||||||
_timers.Remove(voiceChannel.Id);
|
_timers.Remove(voiceChannel.Id); // Clean up after leaving
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task LeaveChannel(SocketVoiceChannel voiceChannel)
|
|
||||||
{
|
|
||||||
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
|
|
||||||
{
|
|
||||||
await voiceChannel.DisconnectAsync();
|
|
||||||
await _timers[voiceChannel.Id].DisposeAsync();
|
|
||||||
_timers.Remove(voiceChannel.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public static class Command
|
|||||||
public const string Description = "Disconnect from the voice channel!";
|
public const string Description = "Disconnect from the voice channel!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Clear
|
||||||
|
{
|
||||||
|
public const string Name = "clear";
|
||||||
|
public const string Description = "Clear the music queue!";
|
||||||
|
}
|
||||||
|
|
||||||
public static class Skip
|
public static class Skip
|
||||||
{
|
{
|
||||||
public const string Name = "skip";
|
public const string Name = "skip";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public static class SlashCommandRegistration
|
|||||||
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
|
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
|
||||||
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
|
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
|
||||||
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
||||||
|
RegisterCommand(client, Command.Clear.Name, Command.Clear.Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterCommand(
|
private static void RegisterCommand(
|
||||||
|
|||||||
2
Bot/generate-trusted-session.sh
Normal file
2
Bot/generate-trusted-session.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
docker run quay.io/invidious/youtube-trusted-session-generator
|
||||||
|
read -p "Copy the codes and press enter to close the terminal."
|
||||||
18
README.md
18
README.md
@@ -23,6 +23,24 @@ Lunaris supports AI chat using a large language model, this is done by hosting t
|
|||||||
|
|
||||||
The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system.
|
The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system.
|
||||||
|
|
||||||
|
*NOTE: you need to download the model from the Ollama ui, the model name which is preselected in the code is called ``gemma``.*
|
||||||
|
|
||||||
|
## PM2 Setup
|
||||||
|
- Install PM2 and configure it following their setup guide
|
||||||
|
#### Lavalink
|
||||||
|
* Download Lavalink 4.X.X (.jar)
|
||||||
|
* Install Java 17
|
||||||
|
|
||||||
|
If using Linux run following command to start Lavalink with PM2:
|
||||||
|
``pm2 start "sudo java -Xmx1G -jar Lavalink.jar" --name Lavalink4.0.7``
|
||||||
|
|
||||||
|
For me I have Lavalink.jar downloaded in ``/opt`` folder from Linux root. By running Lavalink using PM2, you can monitor it and manage it from a page in your browser instead of having to access the server terminal.
|
||||||
|
#### Lunaris
|
||||||
|
* Install dotnet
|
||||||
|
|
||||||
|
Register the Lunaris bot with PM2:
|
||||||
|
``pm2 start "dotnet Lunaris2.dll"``
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- `/play <song>`: Plays the specified song in the voice channel you're currently in.
|
- `/play <song>`: Plays the specified song in the voice channel you're currently in.
|
||||||
|
|||||||
@@ -2,7 +2,45 @@ server: # REST and WS server
|
|||||||
port: 2333
|
port: 2333
|
||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
plugins:
|
plugins:
|
||||||
|
lavasrc:
|
||||||
|
providers: # Custom providers for track loading. This is the default
|
||||||
|
# - "dzisrc:%ISRC%" # Deezer ISRC provider
|
||||||
|
# - "dzsearch:%QUERY%" # Deezer search provider
|
||||||
|
- "ytsearch:\"%ISRC%\"" # Will be ignored if track does not have an ISRC. See https://en.wikipedia.org/wiki/International_Standard_Recording_Code
|
||||||
|
- "ytsearch:%QUERY%" # Will be used if track has no ISRC or no track could be found for the ISRC
|
||||||
|
# you can add multiple other fallback sources here
|
||||||
|
sources:
|
||||||
|
spotify: true # Enable Spotify source
|
||||||
|
applemusic: false # Enable Apple Music source
|
||||||
|
deezer: false # Enable Deezer source
|
||||||
|
yandexmusic: false # Enable Yandex Music source
|
||||||
|
flowerytts: false # Enable Flowery TTS source
|
||||||
|
youtube: false # Enable YouTube search source (https://github.com/topi314/LavaSearch)
|
||||||
|
vkmusic: false # Enable Vk Music source
|
||||||
|
spotify:
|
||||||
|
clientId: "ID"
|
||||||
|
clientSecret: "SECRET"
|
||||||
|
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
|
||||||
|
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||||
|
playlistLoadLimit: 6 # The number of pages at 100 tracks each
|
||||||
|
albumLoadLimit: 6 # The number of pages at 50 tracks each
|
||||||
|
resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow)
|
||||||
|
localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"`
|
||||||
youtube:
|
youtube:
|
||||||
|
oauth:
|
||||||
|
# setting "enabled: true" is the bare minimum to get OAuth working.
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# if you have a refresh token, you may set it below (make sure to uncomment the line to apply it).
|
||||||
|
# setting a valid refresh token will skip the OAuth flow entirely. See above note on how to retrieve
|
||||||
|
# your refreshToken.
|
||||||
|
# Set this if you don't want the OAuth flow to be triggered, if you intend to supply a refresh token later.
|
||||||
|
# Initialization is skipped automatically if a valid refresh token is supplied. Leave this commented if you're
|
||||||
|
# completing the OAuth flow for the first time/do not have a refresh token.
|
||||||
|
# skipInitialization: true
|
||||||
|
# pot: // run generate-trusted-session.sh
|
||||||
|
# token: ""
|
||||||
|
# visitorData: ""
|
||||||
enabled: true # Whether this source can be used.
|
enabled: true # Whether this source can be used.
|
||||||
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
|
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
|
||||||
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
||||||
@@ -19,11 +57,17 @@ plugins:
|
|||||||
# another_key: another_value
|
# another_key: another_value
|
||||||
lavalink:
|
lavalink:
|
||||||
plugins:
|
plugins:
|
||||||
# - dependency: "group:artifact:version"
|
- dependency: com.github.devoxin:lavadspx-plugin:0.0.5 # replace {VERSION} with the latest version from the "Releases" tab.
|
||||||
# repository: "repository"
|
repository: https://jitpack.io
|
||||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.5.2"
|
- dependency: "dev.lavalink.youtube:youtube-plugin:1.8.3"
|
||||||
snapshot: false # Set to true if you want to use a snapshot version.
|
snapshot: false # Set to true if you want to use a snapshot version.
|
||||||
|
- dependency: "com.github.topi314.lavasearch:lavasearch-plugin:1.0.0"
|
||||||
|
repository: "https://maven.lavalink.dev/releases" # this is optional for lavalink v4.0.0-beta.5 or greater
|
||||||
|
snapshot: false # set to true if you want to use snapshot builds (see below)
|
||||||
|
- dependency: "com.github.topi314.sponsorblock:sponsorblock-plugin:3.0.1"
|
||||||
|
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.2.0"
|
||||||
|
repository: "https://maven.lavalink.dev/releases" # this is optional for lavalink v4.0.0-beta.5 or greater
|
||||||
|
snapshot: false # set to true if you want to use snapshot builds (see below)
|
||||||
server:
|
server:
|
||||||
password: "youshallnotpass"
|
password: "youshallnotpass"
|
||||||
sources:
|
sources:
|
||||||
@@ -56,20 +100,6 @@ lavalink:
|
|||||||
youtubeSearchEnabled: true
|
youtubeSearchEnabled: true
|
||||||
soundcloudSearchEnabled: true
|
soundcloudSearchEnabled: true
|
||||||
gc-warnings: true
|
gc-warnings: true
|
||||||
#ratelimit:
|
|
||||||
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
|
|
||||||
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
|
|
||||||
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
|
|
||||||
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
|
|
||||||
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
|
|
||||||
#youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without.
|
|
||||||
#email: "" # Email of Google account
|
|
||||||
#password: "" # Password of Google account
|
|
||||||
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
|
|
||||||
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
|
|
||||||
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
|
|
||||||
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
|
|
||||||
#proxyPassword: "" # Password for basic authentication
|
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
prometheus:
|
prometheus:
|
||||||
@@ -79,9 +109,6 @@ metrics:
|
|||||||
sentry:
|
sentry:
|
||||||
dsn: ""
|
dsn: ""
|
||||||
environment: ""
|
environment: ""
|
||||||
# tags:
|
|
||||||
# some_key: some_value
|
|
||||||
# another_key: another_value
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
@@ -99,8 +126,7 @@ logging:
|
|||||||
includePayload: true
|
includePayload: true
|
||||||
maxPayloadLength: 10000
|
maxPayloadLength: 10000
|
||||||
|
|
||||||
|
|
||||||
logback:
|
logback:
|
||||||
rollingpolicy:
|
rollingpolicy:
|
||||||
max-file-size: 1GB
|
max-file-size: 1GB
|
||||||
max-history: 30
|
max-history: 30
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
lavalink:
|
lavalink:
|
||||||
# pin the image version to Lavalink v4
|
# pin the image version to Lavalink v4
|
||||||
image: ghcr.io/lavalink-devs/lavalink:4.0.7
|
image: ghcr.io/lavalink-devs/lavalink:4.0.8
|
||||||
container_name: lavalink
|
container_name: lavalink
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,3 +1 @@
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
read -p "Press enter to continue"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user