Merged projects with plugins and modules

This commit is contained in:
2024-07-22 01:18:00 +03:00
parent 1fd065f4c2
commit 8ace51c840
59 changed files with 1669 additions and 73 deletions

View File

@@ -0,0 +1,96 @@
using Discord;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Online;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class AddMelody: DBCommand
{
public string Command => "add_melody";
public List<string>? Aliases => new()
{
"madd"
};
public string Description => "Add a custom melody to the database";
public string Usage => "add_melody [title],[description?],[aliases],[byteSize]";
public bool requireAdmin => false;
public async void ExecuteServer(DbCommandExecutingArguments args)
{
var arguments = string.Join(" ", args.Arguments);
string[] split = arguments.Split(',');
if (split.Length < 4)
{
var message = "";
message += "Invalid arguments given. Please use the following format:\n";
message += "add_melody [title],[description?],[aliases],[byteSize]\n";
message += "title: The title of the melody\n";
message += "description: The description of the melody\n";
message += "aliases: The aliases of the melody. Use | to separate them\n";
message += "byteSize: The byte size of the melody. Default is 1024. ( & will use default)\n";
await args.Context.Channel.SendMessageAsync(message);
return;
}
if (args.Context.Message.Attachments.Count == 0)
{
await args.Context.Channel.SendMessageAsync("You must upload a valid .mp3 audio or .mp4 video file !!");
return;
}
var file = args.Context.Message.Attachments.FirstOrDefault();
if (!(file.Filename.EndsWith(".mp3") || file.Filename.EndsWith(".mp4")))
{
await args.Context.Channel.SendMessageAsync("Invalid file format !!");
return;
}
var title = split[0];
var description = split[1];
string[]? aliases = split[2]?.Split('|') ?? null;
var byteSize = split[3];
int bsize;
if (!int.TryParse(byteSize, out bsize))
bsize = 1024;
var msg = await args.Context.Channel.SendMessageAsync("Saving melody ...");
Console.WriteLine("Saving melody");
IProgress<float> downloadProgress = new Progress<float>();
var location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3");
Directory.CreateDirectory(Application.GetResourceFullPath("Music/Melodies"));
await ServerCom.DownloadFileAsync(file.Url, location, downloadProgress);
Console.WriteLine($"Done. Saved at {location}");
await msg.ModifyAsync(a => a.Content = "Done");
var info =
MusicInfoExtensions.CreateMusicInfo(title, location, description ?? "Unknown", aliases.ToList(), bsize);
Variables._MusicDatabase?.Add(title, info);
var builder = new EmbedBuilder();
builder.Title = "A new music was successfully added !";
builder.AddField("Title", info.Title);
builder.AddField("Description", info.Description);
builder.AddField("Aliases", string.Join(" | ", aliases));
await args.Context.Channel.SendMessageAsync(embed: builder.Build());
await Variables._MusicDatabase.SaveToFile();
}
}

View File

@@ -0,0 +1,77 @@
using System.Diagnostics;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class AddMelodyYoutube: DBCommand
{
public string Command => "add_melody_youtube";
public List<string>? Aliases => new()
{
"madd-yt"
};
public string Description => "Add melody to the database from a youtube link";
public string Usage => "add_melody_youtube [URL] <alias1|alias2|...>";
public bool requireAdmin => true;
public async void ExecuteServer(DbCommandExecutingArguments args)
{
if (args.Arguments is null)
{
await args.Context.Channel.SendMessageAsync("Invalid arguments given. Please use the following format:\nadd_melody_youtube [URL]");
return;
}
var URL = args.Arguments[0];
if (!URL.StartsWith("https://www.youtube.com/watch?v=") && !URL.StartsWith("https://youtu.be/"))
{
await args.Context.Channel.SendMessageAsync("Invalid URL given. Please use the following format:\nadd_melody_youtube [URL]");
return;
}
if (args.Arguments.Length <= 1)
{
await args.Channel.SendMessageAsync("Please specify at least one alias for the melody !");
return;
}
var msg = await args.Context.Channel.SendMessageAsync("Saving melody ...");
var title = await YoutubeDLP.DownloadMelody(URL);
if (title == null)
{
await msg.ModifyAsync(x => x.Content = "Failed to download melody !");
return;
}
var joinedAliases = string.Join(" ", args.Arguments.Skip(1));
List<string> aliases = joinedAliases.Split('|').ToList();
if (Variables._MusicDatabase.ContainsMelodyWithNameOrAlias(title))
Variables._MusicDatabase.Remove(title);
Variables._MusicDatabase.Add(title, new MusicInfo()
{
Aliases = aliases,
ByteSize = 1024,
Description = "Melody added from youtube link",
Location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3"),
Title = title
}
);
await Variables._MusicDatabase.SaveToFile();
await msg.ModifyAsync(x => x.Content = "Melody saved !");
}
}

