From c0f7c8cc1db049a5a4bff8868df7eb4006751219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Henrique=20Ivanchechen?= Date: Sat, 11 Feb 2023 10:37:40 -0300 Subject: [PATCH] refactor --- Extensions/RestMessageExtensions.cs | 18 +++ Extensions/SocketMessageExtensions.cs | 25 +++ Program.cs | 24 ++- Services/CommandHandlingService.cs | 7 +- Services/PlayerService.cs | 213 ++++++++------------------ Services/YoutubeService.cs | 140 +++++++++++++++++ 6 files changed, 268 insertions(+), 159 deletions(-) create mode 100644 Extensions/RestMessageExtensions.cs create mode 100644 Extensions/SocketMessageExtensions.cs create mode 100644 Services/YoutubeService.cs diff --git a/Extensions/RestMessageExtensions.cs b/Extensions/RestMessageExtensions.cs new file mode 100644 index 0000000..7232f44 --- /dev/null +++ b/Extensions/RestMessageExtensions.cs @@ -0,0 +1,18 @@ +using Discord; +using Discord.Rest; + +namespace Kasbot.Extensions +{ + public static class RestMessageExtensions + { + public static async Task TryDeleteAsync(this RestMessage message, RequestOptions options = null) + { + try + { + await message.DeleteAsync(options); + } + catch { } + } + + } +} diff --git a/Extensions/SocketMessageExtensions.cs b/Extensions/SocketMessageExtensions.cs new file mode 100644 index 0000000..c3e7cdd --- /dev/null +++ b/Extensions/SocketMessageExtensions.cs @@ -0,0 +1,25 @@ +using Discord; +using Discord.WebSocket; + +namespace Kasbot.Extensions +{ + public static class SocketMessageExtensions + { + private const int MessageDelay = 3_000; // in ms + + public static async Task SendTemporaryMessageAsync(this ISocketMessageChannel channel, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + var message = await channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + await Task.Delay(MessageDelay); + await message.DeleteAsync(); + } + + public static async Task SendTemporaryMessageAsync(this IMessageChannel channel, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + var message = await channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + await Task.Delay(MessageDelay); + await message.DeleteAsync(); + } + + } +} diff --git a/Program.cs b/Program.cs index edd7d88..15c77f7 100644 --- a/Program.cs +++ b/Program.cs @@ -9,8 +9,15 @@ namespace TextCommandFramework { class Program { + private static string TOKEN = Environment.GetEnvironmentVariable("TOKEN"); + static void Main(string[] args) { + if (TOKEN == null) + { + throw new Exception("Discord Bot Token was not found."); + } + new Program().MainAsync().GetAwaiter().GetResult(); } @@ -18,12 +25,6 @@ namespace TextCommandFramework { using (var services = ConfigureServices()) { - var token = Environment.GetEnvironmentVariable("TOKEN"); - - if (token == null) - { - throw new Exception("Discord Bot Token was not found."); - } var client = services.GetRequiredService(); @@ -32,8 +33,8 @@ namespace TextCommandFramework client.Ready += () => Client_Ready(client); services.GetRequiredService().Log += LogAsync; - await client.LoginAsync(TokenType.Bot, token); - await client.StartAsync(); + await Connect(client); + client.Disconnected += async (ex) => await Connect(client); await services.GetRequiredService().InitializeAsync(); @@ -41,6 +42,12 @@ namespace TextCommandFramework } } + public async Task Connect(DiscordSocketClient client) + { + await client.LoginAsync(TokenType.Bot, TOKEN); + await client.StartAsync(); + } + private async Task Client_Ready(DiscordSocketClient client) { var announceLoginGuild = ulong.Parse(Environment.GetEnvironmentVariable("ANNOUNCE_LOGIN_GUILD") ?? "0"); @@ -85,6 +92,7 @@ namespace TextCommandFramework }) .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Services/CommandHandlingService.cs b/Services/CommandHandlingService.cs index 025cbb9..7dc9972 100644 --- a/Services/CommandHandlingService.cs +++ b/Services/CommandHandlingService.cs @@ -1,6 +1,7 @@ using Discord; using Discord.Commands; using Discord.WebSocket; +using Kasbot.Extensions; using Microsoft.Extensions.DependencyInjection; using System; using System.Reflection; @@ -57,11 +58,7 @@ namespace TextCommandFramework.Services if (result.IsSuccess) return; - var message = await context.Channel.SendMessageAsync($"error: {result}"); - - await Task.Delay(5_000); - - await message.DeleteAsync(); + await context.Channel.SendTemporaryMessageAsync($"Error: {result}"); } } } diff --git a/Services/PlayerService.cs b/Services/PlayerService.cs index d0b8b7d..93fbc39 100644 --- a/Services/PlayerService.cs +++ b/Services/PlayerService.cs @@ -1,102 +1,37 @@ using Discord; using Discord.Audio; using Discord.Commands; -using Discord.Rest; -using Discord.WebSocket; +using Kasbot.Extensions; using System.Diagnostics; -using YoutubeExplode; -using YoutubeExplode.Videos; -using YoutubeExplode.Videos.Streams; namespace Kasbot.Services { public class PlayerService { public Dictionary Clients { get; set; } + public YoutubeService YoutubeService { get; set; } - public PlayerService() + public PlayerService(YoutubeService youtubeService) { + this.YoutubeService = youtubeService; + Clients = new Dictionary(); } - private async Task> DownloadPlaylistMetadataFromYoutube(SocketUserMessage message, string search) + private async Task CreateConnection(SocketCommandContext Context) { - var list = new List(); - var youtube = new YoutubeClient(); + IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel; + var conn = new Connection(); + IAudioClient audioClient = await channel.ConnectAsync(); - var playlistInfo = await youtube.Playlists.GetAsync(search); - await youtube.Playlists.GetVideosAsync(search).ForEachAsync(videoId => - { - var media = new Media(); - - media.Name = videoId.Title; - media.Length = videoId.Duration ?? new TimeSpan(0); - media.VideoId = videoId.Id; - media.Message = message; - - list.Add(media); - }); - - await message.Channel.SendMessageAsync($"Queued **{list.Count}** items from *{playlistInfo.Title}* playlist."); - - return list; - } - - private async Task DownloadMetadataFromYoutube(Media media) - { - var youtube = new YoutubeClient(); - - IVideo? videoId; - - if (media.Search.StartsWith("http://") || media.Search.StartsWith("https://")) - videoId = await youtube.Videos.GetAsync(media.Search); - else - videoId = await youtube.Search.GetVideosAsync(media.Search).FirstOrDefaultAsync(); - - if (videoId == null) - { - return media; - } - - media.Name = videoId.Title; - media.Length = videoId.Duration ?? new TimeSpan(0); - media.VideoId = videoId.Id; - - return media; - } - - private async Task DownloadAudioFromYoutube(Media media) - { - if (media.VideoId == null) - { - return null; - } - - var memoryStream = new MemoryStream(); - var youtube = new YoutubeClient(); - - var streamInfoSet = await youtube.Videos.Streams.GetManifestAsync((VideoId) media.VideoId); - var streamInfo = streamInfoSet.GetAudioOnlyStreams().GetWithHighestBitrate(); - var streamVideo = await youtube.Videos.Streams.GetAsync(streamInfo); - streamVideo.Position = 0; + audioClient.Disconnected += (ex) => Stop(Context.Guild.Id); - streamVideo.CopyTo(memoryStream); - memoryStream.Position = 0; + conn.AudioClient = audioClient; - return memoryStream; - } + if (Clients.ContainsKey(Context.Guild.Id)) + Clients.Remove(Context.Guild.Id); - private Connection CreateConnection(IAudioClient audioClient, ulong guildId) - { - var conn = new Connection() - { - AudioClient = audioClient, - }; - - if (Clients.ContainsKey(guildId)) - Clients.Remove(guildId); - - Clients.Add(guildId, conn); + Clients.Add(Context.Guild.Id, conn); return conn; } @@ -116,51 +51,51 @@ namespace Kasbot.Services return; } - IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel; - - var audioClient = await channel.ConnectAsync(); - audioClient.Disconnected += (ex) => AudioClient_ClientDisconnected(Context.Guild.Id); - - conn = CreateConnection(audioClient, Context.Guild.Id); + conn = await CreateConnection(Context); await Enqueue(Context.Guild.Id, conn, media); } private async Task Enqueue(ulong guildId, Connection conn, Media media) { - if (media.Search.StartsWith("https://") && media.Search.Contains("playlist?list=")) + var startPlay = conn.Queue.Count == 0; + + switch (YoutubeService.GetSearchType(media.Search)) { - var startPlay = conn.Queue.Count == 0; - var medias = await DownloadPlaylistMetadataFromYoutube(media.Message, media.Search); + case SearchType.StringSearch: + case SearchType.VideoURL: + media = await YoutubeService.DownloadMetadataFromYoutube(media); - medias.ForEach(m => conn.Queue.Enqueue(m)); + if (media.VideoId == null) + { + await media.Channel.SendTemporaryMessageAsync($"No video found for \"{media.Search}\"."); + return; + } - if (startPlay) - { - await PlayNext(guildId); - } + conn.Queue.Enqueue(media); + if (startPlay) + await PlayNext(guildId); + else + { + var message = $"Queued **{media.Name}** *({media.Length.TotalMinutes:00}:{media.Length.Seconds:00})*"; + media.QueueMessage = await media.Channel.SendMessageAsync(message); + } - return; - } + break; + case SearchType.ChannelURL: + case SearchType.PlaylistURL: + var collection = await YoutubeService.DownloadPlaylistMetadataFromYoutube(media.Message, media.Search); - media = await DownloadMetadataFromYoutube(media); + collection.Medias.ForEach(m => conn.Queue.Enqueue(m)); + + await media.Channel.SendMessageAsync($"Queued **{collection.Medias.Count}** items from *{collection.CollectionName}* playlist."); - if (media.VideoId == null) - { - var message = await media.Message.Channel.SendMessageAsync($"No video found for \"{media.Search}\"."); - await Task.Delay(3_000); - await message.DeleteAsync(); - return; - } + if (startPlay) + await PlayNext(guildId); - conn.Queue.Enqueue(media); - if (conn.Queue.Count == 1) - { - await PlayNext(guildId); - } - else - { - var message = $"Queued **{media.Name}** *({media.Length.TotalMinutes:00}:{media.Length.Seconds:00})*"; - media.QueueMessage = await media.Message.Channel.SendMessageAsync(message); + break; + case SearchType.None: + default: + break; } } @@ -175,12 +110,11 @@ namespace Kasbot.Services if (nextMedia == null) { - Clients[guildId].Queue.Clear(); await Stop(guildId); return; } - var mp3Stream = await DownloadAudioFromYoutube(nextMedia); + var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia); if (mp3Stream == null) { @@ -192,11 +126,11 @@ namespace Kasbot.Services var ffmpeg = CreateStream(); var message = $"⏯ Playing: **{nextMedia.Name}** *({nextMedia.Length.TotalMinutes:00}:{nextMedia.Length.Seconds:00})*"; - nextMedia.PlayMessage = await nextMedia.Message.Channel.SendMessageAsync(message); + nextMedia.PlayMessage = await nextMedia.Channel.SendMessageAsync(message); if (nextMedia.QueueMessage != null) { - await nextMedia.QueueMessage.DeleteAsync(); + await nextMedia.QueueMessage.TryDeleteAsync(); } Task stdin = new Task(() => @@ -239,11 +173,26 @@ namespace Kasbot.Services stdin.Start(); stdout.Start(); + await stdin.ContinueWith(async ac => + { + if (ac.Exception != null) + { + await nextMedia.Channel.SendTemporaryMessageAsync("Error in input stream: " + ac.Exception.ToString()); + } + }); + await stdout.ContinueWith(async ac => + { + if (ac.Exception!= null) + { + await nextMedia.Channel.SendTemporaryMessageAsync("Error while playing: " + ac.Exception.ToString()); + } + }); + Task.WaitAll(stdin, stdout); ffmpeg.Close(); - await nextMedia.PlayMessage.DeleteAsync(); + await nextMedia.PlayMessage.TryDeleteAsync(); if (Clients[guildId].Queue.Count > 0) Clients[guildId].Queue.Dequeue(); @@ -251,11 +200,6 @@ namespace Kasbot.Services await PlayNext(guildId); } - private async Task AudioClient_ClientDisconnected(ulong arg) - { - await Stop(arg); - } - private Process CreateStream() { var process = Process.Start(new ProcessStartInfo @@ -299,7 +243,7 @@ namespace Kasbot.Services foreach (var v in media.Queue.Skip(1)) { - await RemoveMediaMessages(v); + await v.PlayMessage.TryDeleteAsync(); } media.Queue.Clear(); @@ -308,16 +252,6 @@ namespace Kasbot.Services media.CurrentAudioStream.Close(); } - private async Task RemoveMediaMessages(Media media) - { - try - { - if (media.PlayMessage != null) - await media.PlayMessage.DeleteAsync(); - } - catch { } - } - public async Task Leave(ulong guildId) { if (!Clients.ContainsKey(guildId)) @@ -339,17 +273,4 @@ namespace Kasbot.Services public Stream? CurrentAudioStream { get; set; } public Queue Queue { get; set; } = new Queue(); } - - public class Media - { - public string Search { get; set; } - - public string Name { get; set; } - public TimeSpan Length { get; set; } - - public VideoId? VideoId { get; set; } - public SocketUserMessage Message { get; set; } - public RestUserMessage PlayMessage { get; set; } - public RestUserMessage? QueueMessage { get; set; } - } } diff --git a/Services/YoutubeService.cs b/Services/YoutubeService.cs new file mode 100644 index 0000000..e530d94 --- /dev/null +++ b/Services/YoutubeService.cs @@ -0,0 +1,140 @@ +using Discord.WebSocket; +using YoutubeExplode.Videos; +using YoutubeExplode; +using Discord.Rest; +using YoutubeExplode.Videos.Streams; + +namespace Kasbot.Services +{ + public class YoutubeService + { + public YoutubeService() + { + + } + + public async Task DownloadPlaylistMetadataFromYoutube(SocketUserMessage message, string search) + { + var collection = new MediaCollection(); + var youtube = new YoutubeClient(); + + var playlistInfo = await youtube.Playlists.GetAsync(search); + await youtube.Playlists.GetVideosAsync(search).ForEachAsync(videoId => + { + var media = new Media(); + + media.Name = videoId.Title; + media.Length = videoId.Duration ?? new TimeSpan(0); + media.VideoId = videoId.Id; + media.Message = message; + + collection.Medias.Add(media); + }); + + collection.CollectionName = playlistInfo.Title; + + return collection; + } + + public async Task DownloadMetadataFromYoutube(Media media) + { + var youtube = new YoutubeClient(); + + IVideo? videoId; + + if (media.Search.StartsWith("http://") || media.Search.StartsWith("https://")) + videoId = await youtube.Videos.GetAsync(media.Search); + else + videoId = await youtube.Search.GetVideosAsync(media.Search).FirstOrDefaultAsync(); + + if (videoId == null) + { + return media; + } + + media.Name = videoId.Title; + media.Length = videoId.Duration ?? new TimeSpan(0); + media.VideoId = videoId.Id; + + return media; + } + + public async Task DownloadAudioFromYoutube(Media media) + { + if (media.VideoId == null) + { + return null; + } + + var memoryStream = new MemoryStream(); + var youtube = new YoutubeClient(); + + var streamInfoSet = await youtube.Videos.Streams.GetManifestAsync((VideoId)media.VideoId); + var streamInfo = streamInfoSet.GetAudioOnlyStreams().GetWithHighestBitrate(); + var streamVideo = await youtube.Videos.Streams.GetAsync(streamInfo); + streamVideo.Position = 0; + + streamVideo.CopyTo(memoryStream); + memoryStream.Position = 0; + + return memoryStream; + } + + public SearchType GetSearchType(string query) + { + if (string.IsNullOrEmpty(query)) + return SearchType.None; + + if (query.StartsWith("http://") || query.StartsWith("https://")) + { + if (query.Contains("playlist?list=")) + return SearchType.PlaylistURL; + + // need to add 'else if' for ChannelURL + + return SearchType.VideoURL; + } + + return SearchType.StringSearch; + } + } + + public enum SearchType + { + None, + StringSearch, + VideoURL, + PlaylistURL, + ChannelURL + } + + public class Media + { + public string Search { get; set; } + + public string Name { get; set; } + public TimeSpan Length { get; set; } + + public VideoId? VideoId { get; set; } + public RestUserMessage PlayMessage { get; set; } + public RestUserMessage? QueueMessage { get; set; } + + private SocketUserMessage message; + public SocketUserMessage Message + { + get => message; + set + { + message = value; + this.Channel = value.Channel; + } + } + public ISocketMessageChannel Channel { get; private set; } + } + + public class MediaCollection + { + public string CollectionName { get; set; } + public List Medias { get; set; } = new List(); + } +}