This commit is contained in:
José Henrique Ivanchechen 2023-02-11 10:37:40 -03:00
parent 79b1a0e15e
commit c0f7c8cc1d
6 changed files with 268 additions and 159 deletions

View File

@ -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 { }
}
}
}

View File

@ -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();
}
}
}

View File

@ -9,8 +9,15 @@ namespace TextCommandFramework
{ {
class Program class Program
{ {
private static string TOKEN = Environment.GetEnvironmentVariable("TOKEN");
static void Main(string[] args) static void Main(string[] args)
{ {
if (TOKEN == null)
{
throw new Exception("Discord Bot Token was not found.");
}
new Program().MainAsync().GetAwaiter().GetResult(); new Program().MainAsync().GetAwaiter().GetResult();
} }
@ -18,12 +25,6 @@ namespace TextCommandFramework
{ {
using (var services = ConfigureServices()) 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<DiscordSocketClient>(); var client = services.GetRequiredService<DiscordSocketClient>();
@ -32,8 +33,8 @@ namespace TextCommandFramework
client.Ready += () => Client_Ready(client); client.Ready += () => Client_Ready(client);
services.GetRequiredService<CommandService>().Log += LogAsync; services.GetRequiredService<CommandService>().Log += LogAsync;
await client.LoginAsync(TokenType.Bot, token); await Connect(client);
await client.StartAsync(); client.Disconnected += async (ex) => await Connect(client);
await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); await services.GetRequiredService<CommandHandlingService>().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) private async Task Client_Ready(DiscordSocketClient client)
{ {
var announceLoginGuild = ulong.Parse(Environment.GetEnvironmentVariable("ANNOUNCE_LOGIN_GUILD") ?? "0"); var announceLoginGuild = ulong.Parse(Environment.GetEnvironmentVariable("ANNOUNCE_LOGIN_GUILD") ?? "0");
@ -85,6 +92,7 @@ namespace TextCommandFramework
}) })
.AddSingleton<DiscordSocketClient>() .AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>() .AddSingleton<CommandService>()
.AddSingleton<YoutubeService>()
.AddSingleton<PlayerService>() .AddSingleton<PlayerService>()
.AddSingleton<CommandHandlingService>() .AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>() .AddSingleton<HttpClient>()

View File

@ -1,6 +1,7 @@
using Discord; using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Kasbot.Extensions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Reflection; using System.Reflection;
@ -57,11 +58,7 @@ namespace TextCommandFramework.Services
if (result.IsSuccess) if (result.IsSuccess)
return; return;
var message = await context.Channel.SendMessageAsync($"error: {result}"); await context.Channel.SendTemporaryMessageAsync($"Error: {result}");
await Task.Delay(5_000);
await message.DeleteAsync();
} }
} }
} }

View File