View File

@@ -0,0 +1,38 @@
using Discord;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class SearchMelody: DBCommand
{
public string Command => "search_melody";
public List<string>? Aliases => null;
public string Description => "Search for a melody in the database";
public string Usage => "search_melody [melody name OR one of its aliases]";
public bool requireAdmin => false;
public void ExecuteServer(DbCommandExecutingArguments args)
{
var title = string.Join(" ", args.Arguments);
if (string.IsNullOrWhiteSpace(title))
{
args.Context.Channel.SendMessageAsync("You need to specify a melody name");
return;
}
List<MusicInfo>? info = Variables._MusicDatabase.GetMusicInfoList(title);
if (info == null || info.Count == 0)
{
args.Context.Channel.SendMessageAsync("No melody with that name or alias was found");
return;
}
if (info.Count > 1)
args.Context.Channel.SendMessageAsync(embed: info.ToEmbed(Color.DarkOrange));
else
args.Context.Channel.SendMessageAsync(embed: info[0].ToEmbed(Color.DarkOrange));
}
}

View File

@@ -0,0 +1,27 @@
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.Events;
public class OnLoad: DBEvent
{
private static readonly string _DefaultMusicPath = "Music/";
private static readonly string _DefaultSaveLocation = "Music/Melodies/";
private static readonly string _DefaultMusicDB = "Music/music_db.json";
public string Name => "Music Commands";
public string Description => "The default music commands event loader";
public bool RequireOtherThread => false;
public async void Start(DiscordSocketClient client)
{
var path1 = Application.GetResourceFullPath(_DefaultMusicPath);
var path2 = Application.GetResourceFullPath(_DefaultSaveLocation);
var path3 = Application.GetResourceFullPath(_DefaultMusicDB);
Directory.CreateDirectory(path1);
Directory.CreateDirectory(path2);
Variables._MusicDatabase = new MusicDatabase(path3);
await Variables._MusicDatabase.LoadFromFile();
}
}

View File

@@ -0,0 +1,35 @@
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Events;
public class OnVoiceRemoved: DBEvent
{
public string Name => "Event: OnVoiceRemoved";
public string Description => "Called when bot leaves a voice channel";
public bool RequireOtherThread => false;
public void Start(DiscordSocketClient client)
{
client.UserVoiceStateUpdated += async (user, oldState, newState) =>
{
if (user.Id == client.CurrentUser.Id && newState.VoiceChannel == null)
{
Variables._MusicPlayer?.MusicQueue.Clear();
Variables._MusicPlayer?.Skip();
Variables._MusicPlayer?.Stop();
await Variables.audioClient!.StopAsync();
Variables.audioClient = null;
Variables._MusicPlayer = null;
Application.CurrentApplication.Logger.Log("Bot left voice channel.", this, LogType.Info);
}
};
}
}

View File

@@ -0,0 +1,64 @@
using DiscordBotCore.Others;
namespace MusicPlayer;
public class MusicDatabase: SettingsDictionary<string, MusicInfo>
{
public MusicDatabase(string file): base(file)
{
}
/// <summary>
/// Checks if the database contains a melody with the specified name or alias
/// </summary>
/// <param name="melodyName">The name (alias) of the melody</param>
/// <returns></returns>
public bool ContainsMelodyWithNameOrAlias(string melodyName)
{
return ContainsKey(melodyName) || Values.Any(elem => elem.Aliases.Contains(melodyName, StringComparer.CurrentCultureIgnoreCase));
}
/// <summary>
/// Tries to get the music info of a melody with the specified name or alias. Returns the first match or null if no match was found.
/// </summary>
/// <param name="searchQuery">The music name or one of its aliases.</param>
/// <returns></returns>
public MusicInfo? GetMusicInfo(string searchQuery)
{
return FirstOrDefault(kvp => kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) ||
kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase))
).Value;
}
/// <summary>
/// Get a list of music info that match the search query. Returns null if an error occurred, or empty list if no match was found.
/// </summary>
/// <param name="searchQuery">The search query</param>
/// <returns>null if an error occured, otherwise a list with songs that match the search query. If no song match the list is empty</returns>
public List<MusicInfo>? GetMusicInfoList(string searchQuery)
{
try
{
return this.Where(kvp =>
kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) ||
kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase))
)
.Select(item => item.Value).ToList();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
return null;
}
}
/// <summary>
/// Adds a new entry to the database based on the music info. It uses the title as the key.
/// </summary>
/// <param name="musicInfo">The music to add to</param>
public void AddNewEntryBasedOnMusicInfo(MusicInfo musicInfo)
{
Add(musicInfo.Title, musicInfo);
}
}

