Initial commit

This commit is contained in:
Mira 2025-01-27 18:36:22 +01:00
commit 79e7cffb4e
Signed by untrusted user who does not match committer: Xorog
GPG key ID: 983798ED9C3E7C36
49 changed files with 6399 additions and 0 deletions

309
Entities/GuildMusic.cs Normal file
View file

@ -0,0 +1,309 @@
// Project Makoto Example Plugin
// Copyright (C) 2023 Fortunevale
// This code is licensed under MIT license (see 'LICENSE'-file for details)
using System.Linq;
using DisCatSharp.Lavalink.Entities;
using DisCatSharp.Lavalink.Enums;
using Newtonsoft.Json;
using ProjectMakoto.Database;
using ProjectMakoto.Enums;
using Xorog.UniversalExtensions;
namespace ProjectMakoto.Plugins.Music.Entities;
[TableName("guilds")]
public class GuildMusic : PluginDatabaseTable
{
public GuildMusic(BasePlugin plugin, ulong identifierValue) : base(plugin, identifierValue)
{
this.Id = identifierValue;
}
[ColumnName("GuildId"), ColumnType(ColumnTypes.BigInt), Primary]
internal ulong Id { get; init; }
public void Reset()
{
this.SongQueue = [];
this.ChannelId = 0;
this.CurrentVideo = null;
this.CurrentVideoPosition = -1;
this.Repeat = false;
this.Shuffle = false;
this.IsPaused = false;
this.Disposed = false;
}
private DiscordGuild Guild { get; set; }
public List<ulong> collectedSkips = [];
public List<ulong> collectedDisconnectVotes = [];
public List<ulong> collectedClearQueueVotes = [];
[ColumnName("SongQueue"), ColumnType(ColumnTypes.LongText), Default("[]")]
public QueueInfo[] SongQueue
{
get => JsonConvert.DeserializeObject<QueueInfo[]>(this.GetValue<string>(this.Id, "SongQueue")) ?? [];
set => _ = this.SetValue(this.Id, "SongQueue", JsonConvert.SerializeObject(value));
}
[ColumnName("Channel"), ColumnType(ColumnTypes.BigInt), Default("0")]
public ulong ChannelId
{
get => this.GetValue<ulong>(this.Id, "Channel");
set => _ = this.SetValue(this.Id, "Channel", value);
}
[ColumnName("CurrentVideo"), ColumnType(ColumnTypes.Text), Nullable]
public string? CurrentVideo
{
get => this.GetValue<string>(this.Id, "CurrentVideo");
set => _ = this.SetValue(this.Id, "CurrentVideo", value ?? string.Empty);
}
[ColumnName("CurrentPosition"), ColumnType(ColumnTypes.BigInt), Default("-1")]
public long CurrentVideoPosition
{
get => this.GetValue<long>(this.Id, "CurrentPosition");
set => _ = this.SetValue(this.Id, "CurrentPosition", value);
}
[ColumnName("Repeat"), ColumnType(ColumnTypes.TinyInt), Default("0")]
public bool Repeat
{
get => this.GetValue<bool>(this.Id, "Repeat");
set => _ = this.SetValue(this.Id, "Repeat", value);
}
[ColumnName("Shuffle"), ColumnType(ColumnTypes.TinyInt), Default("0")]
public bool Shuffle
{
get => this.GetValue<bool>(this.Id, "Shuffle");
set => _ = this.SetValue(this.Id, "Shuffle", value);
}
[ColumnName("Paused"), ColumnType(ColumnTypes.TinyInt), Default("0")]
public bool IsPaused
{
get => this.GetValue<bool>(this.Id, "Paused");
set => _ = this.SetValue(this.Id, "Paused", value);
}
public sealed class QueueInfo(string VideoTitle, string Url, TimeSpan length, ulong? guild, ulong? user)
{
public string UUID { get; set; } = Guid.NewGuid().ToString();
public string VideoTitle { get; set; } = VideoTitle;
public string Url { get; set; } = Url;
public TimeSpan Length { get; set; } = length;
public ulong GuildId = guild ?? 0;
public ulong UserId = user ?? 0;
}
public bool Disposed { private set; get; } = false;
public bool Initialized { private set; get; } = false;
public void Dispose(Bot _bot, ulong Id, string reason)
{
this.Disposed = true;
MusicPlugin.Plugin!._logger.LogDebug("Disposed Player for {Id}. ({reason})", Id, reason);
MusicPlugin.Plugin.Guilds![Id].Reset();
}
public void QueueHandler(Bot _bot, DiscordClient sender, LavalinkSession session, LavalinkGuildPlayer guildPlayer)
{
_ = Task.Run(async () =>
{
try
{
if (this.Initialized || this.Disposed)
return;
this.Initialized = true;
this.Guild = guildPlayer.Guild;
MusicPlugin.Plugin!._logger.LogDebug("Initializing Player for {Guild}..", this.Guild.Id);
var UserAmount = guildPlayer.Channel.Users.Count;
CancellationTokenSource VoiceUpdateTokenSource = new();
Task VoiceStateUpdated(DiscordClient s, VoiceStateUpdateEventArgs e)
{
if (e.Guild is null || e.Guild?.Id != this.Guild?.Id)
return Task.CompletedTask;
_ = Task.Run(() =>
{
if (e.Channel?.Id == guildPlayer.Channel?.Id || e.Before?.Channel?.Id == guildPlayer.Channel?.Id)
{
VoiceUpdateTokenSource.Cancel();
VoiceUpdateTokenSource = new();
UserAmount = e.Channel is not null ? e.Channel.Users.Count : e.Guild!.Channels.First(x => x.Key == e.Before?.Channel?.Id).Value.Users.Count;
MusicPlugin.Plugin!._logger.LogTrace("UserAmount updated to {UserAmount} for {Guild}", UserAmount, this.Guild!.Id);
if (UserAmount <= 1)
_ = Task.Delay(30000, VoiceUpdateTokenSource.Token).ContinueWith(x =>
{
if (!x.IsCompletedSuccessfully)
return;
if (this.Disposed)
return;
if (UserAmount <= 1)
{
MusicPlugin.Plugin.Guilds![this.Id].Dispose(_bot, e.Guild!.Id, "No users");
MusicPlugin.Plugin.Guilds![this.Id].Reset();
}
});
}
return Task.CompletedTask;
}).Add(_bot);
_ = Task.Run(() =>
{
if (e.User.Id == sender.CurrentUser.Id)
{
if (e.After is null || e.After.Channel is null)
{
_ = guildPlayer.DisconnectAsync();
this.Dispose(_bot, e.Guild!.Id, "Disconnected");
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}).Add(_bot);
return Task.CompletedTask;
}
Task StateUpdated(LavalinkGuildPlayer sender, LavalinkPlayerStateUpdateEventArgs e)
{
this.CurrentVideo = (sender.CurrentTrack?.Info?.Uri ?? new UriBuilder().Uri).ToString();
this.CurrentVideoPosition = (Convert.ToInt64(e.State?.Position.TotalSeconds ?? -1d));
return Task.CompletedTask;
}
MusicPlugin.Plugin!._logger.LogDebug("Initializing VoiceStateUpdated Event for {Guild}..", this.Guild.Id);
sender.VoiceStateUpdated += VoiceStateUpdated;
MusicPlugin.Plugin!._logger.LogDebug("Initializing PlayerUpdated Event for {Guild}..", this.Guild.Id);
guildPlayer.StateUpdated += StateUpdated;
QueueInfo? LastPlayedTrack = null;
while (true)
{
var WaitSeconds = 30;
while ((guildPlayer!.CurrentTrack is not null || MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Length <= 0) && !this.Disposed)
{
if (guildPlayer.CurrentTrack is null && MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Length <= 0)
{
WaitSeconds--;
if (WaitSeconds <= 0)
break;
}
await Task.Delay(1000);
}
if (this.Disposed)
{
sender.VoiceStateUpdated -= VoiceStateUpdated;
guildPlayer.StateUpdated -= StateUpdated;
_ = guildPlayer.DisconnectAsync();
this.Dispose(this.Bot, this.Id, "Graceful Disconnect");
return;
}
if (WaitSeconds <= 0)
this.Dispose(_bot, this.Guild.Id, "Time out, nothing playing");
QueueInfo Track;
var skipSongs = 0;
if (LastPlayedTrack is not null &&
MusicPlugin.Plugin.Guilds![this.Guild.Id].Repeat &&
MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.IsNotNullAndNotEmpty() &&
MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Contains(LastPlayedTrack))
{
skipSongs = Array.IndexOf(MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue, LastPlayedTrack) + 1;
if (skipSongs >= MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Length)
skipSongs = 0;
}
if (this.SongQueue.Length <= 0)
{
this.Dispose(_bot, this.Guild.Id, "Queue empty");
continue;
}
Track = MusicPlugin.Plugin.Guilds![this.Guild.Id].Shuffle
? MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.OrderBy(_ => Guid.NewGuid()).ToList().First()
: MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.ToList().Skip(skipSongs).First();
LastPlayedTrack = Track;
MusicPlugin.Plugin.Guilds![this.Guild.Id].collectedSkips.Clear();
var loadResult = await session.LoadTracksAsync(LavalinkSearchType.Plain, Track.Url);
if (loadResult.LoadType is LavalinkLoadResultType.Error or LavalinkLoadResultType.Empty)
{
MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue = MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Remove(x => x.UUID, Track);
continue;
}
var loadedTrack = loadResult.LoadType switch
{
LavalinkLoadResultType.Track => loadResult.GetResultAs<LavalinkTrack>(),
LavalinkLoadResultType.Playlist => loadResult.GetResultAs<LavalinkPlaylist>().Tracks.First(),
LavalinkLoadResultType.Search => loadResult.GetResultAs<List<LavalinkTrack>>().First(),
_ => throw new InvalidOperationException("Unexpected load result type.")
};
guildPlayer = session.GetGuildPlayer(this.Guild) ?? throw new NullReferenceException();
this.ChannelId = guildPlayer.Channel.Id;
if (guildPlayer is not null)
{
_ = await guildPlayer.PlayAsync(loadedTrack);
}
else
{
this.Dispose(_bot, this.Guild.Id, "guildConnection is null");
continue;
}
if (!MusicPlugin.Plugin.Guilds![this.Guild.Id].Repeat)
MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue = MusicPlugin.Plugin.Guilds![this.Guild.Id].SongQueue.Remove(x => x.UUID, Track);
}
}
catch (Exception ex)
{
MusicPlugin.Plugin!._logger.LogError(ex, "An exception occurred while trying to handle music Channel");
_ = guildPlayer.DisconnectAsync();
this.Dispose(_bot, this.Guild.Id, "Exception");
throw;
}
}).Add(_bot);
}
}

26
Entities/PlaylistEntry.cs Normal file
View file

@ -0,0 +1,26 @@
// Project Makoto
// Copyright (C) 2024 Fortunevale
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
namespace ProjectMakoto.Plugins.Music.Entities;
public sealed class PlaylistEntry
{
private string _Title { get; set; }
[JsonProperty(Required = Required.Always)]
public string Title { get => this._Title; set => this._Title = value.TruncateWithIndication(100); }
private TimeSpan? _Length { get; set; }
public TimeSpan? Length { get => this._Length; set => this._Length = value; }
private string _Url { get; set; }
[JsonProperty(Required = Required.Always)]
public string Url { get => this._Url; set => this._Url = value.TruncateWithIndication(2048); }
public DateTime AddedTime { get; set; } = DateTime.UtcNow;
}

12
Entities/PluginConfig.cs Normal file
View file

@ -0,0 +1,12 @@
// Project Makoto Example Plugin
// Copyright (C) 2023 Fortunevale
// This code is licensed under MIT license (see 'LICENSE'-file for details)
namespace ProjectMakoto.Plugins.Music.Entities;
public class PluginConfig
{
public string Host = "127.0.0.1";
public int Port = 2333;
public string Password = "youshallnotpass";
}

240
Entities/Translations.cs Normal file
View file

@ -0,0 +1,240 @@
// Project Makoto
// Copyright (C) 2023 Fortunevale
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
namespace ProjectMakoto.Plugins.Music.Entities;
#pragma warning disable CS8981
#pragma warning disable CS8618
#pragma warning disable IDE1006
public class Translations : ITranslations
{
public Dictionary<string, int> Progress = new();
public CommandTranslation[] CommandList { get; set; }
#region AutoGenerated
public commands Commands;
public sealed class commands
{
public music Music;
public sealed class music
{
public playlists Playlists;
public sealed class playlists
{
public modify Modify;
public sealed class modify
{
public SingleTranslationKey DeleteNote;
public SingleTranslationKey HexHelp;
public SingleTranslationKey NewPlaylistColorPrompt;
public SingleTranslationKey NewPlaylistColor;
public SingleTranslationKey ThumbnailError;
public SingleTranslationKey ThumbnailSizeError;
public SingleTranslationKey ImportingThumbnail;
public SingleTranslationKey UploadThumbnail;
public SingleTranslationKey AddSong;
public SingleTranslationKey TrackLimit;
public SingleTranslationKey ModifyingPlaylist;
public SingleTranslationKey Track;
public SingleTranslationKey CurrentTrackCount;
public SingleTranslationKey RemoveDuplicates;
public SingleTranslationKey RemoveTracks;
public SingleTranslationKey AddTracks;
public SingleTranslationKey ChangeThumbnail;
public SingleTranslationKey ChangeColor;
public SingleTranslationKey ChangeName;
}
public createPlaylist CreatePlaylist;
public sealed class createPlaylist
{
public SingleTranslationKey Created;
public SingleTranslationKey Creating;
public SingleTranslationKey SupportedAddType;
public SingleTranslationKey SetFirstTracks;
public SingleTranslationKey SetPlaylistName;
public SingleTranslationKey FirstTracks;
public SingleTranslationKey PlaylistName;
public SingleTranslationKey CreatePlaylist;
public SingleTranslationKey ChangeTracks;
public SingleTranslationKey ChangeName;
}
public import Import;
public sealed class import
{
public SingleTranslationKey ImportFailed;
public SingleTranslationKey Importing;
public SingleTranslationKey UploadExport;
public SingleTranslationKey Created;
public SingleTranslationKey Creating;
public SingleTranslationKey NotLoaded;
public SingleTranslationKey PlaylistUrl;
public SingleTranslationKey ImportPlaylist;
public SingleTranslationKey ImportMethod;
public SingleTranslationKey ExportedPlaylist;
public SingleTranslationKey Link;
}
public delete Delete;
public sealed class delete
{
public SingleTranslationKey Deleted;
public SingleTranslationKey Deleting;
}
public export Export;
public sealed class export
{
public SingleTranslationKey Exported;
}
public share Share;
public sealed class share
{
public MultiTranslationKey Shared;
}
public addToQueue AddToQueue;
public sealed class addToQueue
{
public SingleTranslationKey Adding;
}
public manage Manage;
public sealed class manage
{
public SingleTranslationKey PlaylistSelectorDelete;
public SingleTranslationKey PlaylistSelectorModify;
public SingleTranslationKey PlaylistSelectorExport;
public SingleTranslationKey PlaylistSelectorShare;
public SingleTranslationKey PlaylistSelectorQueue;
public SingleTranslationKey DeleteButton;
public SingleTranslationKey ModifyButton;
public SingleTranslationKey CreateNewButton;
public SingleTranslationKey SaveCurrentButton;
public SingleTranslationKey ImportButton;
public SingleTranslationKey ExportButton;
public SingleTranslationKey ShareButton;
public SingleTranslationKey AddToQueueButton;
public SingleTranslationKey NoPlaylists;
}
public loadShare LoadShare;
public sealed class loadShare
{
public SingleTranslationKey Imported;
public SingleTranslationKey Importing;
public SingleTranslationKey ImportButton;
public SingleTranslationKey CreatedBy;
public SingleTranslationKey PlaylistName;
public SingleTranslationKey Found;
public SingleTranslationKey NotFound;
public SingleTranslationKey Loading;
}
public SingleTranslationKey ThumbnailModerationNote;
public SingleTranslationKey NameModerationNote;
public SingleTranslationKey Tracks;
public SingleTranslationKey NoPlaylist;
public SingleTranslationKey PlayListLimit;
public SingleTranslationKey Title;
}
public skip Skip;
public sealed class skip
{
public SingleTranslationKey VoteButton;
public SingleTranslationKey VoteStarted;
public SingleTranslationKey Skipped;
public SingleTranslationKey AlreadyVoted;
}
public shuffle Shuffle;
public sealed class shuffle
{
public SingleTranslationKey Off;
public SingleTranslationKey On;
}
public repeat Repeat;
public sealed class repeat
{
public SingleTranslationKey Off;
public SingleTranslationKey On;
}
public removeQueue RemoveQueue;
public sealed class removeQueue
{
public SingleTranslationKey Removed;
public SingleTranslationKey NoSong;
public SingleTranslationKey OutOfRange;
}
public join Join;
public sealed class join
{
public SingleTranslationKey AlreadyUsed;
public SingleTranslationKey Joined;
}
public queue Queue;
public sealed class queue
{
public SingleTranslationKey Play;
public SingleTranslationKey Repeat;
public SingleTranslationKey Shuffle;
public SingleTranslationKey NoSong;
public SingleTranslationKey CurrentlyPlaying;
public SingleTranslationKey Track;
public SingleTranslationKey QueueCount;
}
public play Play;
public sealed class play
{
public SingleTranslationKey SearchSuccess;
public SingleTranslationKey NoMatches;
public SingleTranslationKey FailedToLoad;
public SingleTranslationKey PlatformSelect;
public SingleTranslationKey LookingForPlatform;
public SingleTranslationKey LookingFor;
public SingleTranslationKey Duration;
public SingleTranslationKey Uploader;
public SingleTranslationKey QueuePosition;
public SingleTranslationKey QueuePositions;
public SingleTranslationKey QueuedSingle;
public SingleTranslationKey QueuedMultiple;
public SingleTranslationKey Preparing;
}
public pause Pause;
public sealed class pause
{
public SingleTranslationKey Resumed;
public SingleTranslationKey Paused;
}
public forceSkip ForceSkip;
public sealed class forceSkip
{
public SingleTranslationKey Skipped;
}
public forceDisconnect ForceDisconnect;
public sealed class forceDisconnect
{
public SingleTranslationKey Disconnected;
}
public forceClearQueue ForceClearQueue;
public sealed class forceClearQueue
{
public SingleTranslationKey Cleared;
}
public disconnect Disconnect;
public sealed class disconnect
{
public SingleTranslationKey VoteButton;
public SingleTranslationKey VoteStarted;
public SingleTranslationKey Disconnected;
public SingleTranslationKey AlreadyVoted;
}
public clearQueue ClearQueue;
public sealed class clearQueue
{
public SingleTranslationKey VoteButton;
public SingleTranslationKey VoteStarted;
public SingleTranslationKey Cleared;
public SingleTranslationKey AlreadyVoted;
}
public SingleTranslationKey DjRole;
public SingleTranslationKey NotSameChannel;
}
}
#endregion AutoGenerated
}

40
Entities/UserMusic.cs Normal file
View file

@ -0,0 +1,40 @@
// Project Makoto Example Plugin
// Copyright (C) 2023 Fortunevale
// This code is licensed under MIT license (see 'LICENSE'-file for details)
using System.Linq;
using DisCatSharp.Lavalink.Entities;
using DisCatSharp.Lavalink.Enums;
using Newtonsoft.Json;
using ProjectMakoto.Database;
using ProjectMakoto.Enums;
using Xorog.UniversalExtensions;
namespace ProjectMakoto.Plugins.Music.Entities;
[TableName("users")]
public class UserMusic : PluginDatabaseTable
{
public UserMusic(BasePlugin plugin, ulong identifierValue) : base(plugin, identifierValue)
{
this.Id = identifierValue;
}
[ColumnName("UserId"), ColumnType(ColumnTypes.BigInt), Primary]
internal ulong Id { get; init; }
[ColumnName("Playlists"), ColumnType(ColumnTypes.LongText), Default("[]")]
public UserPlaylist[] Playlists
{
get => (JsonConvert.DeserializeObject<UserPlaylist[]>(this.GetValue<string>(this.Id, "Playlists")) ?? [])
.Select(x =>
{
x.Bot = this.Bot;
x.Parent = this;
return x;
}).ToArray();
set => this.SetValue(this.Id, "Playlists", JsonConvert.SerializeObject(value));
}
}

77
Entities/UserPlaylist.cs Normal file
View file

@ -0,0 +1,77 @@
// Project Makoto
// Copyright (C) 2024 Fortunevale
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
namespace ProjectMakoto.Plugins.Music.Entities;
public sealed class UserPlaylist
{
[JsonIgnore]
public Bot Bot { get; set; }
[JsonIgnore]
public UserMusic Parent { get; set; }
public string PlaylistId { get; set; } = Guid.NewGuid().ToString();
private string _PlaylistName { get; set; } = "";
[JsonProperty(Required = Required.Always)]
public string PlaylistName
{
get => this._PlaylistName;
set
{
this._PlaylistName = value.TruncateWithIndication(256);
this.Update();
}
}
private string _PlaylistColor { get; set; } = "#FFFFFF";
public string PlaylistColor
{
get => this._PlaylistColor;
set
{
this._PlaylistColor = value.Truncate(7).IsValidHexColor();
this.Update();
}
}
private string _PlaylistThumbnail { get; set; } = "";
public string PlaylistThumbnail
{
get => this._PlaylistThumbnail;
set
{
this._PlaylistThumbnail = value.Truncate(2048);
this.Update();
}
}
private PlaylistEntry[] _List = Array.Empty<PlaylistEntry>();
[JsonProperty(Required = Required.Always)]
public PlaylistEntry[] List
{
get => this._List;
set
{
this._List = value.Take(250).ToArray();
this.Update();
}
}
void Update()
{
if (this.Bot is null || this.Parent is null)
return;
this.Parent.Playlists = this.Parent.Playlists.Update(x => x.PlaylistId, this);
}
}