309 lines
12 KiB
C#
309 lines
12 KiB
C#
// 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);
|
|
}
|
|
}
|