View File

@@ -0,0 +1,10 @@
namespace MusicPlayer;
public class MusicInfo
{
public string Title { get; init; }
public string? Description { get; init; }
public string Location { get; init; }
public List<string>? Aliases { get; init; }
public int? ByteSize { get; init; } = 1024;
}

View File

@@ -0,0 +1,53 @@
using Discord;
namespace MusicPlayer;
public static class MusicInfoExtensions
{
public static void AddAlias(this MusicInfo musicInfo, string alias)
{
musicInfo.Aliases.Add(alias);
}
public static void RemoveAlias(this MusicInfo musicInfo, string alias)
{
musicInfo.Aliases.Remove(alias);
}
public static MusicInfo CreateMusicInfo(string title, string fileLocation, string? Description = "Unknown", List<string>? aliases = null, int? byteSize = 1024)
{
return new MusicInfo()
{
Title = title,
Aliases = aliases,
Description = Description,
Location = fileLocation,
ByteSize = byteSize
};
}
public static Embed ToEmbed(this MusicInfo musicInfo, Color? embedColor = null)
{
var builder = new EmbedBuilder();
builder.Color = embedColor ?? Color.Default;
builder.WithTitle(musicInfo.Title);
builder.WithDescription(musicInfo.Description);
if (musicInfo.Aliases != null)
builder.AddField("Aliases", string.Join(", ", musicInfo.Aliases));
else
builder.AddField("Aliases", "None");
builder.AddField("Location", musicInfo.Location);
builder.AddField("ByteSize", musicInfo.ByteSize);
return builder.Build();
}
public static Embed ToEmbed(this List<MusicInfo> musicInfo, Color? embedColor = null)
{
var builder = new EmbedBuilder();
builder.Color = embedColor ?? Color.Default;
builder.WithTitle("Search results");
builder.WithDescription("Found " + musicInfo.Count + " results");
builder.AddField("Results", string.Join("\n", musicInfo.Select(item => item.Title)));
return builder.Build();
}
}

View File

