changing folder structure
This commit is contained in:
64
Kasbot.APP/Services/CommandHandlingService.cs
Normal file
64
Kasbot.APP/Services/CommandHandlingService.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using Kasbot.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Kasbot.Services
|
||||
{
|
||||
public class CommandHandlingService
|
||||
{
|
||||
private readonly CommandService _commands;
|
||||
private readonly DiscordShardedClient _discord;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private readonly string CommandPrefix = "!";
|
||||
|
||||
public CommandHandlingService(IServiceProvider services)
|
||||
{
|
||||
_commands = services.GetRequiredService<CommandService>();
|
||||
_discord = services.GetRequiredService<DiscordShardedClient>();
|
||||
_services = services;
|
||||
|
||||
_commands.CommandExecuted += CommandExecutedAsync;
|
||||
_discord.MessageReceived += MessageReceivedAsync;
|
||||
|
||||
CommandPrefix = Environment.GetEnvironmentVariable("COMMAND_PREFIX") ?? "!";
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
|
||||
}
|
||||
|
||||
public async Task MessageReceivedAsync(SocketMessage rawMessage)
|
||||
{
|
||||
if (!(rawMessage is SocketUserMessage message))
|
||||
return;
|
||||
if (message.Source != MessageSource.User)
|
||||
return;
|
||||
|
||||
var argPos = 0;
|
||||
|
||||
//Check if the message sent has the specified prefix
|
||||
if (!message.HasStringPrefix(CommandPrefix, ref argPos)) return;
|
||||
|
||||
var context = new ShardedCommandContext(_discord, message);
|
||||
await _commands.ExecuteAsync(context, argPos, _services);
|
||||
}
|
||||
|
||||
public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
|
||||
{
|
||||
if (!command.IsSpecified)
|
||||
return;
|
||||
|
||||
if (result.IsSuccess)
|
||||
return;
|
||||
|
||||
await context.Channel.SendTemporaryMessageAsync($"Error: {result}");
|
||||
}
|
||||
}
|
||||
}
|
79
Kasbot.APP/Services/Internal/AudioService.cs
Normal file
79
Kasbot.APP/Services/Internal/AudioService.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Discord.Audio;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Kasbot.Services.Internal
|
||||
{
|
||||
public class AudioService
|
||||
{
|
||||
public AudioService() { }
|
||||
|
||||
public void StartAudioTask(Stream inputStream, IAudioClient outputAudioClient, Action<Stream> onStartAudio, Action<Task> onFinish)
|
||||
{
|
||||
var ffmpeg = CreateFFmpeg();
|
||||
|
||||
Task stdin = new Task(() =>
|
||||
{
|
||||
using (var output = inputStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
output.CopyTo(ffmpeg.StandardInput.BaseStream);
|
||||
ffmpeg.StandardInput.Close();
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
output.Flush();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Task stdout = new Task(() =>
|
||||
{
|
||||
using (var output = ffmpeg.StandardOutput.BaseStream)
|
||||
using (var discord = outputAudioClient.CreatePCMStream(AudioApplication.Music))
|
||||
{
|
||||
try
|
||||
{
|
||||
onStartAudio.Invoke(ffmpeg.StandardOutput.BaseStream);
|
||||
output.CopyTo(discord);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
discord.Flush();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stdin.Start();
|
||||
stdout.Start();
|
||||
|
||||
stdin.ContinueWith(onFinish);
|
||||
stdout.ContinueWith(onFinish);
|
||||
|
||||
Task.WaitAll(stdin, stdout);
|
||||
|
||||
ffmpeg.Close();
|
||||
}
|
||||
|
||||
private Process CreateFFmpeg()
|
||||
{
|
||||
var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = $"-hide_banner -loglevel panic -i pipe:0 -ac 2 -f s16le -ar 48000 pipe:1",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true
|
||||
});
|
||||
|
||||
if (process == null || process.HasExited)
|
||||
{
|
||||
throw new Exception("Sorry, ffmpeg killed itself in a tragic accident!");
|
||||
}
|
||||
|
||||
return process;
|
||||
}
|
||||
}
|
||||
}
|
144
Kasbot.APP/Services/Internal/YoutubeService.cs
Normal file
144
Kasbot.APP/Services/Internal/YoutubeService.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Discord.WebSocket;
|
||||
using YoutubeExplode.Videos;
|
||||
using YoutubeExplode;
|
||||
using Discord.Rest;
|
||||
using YoutubeExplode.Videos.Streams;
|
||||
using Kasbot.Models;
|
||||
|
||||
namespace Kasbot.Services.Internal
|
||||
{
|
||||
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
|
||||
{
|
||||
Name = videoId.Title,
|
||||
Length = videoId.Duration ?? new TimeSpan(0),
|
||||
VideoId = videoId.Id,
|
||||
Message = message,
|
||||
Flags = new Flags()
|
||||
};
|
||||
|
||||
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 Flags Flags { 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;
|
||||
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>();
|
||||
}
|
||||
}
|
20
Kasbot.APP/Services/PictureService.cs
Normal file
20
Kasbot.APP/Services/PictureService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Kasbot.Services
|
||||
{
|
||||
public class PictureService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public PictureService(HttpClient http)
|
||||
=> _http = http;
|
||||
|
||||
public async Task<Stream> GetCatPictureAsync()
|
||||
{
|
||||
var resp = await _http.GetAsync("https://cataas.com/cat");
|
||||
return await resp.Content.ReadAsStreamAsync();
|
||||
}
|
||||
}
|
||||
}
|
249
Kasbot.APP/Services/PlayerService.cs
Normal file
249
Kasbot.APP/Services/PlayerService.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using Discord;
|
||||
using Discord.Audio;
|
||||
using Discord.Commands;
|
||||
using Kasbot.Extensions;
|
||||
using Kasbot.Models;
|
||||
using Kasbot.Services.Internal;
|
||||
|
||||
namespace Kasbot.Services
|
||||
{
|
||||
public class PlayerService
|
||||
{
|
||||
public Dictionary<ulong, Connection> Clients { get; set; }
|
||||
public YoutubeService YoutubeService { get; set; }
|
||||
public AudioService AudioService { get; set; }
|
||||
|
||||
public PlayerService(YoutubeService youtubeService, AudioService audioService)
|
||||
{
|
||||
YoutubeService = youtubeService;
|
||||
AudioService = audioService;
|
||||
|
||||
Clients = new Dictionary<ulong, Connection>();
|
||||
}
|
||||
|
||||
private async Task<Connection> CreateConnection(ulong guildId, IVoiceChannel voiceChannel)
|
||||
{
|
||||
var conn = new Connection();
|
||||
IAudioClient audioClient = await voiceChannel.ConnectAsync(selfDeaf: true);
|
||||
|
||||
conn.AudioClient = audioClient;
|
||||
conn.AudioChannel = voiceChannel;
|
||||
|
||||
if (Clients.ContainsKey(guildId))
|
||||
Clients.Remove(guildId);
|
||||
|
||||
Clients.Add(guildId, conn);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
public async Task Play(ShardedCommandContext Context, string arguments, Flags flags)
|
||||
{
|
||||
var media = new Media()
|
||||
{
|
||||
Message = Context.Message,
|
||||
Search = arguments,
|
||||
Flags = flags,
|
||||
Name = "",
|
||||
};
|
||||
var guildId = Context.Guild.Id;
|
||||
var userVoiceChannel = (Context.User as IVoiceState).VoiceChannel;
|
||||
|
||||
if (Clients.TryGetValue(guildId, out var conn))
|
||||
{
|
||||
if (conn.AudioChannel.Id != userVoiceChannel.Id)
|
||||
{
|
||||
await Stop(guildId);
|
||||
conn = await CreateConnection(guildId, userVoiceChannel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
conn = await CreateConnection(guildId, userVoiceChannel);
|
||||
}
|
||||
|
||||
await Enqueue(guildId, conn, media);
|
||||
}
|
||||
|
||||
private async Task Enqueue(ulong guildId, Connection conn, Media media)
|
||||
{
|
||||
var startPlay = conn.Queue.Count == 0;
|
||||
|
||||
media.Search.Trim();
|
||||
|
||||
switch (YoutubeService.GetSearchType(media.Search))
|
||||
{
|
||||
case SearchType.StringSearch:
|
||||
case SearchType.VideoURL:
|
||||
media = await YoutubeService.DownloadMetadataFromYoutube(media);
|
||||
|
||||
if (media.VideoId == null)
|
||||
{
|
||||
await media.Channel.SendTemporaryMessageAsync($"No video found for \"{media.Search}\".");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
break;
|
||||
case SearchType.ChannelURL:
|
||||
case SearchType.PlaylistURL:
|
||||
var collection = await YoutubeService.DownloadPlaylistMetadataFromYoutube(media.Message, media.Search);
|
||||
|
||||
collection.Medias.ForEach(m => conn.Queue.Enqueue(m));
|
||||
|
||||
await media.Channel.SendMessageAsync($"Queued **{collection.Medias.Count}** items from *{collection.CollectionName}* playlist.");
|
||||
|
||||
if (startPlay)
|
||||
await PlayNext(guildId);
|
||||
|
||||
break;
|
||||
case SearchType.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PlayNext(ulong guildId)
|
||||
{
|
||||
if (!Clients.ContainsKey(guildId) || Clients[guildId].Queue.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nextMedia = Clients[guildId].Queue.FirstOrDefault();
|
||||
|
||||
if (nextMedia == null)
|
||||
{
|
||||
await Stop(guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
// since we can't verify if the bot was disconnected by a websocket error, we do this check which should do enough
|
||||
if (Clients[guildId].AudioClient.ConnectionState == ConnectionState.Disconnected)
|
||||
{
|
||||
var voiceChannel = Clients[guildId].AudioChannel;
|
||||
Clients.Remove(guildId);
|
||||
await CreateConnection(guildId, voiceChannel);
|
||||
}
|
||||
|
||||
var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia);
|
||||
|
||||
if (mp3Stream == null)
|
||||
{
|
||||
await Stop(guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
var audioClient = Clients[guildId].AudioClient;
|
||||
|
||||
if (!nextMedia.Flags.Silent)
|
||||
{
|
||||
var message = $"⏯ Playing: **{nextMedia.Name}** *({nextMedia.Length.TotalMinutes:00}:{nextMedia.Length.Seconds:00})*";
|
||||
nextMedia.PlayMessage = await nextMedia.Channel.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
if (nextMedia.QueueMessage != null)
|
||||
{
|
||||
await nextMedia.QueueMessage.TryDeleteAsync();
|
||||
}
|
||||
|
||||
AudioService.StartAudioTask(mp3Stream, audioClient,
|
||||
(outAudioStream) =>
|
||||
{
|
||||
Clients[guildId].CurrentAudioStream = outAudioStream;
|
||||
},
|
||||
async (ac) =>
|
||||
{
|
||||
if (ac.Exception != null)
|
||||
{
|
||||
await nextMedia.Channel.SendTemporaryMessageAsync("Error in stream: " + ac.Exception.ToString());
|
||||
}
|
||||
});
|
||||
|
||||
await nextMedia.PlayMessage.TryDeleteAsync();
|
||||
|
||||
if (Clients[guildId].Queue.Count > 0 &&
|
||||
!Clients[guildId].Queue.First().Flags.Repeat)
|
||||
Clients[guildId].Queue.Dequeue();
|
||||
|
||||
await PlayNext(guildId);
|
||||
}
|
||||
|
||||
public Task Skip(ulong guildId)
|
||||
{
|
||||
if (!Clients.ContainsKey(guildId))
|
||||
throw new Exception("Bot is not connected!");
|
||||
|
||||
var media = Clients[guildId];
|
||||
|
||||
if (media.CurrentAudioStream == null)
|
||||
throw new Exception("There is no audio playing!");
|
||||
|
||||
media.CurrentAudioStream.Close();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Stop(ulong guildId)
|
||||
{
|
||||
if (!Clients.ContainsKey(guildId))
|
||||
throw new Exception("Bot is not connected!");
|
||||
|
||||
var media = Clients[guildId];
|
||||
|
||||
foreach (var v in media.Queue.Skip(0))
|
||||
{
|
||||
await v.PlayMessage.TryDeleteAsync();
|
||||
}
|
||||
|
||||
media.Queue.Clear();
|
||||
|
||||
if (media.CurrentAudioStream != null)
|
||||
media.CurrentAudioStream.Close();
|
||||
}
|
||||
|
||||
public async Task Leave(ulong guildId)
|
||||
{
|
||||
if (!Clients.ContainsKey(guildId))
|
||||
throw new Exception("Bot is not connected!");
|
||||
|
||||
await Stop(guildId);
|
||||
var media = Clients[guildId];
|
||||
|
||||
if (media.AudioClient != null)
|
||||
await media.AudioClient.StopAsync();
|
||||
|
||||
Clients.Remove(guildId);
|
||||
}
|
||||
|
||||
public async Task Repeat(ulong guildId)
|
||||
{
|
||||
if (!Clients.ContainsKey(guildId))
|
||||
throw new Exception("Bot is not connected!");
|
||||
|
||||
if (Clients[guildId].Queue.Count == 0)
|
||||
throw new Exception("The queue is empty!");
|
||||
|
||||
var media = Clients[guildId].Queue.First();
|
||||
media.Flags.Repeat = !media.Flags.Repeat;
|
||||
await media.Channel.SendTemporaryMessageAsync(media.Flags.Repeat ? "Repeat turned on!" : "Repeat turned off!");
|
||||
}
|
||||
|
||||
public async Task Join(ShardedCommandContext Context)
|
||||
{
|
||||
var guildId = Context.Guild.Id;
|
||||
if (Clients.ContainsKey(guildId))
|
||||
return;
|
||||
|
||||
await CreateConnection(guildId, (Context.User as IVoiceState).VoiceChannel);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user