Merged projects with plugins and modules
This commit is contained in:
13
Plugins/CppWrapper/CppWrapper.csproj
Normal file
13
Plugins/CppWrapper/CppWrapper.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DiscordBotCore\DiscordBotCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
112
Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs
Normal file
112
Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using DiscordBotCore;
|
||||
|
||||
namespace CppWrapper.LibraryManagement
|
||||
{
|
||||
public sealed class ExternLibrary
|
||||
{
|
||||
public string LibraryPath { get; init; }
|
||||
public IntPtr LibraryHandle { get; private set; }
|
||||
|
||||
public ExternLibrary(string libraryPath)
|
||||
{
|
||||
LibraryPath = libraryPath;
|
||||
LibraryHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void InitializeLibrary()
|
||||
{
|
||||
if(LibraryHandle != IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Loading library {LibraryPath}");
|
||||
|
||||
|
||||
if(!NativeLibrary.TryLoad(LibraryPath, out IntPtr hModule))
|
||||
{
|
||||
throw new DllNotFoundException($"Unable to load library {LibraryPath}");
|
||||
}
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Library {LibraryPath} loaded successfully [{hModule}]");
|
||||
|
||||
LibraryHandle = hModule;
|
||||
}
|
||||
|
||||
public void FreeLibrary()
|
||||
{
|
||||
if(LibraryHandle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(LibraryHandle);
|
||||
LibraryHandle = IntPtr.Zero;
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Library {LibraryPath} freed successfully");
|
||||
}
|
||||
|
||||
private IntPtr GetFunctionPointer(string functionName)
|
||||
{
|
||||
if(LibraryHandle == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Library is not loaded");
|
||||
}
|
||||
|
||||
if(!NativeLibrary.TryGetExport(LibraryHandle, functionName, out IntPtr functionPointer))
|
||||
{
|
||||
throw new EntryPointNotFoundException($"Unable to find function {functionName}");
|
||||
}
|
||||
|
||||
return functionPointer;
|
||||
}
|
||||
|
||||
public T GetDelegateForFunctionPointer<T>(string methodName) where T : Delegate
|
||||
{
|
||||
IntPtr functionPointer = GetFunctionPointer(methodName);
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Function pointer for {methodName} obtained successfully [address: {functionPointer}]");
|
||||
|
||||
T result = (T)Marshal.GetDelegateForFunctionPointer(functionPointer, typeof(T));
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Delegate for {methodName} created successfully");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IntPtr GetFunctionPointerForDelegate<T>(T functionDelegate) where T : Delegate
|
||||
{
|
||||
IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(functionDelegate);
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Function pointer for delegate {functionDelegate.Method.Name} obtained successfully [address: {functionPointer}]");
|
||||
|
||||
return functionPointer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the extern setter function to point its function to this C# function instead.
|
||||
/// This function takes the name of the extern setter function and the C# function to be executed.
|
||||
/// <para><b>How it works:</b></para>
|
||||
/// Find the external setter method by its name. It should take one parameter, which is the pointer to the function to be executed.
|
||||
/// Take the delegate function that should be executed and get its function pointer.
|
||||
/// Call the external setter with the new function memory address. This should replace the old C++ function with the new C# function.
|
||||
/// </summary>
|
||||
/// <param name="setterExternFunctionName">The setter function name</param>
|
||||
/// <param name="executableFunction">The function that the C++ setter will make its internal function to point to</param>
|
||||
/// <typeparam name="ExecuteDelegate">A delegate that reflects the executable function structure</typeparam>
|
||||
/// <typeparam name="SetDelegate">The Setter delegate </typeparam>
|
||||
/// <returns>A response if it exists as an object</returns>
|
||||
public object? SetExternFunctionSetterPointerToCustomDelegate<SetDelegate, ExecuteDelegate>(string setterExternFunctionName, ExecuteDelegate executableFunction) where ExecuteDelegate : Delegate where SetDelegate : Delegate
|
||||
{
|
||||
SetDelegate setterDelegate = GetDelegateForFunctionPointer<SetDelegate>(setterExternFunctionName);
|
||||
IntPtr executableFunctionPtr = GetFunctionPointerForDelegate(executableFunction);
|
||||
|
||||
var result = setterDelegate.DynamicInvoke(executableFunctionPtr);
|
||||
|
||||
Application.CurrentApplication.Logger.Log($"Function {setterExternFunctionName} bound to local action successfully");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Plugins/CppWrapper/Objects/ApplicationStruct.cs
Normal file
34
Plugins/CppWrapper/Objects/ApplicationStruct.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CppWrapper.Objects
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct ApplicationStruct
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 19)]
|
||||
public string ServerId;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2)]
|
||||
public string Prefix;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 71)]
|
||||
public string Token;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct ComplexObject
|
||||
{
|
||||
public int Integer;
|
||||
public double DoubleValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string strValue;
|
||||
|
||||
public ComplexObject(int integer, double doubleValue, string strValue)
|
||||
{
|
||||
Integer = integer;
|
||||
DoubleValue = doubleValue;
|
||||
this.strValue = strValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Plugins/CppWrapper/Objects/ObjectConvertor.cs
Normal file
17
Plugins/CppWrapper/Objects/ObjectConvertor.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using DiscordBotCore;
|
||||
|
||||
namespace CppWrapper.Objects
|
||||
{
|
||||
public static class ObjectConvertor
|
||||
{
|
||||
public static ApplicationStruct ToApplicationStruct(this Application application)
|
||||
{
|
||||
return new ApplicationStruct
|
||||
{
|
||||
Token = application.ApplicationEnvironmentVariables["token"],
|
||||
Prefix = application.ApplicationEnvironmentVariables["prefix"],
|
||||
ServerId = application.ServerID
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Plugins/DiscordBotUI/Delegates.cs
Normal file
22
Plugins/DiscordBotUI/Delegates.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using CppWrapper.Objects;
|
||||
|
||||
namespace DiscordBotUI
|
||||
{
|
||||
public abstract class Delegates
|
||||
{
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void ProcessApplicationData(ref ApplicationStruct appData);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void ProcessComplexObject(ref ComplexObject complexObject);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void CsharpFunctionDelegate();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void SetCsharpFunctionPointerDelegate(IntPtr funcPtr);
|
||||
}
|
||||
}
|
||||
13
Plugins/DiscordBotUI/DiscordBotUI.csproj
Normal file
13
Plugins/DiscordBotUI/DiscordBotUI.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DiscordBotCore\DiscordBotCore.csproj" />
|
||||
<ProjectReference Include="..\CppWrapper\CppWrapper.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
52
Plugins/DiscordBotUI/Entry.cs
Normal file
52
Plugins/DiscordBotUI/Entry.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using DiscordBotCore.Interfaces;
|
||||
using DiscordBotCore.Others;
|
||||
using DiscordBotCore.Others.Actions;
|
||||
|
||||
using CppWrapper.Objects;
|
||||
using CppWrapper.LibraryManagement;
|
||||
using DiscordBotCore;
|
||||
using CppWrapper;
|
||||
|
||||
namespace DiscordBotUI;
|
||||
|
||||
public class Entry : ICommandAction
|
||||
{
|
||||
public string ActionName => "cppui";
|
||||
|
||||
public string? Description => "A C++ linker to the C++ UI for the bot";
|
||||
|
||||
public string? Usage => "cppui";
|
||||
|
||||
public IEnumerable<InternalActionOption> ListOfOptions => [];
|
||||
|
||||
public InternalActionRunType RunType => InternalActionRunType.OnStartupAndCall;
|
||||
|
||||
public async Task Execute(string[]? args)
|
||||
{
|
||||
try{
|
||||
|
||||
string appUiComponent = "./Data/Test/libtestlib.dll";
|
||||
|
||||
ExternLibrary externalLibrary = new ExternLibrary(appUiComponent);
|
||||
externalLibrary.InitializeLibrary();
|
||||
|
||||
externalLibrary.SetExternFunctionSetterPointerToCustomDelegate<Delegates.SetCsharpFunctionPointerDelegate, Delegates.CsharpFunctionDelegate>("setCSharpFunctionPointer", () =>
|
||||
{
|
||||
Console.WriteLine("Hello from C#. This code is called from the C# function");
|
||||
});
|
||||
|
||||
Delegates.ProcessComplexObject processObj = externalLibrary.GetDelegateForFunctionPointer<Delegates.ProcessComplexObject>("ProcessComplexObject");
|
||||
|
||||
ComplexObject complexObject = new ComplexObject(10, 10.5, "Hello from C#");
|
||||
processObj(ref complexObject);
|
||||
|
||||
Console.WriteLine($"Integer: {complexObject.Integer}");
|
||||
Console.WriteLine($"Double: {complexObject.DoubleValue}");
|
||||
Console.WriteLine($"String: {complexObject.strValue}");
|
||||
|
||||
externalLibrary.FreeLibrary();
|
||||
} catch (Exception dllException) {
|
||||
Application.CurrentApplication.Logger.LogException(dllException, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Plugins/LevelingSystem/LevelCommand.cs
Normal file
52
Plugins/LevelingSystem/LevelCommand.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Discord;
|
||||
using DiscordBotCore;
|
||||
using DiscordBotCore.Interfaces;
|
||||
using DiscordBotCore.Others;
|
||||
|
||||
namespace LevelingSystem;
|
||||
|
||||
internal class LevelCommand: DBCommand
|
||||
{
|
||||
public string Command => "level";
|
||||
|
||||
public List<string> Aliases => ["lvl", "rank"];
|
||||
|
||||
public string Description => "Display tour current level";
|
||||
|
||||
public string Usage => "level";
|
||||
|
||||
public bool requireAdmin => false;
|
||||
|
||||
public async void ExecuteServer(DbCommandExecutingArguments args)
|
||||
{
|
||||
if(Variables.Database is null)
|
||||
{
|
||||
Application.CurrentApplication.Logger.Log("Database is not initialized", this, LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
object[]? user = await Variables.Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE UserID=@userId",
|
||||
new KeyValuePair<string, object>("userId", args.Context.Message.Author.Id));
|
||||
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await args.Context.Channel.SendMessageAsync("You are now unranked !");
|
||||
return;
|
||||
}
|
||||
|
||||
var level = (int)user[1];
|
||||
var exp = (int)user[2];
|
||||
|
||||
var builder = new EmbedBuilder();
|
||||
var r = new Random();
|
||||
builder.WithColor(r.Next(256), r.Next(256), r.Next(256));
|
||||
builder.AddField("Current Level", level, true)
|
||||
.AddField("Current EXP", exp, true)
|
||||
.AddField("Required Exp", (level * 8 + 24).ToString(), true);
|
||||
builder.WithTimestamp(DateTimeOffset.Now);
|
||||
builder.WithAuthor(args.Context.Message.Author);
|
||||
await args.Context.Channel.SendMessageAsync(embed: builder.Build());
|
||||
}
|
||||
}
|
||||
83
Plugins/LevelingSystem/LevelEvent.cs
Normal file
83
Plugins/LevelingSystem/LevelEvent.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Discord.WebSocket;
|
||||
|
||||
using DiscordBotCore;
|
||||
using DiscordBotCore.Database;
|
||||
using DiscordBotCore.Interfaces;
|
||||
using static LevelingSystem.Variables;
|
||||
|
||||
namespace LevelingSystem;
|
||||
|
||||
internal class LevelEvent : DBEvent
|
||||
{
|
||||
public string Name => "Leveling System Event Handler";
|
||||
public string Description => "The Leveling System Event Handler";
|
||||
|
||||
public bool RequireOtherThread => false;
|
||||
|
||||
public async void Start(DiscordSocketClient client)
|
||||
{
|
||||
|
||||
Directory.CreateDirectory(DataFolder);
|
||||
await Task.Delay(200);
|
||||
Database = new SqlDatabase(DataFolder + "Users.db");
|
||||
await Database.Open();
|
||||
|
||||
|
||||
if (!File.Exists(DataFolder + "Settings.txt"))
|
||||
{
|
||||
GlobalSettings = new Settings
|
||||
{
|
||||
SecondsToWaitBetweenMessages = 5,
|
||||
MaxExp = 7,
|
||||
MinExp = 1
|
||||
};
|
||||
await DiscordBotCore.Others.JsonManager.SaveToJsonFile(DataFolder + "Settings.txt", GlobalSettings);
|
||||
}
|
||||
else
|
||||
GlobalSettings = await DiscordBotCore.Others.JsonManager.ConvertFromJson<Settings>(DataFolder + "Settings.txt");
|
||||
|
||||
if (!await Database.TableExistsAsync("Levels"))
|
||||
await Database.CreateTableAsync("Levels", "UserID VARCHAR(128)", "Level INT", "EXP INT");
|
||||
|
||||
if (!await Database.TableExistsAsync("Users"))
|
||||
await Database.CreateTableAsync("Users", "UserID VARCHAR(128)", "UserMention VARCHAR(128)", "Username VARCHAR(128)", "Discriminator VARCHAR(128)");
|
||||
|
||||
|
||||
|
||||
client.MessageReceived += ClientOnMessageReceived;
|
||||
}
|
||||
|
||||
private async Task ClientOnMessageReceived(SocketMessage arg)
|
||||
{
|
||||
if (arg.Author.IsBot || arg.IsTTS || arg.Content.StartsWith(Application.CurrentApplication.ApplicationEnvironmentVariables["prefix"]))
|
||||
return;
|
||||
|
||||
if (WaitingList.ContainsKey(arg.Author.Id) && WaitingList[arg.Author.Id] > DateTime.Now.AddSeconds(-GlobalSettings.SecondsToWaitBetweenMessages))
|
||||
return;
|
||||
|
||||
var userID = arg.Author.Id.ToString();
|
||||
|
||||
object[] userData = await Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE userID='{userID}'");
|
||||
if (userData is null)
|
||||
{
|
||||
await Database.ExecuteAsync($"INSERT INTO Levels (UserID, Level, EXP) VALUES ('{userID}', 1, 0)");
|
||||
await Database.ExecuteAsync($"INSERT INTO Users (UserID, UserMention) VALUES ('{userID}', '{arg.Author.Mention}')");
|
||||
return;
|
||||
}
|
||||
|
||||
var level = (int)userData[1];
|
||||
var exp = (int)userData[2];
|
||||
|
||||
|
||||
var random = new Random().Next(GlobalSettings.MinExp, GlobalSettings.MaxExp);
|
||||
if (exp + random >= level * 8 + 24)
|
||||
{
|
||||
await Database.ExecuteAsync($"UPDATE Levels SET Level={level + 1}, EXP={random - (level * 8 + 24 - exp)} WHERE UserID='{userID}'");
|
||||
await arg.Channel.SendMessageAsync($"{arg.Author.Mention} has leveled up to level {level + 1}!");
|
||||
}
|
||||
else await Database.ExecuteAsync($"UPDATE Levels SET EXP={exp + random} WHERE UserID='{userID}'");
|
||||
|
||||
WaitingList.Add(arg.Author.Id, DateTime.Now);
|
||||
}
|
||||
|
||||
}
|
||||
13
Plugins/LevelingSystem/LevelingSystem.csproj
Normal file
13
Plugins/LevelingSystem/LevelingSystem.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DiscordBotCore\DiscordBotCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
20
Plugins/LevelingSystem/ReloadAction.cs
Normal file
20
Plugins/LevelingSystem/ReloadAction.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using DiscordBotCore.Interfaces;
|
||||
using DiscordBotCore.Others;
|
||||
using DiscordBotCore.Others.Actions;
|
||||
|
||||
namespace LevelingSystem;
|
||||
|
||||
public class ReloadAction: ICommandAction
|
||||
{
|
||||
public string ActionName => "LevelingSystemReload";
|
||||
public string? Description => "Reloads the Leveling System config file";
|
||||
public string? Usage => "LevelingSystemReload";
|
||||
public InternalActionRunType RunType => InternalActionRunType.OnCall;
|
||||
|
||||
public IEnumerable<InternalActionOption> ListOfOptions => [];
|
||||
|
||||
public async Task Execute(string[]? args)
|
||||
{
|
||||
Variables.GlobalSettings = await JsonManager.ConvertFromJson<Settings>(Variables.DataFolder + "Settings.txt");
|
||||
}
|
||||
}
|
||||
19
Plugins/LevelingSystem/Settings.cs
Normal file
19
Plugins/LevelingSystem/Settings.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using DiscordBotCore;
|
||||
using DiscordBotCore.Database;
|
||||
|
||||
namespace LevelingSystem;
|
||||
|
||||
public class Settings
|
||||
{
|
||||
public int SecondsToWaitBetweenMessages { get; set; }
|
||||
public int MinExp { get; set; }
|
||||
public int MaxExp { get; set; }
|
||||
}
|
||||
|
||||
internal static class Variables
|
||||
{
|
||||
internal static readonly string DataFolder = Application.GetResourceFullPath("LevelingSystem/");
|
||||
internal static SqlDatabase? Database;
|
||||
internal static readonly Dictionary<ulong, DateTime> WaitingList = new();
|
||||
internal static Settings GlobalSettings = new();
|
||||
}
|
||||
96
Plugins/MusicPlayer/Commands/AddMelody.cs
Normal file
96
Plugins/MusicPlayer/Commands/AddMelody.cs
Normal 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();
|
||||
|
||||
}
|
||||
}
|
||||
77
Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs
Normal file
77
Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs
Normal 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 !");
|
||||
}
|
||||
}
|
||||
38
Plugins/MusicPlayer/Commands/SearchMelody.cs
Normal file
38
Plugins/MusicPlayer/Commands/SearchMelody.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
27
Plugins/MusicPlayer/Events/OnLoad.cs
Normal file
27
Plugins/MusicPlayer/Events/OnLoad.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
35
Plugins/MusicPlayer/Events/OnVoiceRemoved.cs
Normal file
35
Plugins/MusicPlayer/Events/OnVoiceRemoved.cs
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
64
Plugins/MusicPlayer/MusicDatabase.cs
Normal file
64
Plugins/MusicPlayer/MusicDatabase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
10
Plugins/MusicPlayer/MusicInfo.cs
Normal file
10
Plugins/MusicPlayer/MusicInfo.cs
Normal 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;
|
||||
}
|
||||
53
Plugins/MusicPlayer/MusicInfoExtensions.cs
Normal file
53
Plugins/MusicPlayer/MusicInfoExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
175
Plugins/MusicPlayer/MusicPlayer.cs
Normal file
175
Plugins/MusicPlayer/MusicPlayer.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
25
Plugins/MusicPlayer/MusicPlayer.csproj
Normal file
25
Plugins/MusicPlayer/MusicPlayer.csproj
Normal 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>
|
||||
54
Plugins/MusicPlayer/SlashCommands/Loop.cs
Normal file
54
Plugins/MusicPlayer/SlashCommands/Loop.cs
Normal 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");
|
||||
|
||||
}
|
||||
}
|
||||
78
Plugins/MusicPlayer/SlashCommands/Play.cs
Normal file
78
Plugins/MusicPlayer/SlashCommands/Play.cs
Normal 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
|
||||
}
|
||||
}
|
||||
49
Plugins/MusicPlayer/SlashCommands/Queue.cs
Normal file
49
Plugins/MusicPlayer/SlashCommands/Queue.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
35
Plugins/MusicPlayer/SlashCommands/Skip.cs
Normal file
35
Plugins/MusicPlayer/SlashCommands/Skip.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
11
Plugins/MusicPlayer/Variables.cs
Normal file
11
Plugins/MusicPlayer/Variables.cs
Normal 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;
|
||||
}
|
||||
42
Plugins/MusicPlayer/YoutubeDLP.cs
Normal file
42
Plugins/MusicPlayer/YoutubeDLP.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
Plugins/MusicPlayer/libs/linux/ffmpeg
Normal file
BIN
Plugins/MusicPlayer/libs/linux/ffmpeg
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/linux/libopus.so
Normal file
BIN
Plugins/MusicPlayer/libs/linux/libopus.so
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/linux/libsodium.so
Normal file
BIN
Plugins/MusicPlayer/libs/linux/libsodium.so
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/ffmpeg.exe
Normal file
BIN
Plugins/MusicPlayer/libs/windows/ffmpeg.exe
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/libopus.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/libopus.dll
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/libsodium.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/libsodium.dll
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/opus.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/opus.dll
Normal file
Binary file not shown.
Reference in New Issue
Block a user