@@ -0,0 +1,175 @@
using System.Diagnostics;
using Discord.Audio;
using DiscordBotCore;
using DiscordBotCore.Others;
namespace MusicPlayer;
public class MusicPlayer
{
private static int defaultByteSize = 1024;
public Queue<MusicInfo> MusicQueue { get; private set; }
public bool isPaused { get; private set; }
public bool isPlaying { get; private set; }
private bool isQueueRunning;
public int ByteSize { get; private set; }
public MusicInfo? CurrentlyPlaying { get; private set; }
public MusicPlayer()
{
MusicQueue = new Queue<MusicInfo>();
}
public async Task PlayQueue()
{
if (isQueueRunning)
{
Application.CurrentApplication.Logger.Log("Another queue is running !", typeof(MusicPlayer), LogType.Warning);
return;
}
if (Variables.audioClient is null)
{
Application.CurrentApplication.Logger.Log("Audio Client is null", typeof(MusicPlayer), LogType.Warning);
return;
}
isQueueRunning = true;
string? ffmpegPath = await Application.CurrentApplication.PluginManager.GetDependencyLocation("FFMPEG");
if(ffmpegPath is null)
{
Application.CurrentApplication.Logger.Log("FFMPEG is missing. Please install it and try again.", typeof(MusicPlayer), LogType.Error);
isQueueRunning = false;
return;
}
ffmpegPath = ffmpegPath.Replace("\\", "/");
ffmpegPath = Path.GetFullPath(ffmpegPath);
Console.WriteLine("FFMPEG Path: " + ffmpegPath);
while (MusicQueue.TryDequeue(out var dequeuedMusic))
{
CurrentlyPlaying = dequeuedMusic;
await using var dsAudioStream = Variables.audioClient.CreatePCMStream(AudioApplication.Mixed);
using var ffmpeg = CreateStream(ffmpegPath, CurrentlyPlaying.Location);
if (ffmpeg is null)
{
Application.CurrentApplication.Logger.Log($"Failed to start ffmpeg process. FFMPEG is missing or the {CurrentlyPlaying.Location} has an invalid format.", typeof(MusicPlayer), LogType.Error);
continue;
}
await using var ffmpegOut = ffmpeg.StandardOutput.BaseStream;
await PlayCurrentTrack(dsAudioStream, ffmpegOut, CurrentlyPlaying.ByteSize ?? defaultByteSize);
}
isQueueRunning = false;
CurrentlyPlaying = null;
}
public void Loop(int numberOfTimes)
{
if (CurrentlyPlaying is null) return;
Queue<MusicInfo> tempQueue = new();
for (var i = 0; i < numberOfTimes; i++)
{
tempQueue.Enqueue(CurrentlyPlaying);
}
foreach (var musicInfo in MusicQueue)
{
tempQueue.Enqueue(musicInfo);
}
MusicQueue = tempQueue;
}
private async Task PlayCurrentTrack(Stream discordVoiceChannelStream, Stream fileStreamFfmpeg, int byteSize)
{
if (isPlaying) return;
ByteSize = byteSize;
isPlaying = true;
isPaused = false;
while (isPlaying)
{
if (isPaused) continue;
var bits = new byte[byteSize];
var read = await fileStreamFfmpeg.ReadAsync(bits, 0, ByteSize);
if (read == 0) break;
try
{
await discordVoiceChannelStream.WriteAsync(bits, 0, read);
}
catch (Exception ex)
{
Application.CurrentApplication.Logger.LogException(ex, this);
break;
}
}
await discordVoiceChannelStream.FlushAsync();
await fileStreamFfmpeg.FlushAsync();
isPlaying = false;
isPaused = false;
}
public void Pause()
{
isPaused = true;
}
public void Unpause()
{
isPaused = false;
}
public bool Enqueue(string musicName)
{
var minfo = Variables._MusicDatabase.GetMusicInfo(musicName);
if (minfo is null) return false;
MusicQueue.Enqueue(minfo);
return true;
}
public void Skip()
{
isPlaying = false;
}
public void SetVolume(float volume)
{
// set volume
}
private static Process? CreateStream(string? fileName, string path)
{
return Process.Start(new ProcessStartInfo
{
FileName = fileName,
Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1",
UseShellExecute = false,
RedirectStandardOutput = true
}
);
}
public void Stop()
{
MusicQueue.Clear();
isPlaying = false;
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\DiscordBotCore\DiscordBotCore.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="libs\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="libs\**"/>
</ItemGroup>
<ItemGroup>
<None Remove="libs\**"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,54 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Loop: DBSlashCommand
{
public string Name => "loop";
public string Description => "Loop the current song for a certain amount of times. If no times are specified, it will loop once";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => new()
{
new()
{
Type = ApplicationCommandOptionType.Integer,
Name = "times",
Description = "How many times to loop the song",
IsRequired = false
}
};
public void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer.CurrentlyPlaying == null)
{
context.RespondAsync("There is nothing playing right now");
return;
}
var times = context.Data.Options.FirstOrDefault()?.Value.ToString() ?? "1";
if (!int.TryParse(times, out var timesToLoop))
{
context.RespondAsync("Invalid number");
return;
}
if (timesToLoop < 1)
{
context.RespondAsync("You need to specify a number greater than 0");
return;
}
Variables._MusicPlayer.Loop(timesToLoop);
context.RespondAsync($"Looping {Variables._MusicPlayer.CurrentlyPlaying.Title} {timesToLoop} times. Check the queue to see the progress");
}
}

View File