@ -1,102 +1,37 @@
using Discord; using Discord;
using Discord.Audio; using Discord.Audio;
using Discord.Commands; using Discord.Commands;
using Discord.Rest; using Kasbot.Extensions;
using Discord.WebSocket;
using System.Diagnostics; using System.Diagnostics;
using YoutubeExplode;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.Streams;
namespace Kasbot.Services namespace Kasbot.Services
{ {
public class PlayerService public class PlayerService
{ {
public Dictionary<ulong, Connection> Clients { get; set; } public Dictionary<ulong, Connection> Clients { get; set; }
public YoutubeService YoutubeService { get; set; }
public PlayerService() public PlayerService(YoutubeService youtubeService)
{ {
this.YoutubeService = youtubeService;
Clients = new Dictionary<ulong, Connection>(); Clients = new Dictionary<ulong, Connection>();
} }
private async Task<List<Media>> DownloadPlaylistMetadataFromYoutube(SocketUserMessage message, string search) private async Task<Connection> CreateConnection(SocketCommandContext Context)
{ {
var list = new List<Media>(); IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
var youtube = new YoutubeClient(); var conn = new Connection();
IAudioClient audioClient = await channel.ConnectAsync();
var playlistInfo = await youtube.Playlists.GetAsync(search); audioClient.Disconnected += (ex) => Stop(Context.Guild.Id);
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<Media> 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<MemoryStream?> 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); conn.AudioClient = audioClient;
memoryStream.Position = 0;
return memoryStream; if (Clients.ContainsKey(Context.Guild.Id))
} Clients.Remove(Context.Guild.Id);
private Connection CreateConnection(IAudioClient audioClient, ulong guildId) Clients.Add(Context.Guild.Id, conn);
{
var conn = new Connection()
{
AudioClient = audioClient,
};
if (Clients.ContainsKey(guildId))
Clients.Remove(guildId);
Clients.Add(guildId, conn);
return conn; return conn;
} }
@ -116,51 +51,51 @@ namespace Kasbot.Services
return; return;
} }
IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel; conn = await CreateConnection(Context);
var audioClient = await channel.ConnectAsync();
audioClient.Disconnected += (ex) => AudioClient_ClientDisconnected(Context.Guild.Id);
conn = CreateConnection(audioClient, Context.Guild.Id);
await Enqueue(Context.Guild.Id, conn, media); await Enqueue(Context.Guild.Id, conn, media);
} }
private async Task Enqueue(ulong guildId, Connection conn, Media 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; case SearchType.StringSearch:
var medias = await DownloadPlaylistMetadataFromYoutube(media.Message, media.Search); 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) conn.Queue.Enqueue(media);
{ if (startPlay)
await PlayNext(guildId); 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) if (startPlay)
{ await PlayNext(guildId);
var message = await media.Message.Channel.SendMessageAsync($"No video found for \"{media.Search}\".");
await Task.Delay(3_000);
await message.DeleteAsync();
return;
}
conn.Queue.Enqueue(media); break;
if (conn.Queue.Count == 1) case SearchType.None:
{ default:
await PlayNext(guildId); break;
}
else
{
var message = $"Queued **{media.Name}** *({media.Length.TotalMinutes:00}:{media.Length.Seconds:00})*";
media.QueueMessage = await media.Message.Channel.SendMessageAsync(message);
} }
} }
@ -175,12 +110,11 @@ namespace Kasbot.Services
if (nextMedia == null) if (nextMedia == null)
{ {
Clients[guildId].Queue.Clear();
await Stop(guildId); await Stop(guildId);
return; return;
} }
var mp3Stream = await DownloadAudioFromYoutube(nextMedia); var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia);
if (mp3Stream == null) if (mp3Stream == null)
{ {
@ -192,11 +126,11 @@ namespace Kasbot.Services
var ffmpeg = CreateStream(); var ffmpeg = CreateStream();
var message = $"⏯ Playing: **{nextMedia.Name}** *({nextMedia.Length.TotalMinutes:00}:{nextMedia.Length.Seconds:00})*"; 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) if (nextMedia.QueueMessage != null)
{ {
await nextMedia.QueueMessage.DeleteAsync(); await nextMedia.QueueMessage.TryDeleteAsync();
} }
Task stdin = new Task(() => Task stdin = new Task(() =>
@ -239,11 +173,26 @@ namespace Kasbot.Services
stdin.Start(); stdin.Start();
stdout.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); Task.WaitAll(stdin, stdout);
ffmpeg.Close(); ffmpeg.Close();
await nextMedia.PlayMessage.DeleteAsync(); await nextMedia.PlayMessage.TryDeleteAsync();
if (Clients[guildId].Queue.Count > 0) if (Clients[guildId].Queue.Count > 0)
Clients[guildId].Queue.Dequeue(); Clients[guildId].Queue.Dequeue();
@ -251,11 +200,6 @@ namespace Kasbot.Services
await PlayNext(guildId); await PlayNext(guildId);
} }
private async Task AudioClient_ClientDisconnected(ulong arg)
{
await Stop(arg);
}
private Process CreateStream() private Process CreateStream()
{ {
var process = Process.Start(new ProcessStartInfo var process = Process.Start(new ProcessStartInfo
@ -299,7 +243,7 @@ namespace Kasbot.Services
foreach (var v in media.Queue.Skip(1)) foreach (var v in media.Queue.Skip(1))
{ {
await RemoveMediaMessages(v); await v.PlayMessage.TryDeleteAsync();
} }
media.Queue.Clear(); media.Queue.Clear();
@ -308,16 +252,6 @@ namespace Kasbot.Services
media.CurrentAudioStream.Close(); 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) public async Task Leave(ulong guildId)
{ {
if (!Clients.ContainsKey(guildId)) if (!Clients.ContainsKey(guildId))
@ -339,17 +273,4 @@ namespace Kasbot.Services
public Stream? CurrentAudioStream { get; set; } public Stream? CurrentAudioStream { get; set; }
public Queue<Media> Queue { get; set; } = new Queue<Media>(); public Queue<Media> Queue { get; set; } = new Queue<Media>();
} }
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; }
}
} }

140
Services/YoutubeService.cs Normal file
View File

@ -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<MediaCollection> 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<Media> 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<MemoryStream?> 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<Media> Medias { get; set; } = new List<Media>();
}
}