@@ -0,0 +1,78 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.SlashCommands;
public class Play: DBSlashCommand
{
public string Name => "play";
public string Description => "Play music command";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => new()
{
new()
{
IsRequired = true,
Description = "The music name to be played",
Name = "music-name",
Type = ApplicationCommandOptionType.String
}
};
public async void ExecuteServer(SocketSlashCommand context)
{
var melodyName = context.Data.Options.First().Value as string;
if (melodyName is null)
{
await context.RespondAsync("Failed to retrieve melody with name " + melodyName);
return;
}
var melody = Variables._MusicDatabase.GetMusicInfo(melodyName);
if (melody is null)
{
await context.RespondAsync("The searched melody does not exists in the database. Sorry :(");
return;
}
var user = context.User as IGuildUser;
if (user is null)
{
await context.RespondAsync("Failed to get user data from channel ! Check error log at " + DateTime.Now.ToLongTimeString());
Application.CurrentApplication.Logger.Log("User is null while trying to convert from context.User to IGuildUser.", typeof(Play), LogType.Error);
return;
}
var voiceChannel = user.VoiceChannel;
if (voiceChannel is null)
{
await context.RespondAsync("Unknown voice channel. Maybe I do not have permission to join it ?");
return;
}
if (Variables.audioClient is null)
{
Variables.audioClient = await voiceChannel.ConnectAsync(true); // self deaf
}
Variables._MusicPlayer ??= new MusicPlayer();
if (!Variables._MusicPlayer.Enqueue(melodyName))
{
await context.RespondAsync("Failed to enqueue your request. Something went wrong !");
return;
}
await context.RespondAsync("Enqueued your request");
await Variables._MusicPlayer.PlayQueue(); //start queue
}
}

View File

@@ -0,0 +1,49 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Queue: DBSlashCommand
{
public string Name => "queue";
public string Description => "Queue a melody to play";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => null;
public async void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer is null)
{
await context.RespondAsync("No music is currently playing.");
return;
}
if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null)
{
await context.RespondAsync("No music is currently playing");
return;
}
var builder = new EmbedBuilder()
{
Title = "Music Queue",
Description = "Here is the current music queue",
Color = Color.Blue
};
if (Variables._MusicPlayer.CurrentlyPlaying != null)
builder.AddField("Current music", Variables._MusicPlayer.CurrentlyPlaying.Title);
var i = 1;
foreach (var melody in Variables._MusicPlayer.MusicQueue)
{
builder.AddField($"#{i}", melody.Title);
i++;
}
await context.RespondAsync(embed: builder.Build());
}
}

View File

@@ -0,0 +1,35 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Skip: DBSlashCommand
{
public string Name => "skip";
public string Description => "Skip the current melody";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => null;
public async void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer is null)
{
await context.RespondAsync("No music is currently playing.");
return;
}
if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null)
{
await context.RespondAsync("No music is currently playing");
return;
}
var melodyTitle = Variables._MusicPlayer.CurrentlyPlaying.Title;
await context.RespondAsync($"Skipping {melodyTitle} ...");
Variables._MusicPlayer.Skip();
await context.ModifyOriginalResponseAsync(x => x.Content = $"Skipped {melodyTitle}");
}
}

View File

@@ -0,0 +1,11 @@
using Discord.Audio;
namespace MusicPlayer;
public class Variables
{
public static MusicDatabase? _MusicDatabase;
public static MusicPlayer? _MusicPlayer;
public static IAudioClient? audioClient;
}

View File

@@ -0,0 +1,42 @@
using System.Diagnostics;
namespace MusicPlayer;
public class YoutubeDLP
{
public static async Task<string?> DownloadMelody(string url)
{
Console.WriteLine("Downloading melody: " + url);
var process = new Process();
process.StartInfo.FileName = await DiscordBotCore.Application.CurrentApplication.PluginManager.GetDependencyLocation("yt-dlp");
process.StartInfo.Arguments = $"-x --force-overwrites -o \"{DiscordBotCore.Application.GetResourceFullPath("/Music/Melodies")}/%(title)s.%(ext)s\" --audio-format mp3 {url}";
process.StartInfo.RedirectStandardOutput = true;
var title = "";
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
if (args.Data.StartsWith("[ExtractAudio] Destination: "))
{
title = args.Data.Replace("[ExtractAudio] Destination: ", "").Replace(".mp3", "");
title = title.Replace("\\", "/");
title = title.Split('/').Last().Replace(".mp3", "").TrimEnd();
Console.WriteLine("Output title: " + title);
return;
}
Console.WriteLine(args.Data);
}
};
process.Start();
Console.WriteLine("Waiting for process to exit ...");
process.BeginOutputReadLine();
await process.WaitForExitAsync();
return title ?? null;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.