kasbot/Services/PlayerService.cs

277 lines
8.4 KiB
C#

using Discord;
using Discord.Audio;
using Discord.Commands;
using Kasbot.Extensions;
using System.Diagnostics;
namespace Kasbot.Services
{
public class PlayerService
{
public Dictionary<ulong, Connection> Clients { get; set; }
public YoutubeService YoutubeService { get; set; }
public PlayerService(YoutubeService youtubeService)
{
this.YoutubeService = youtubeService;
Clients = new Dictionary<ulong, Connection>();
}
private async Task<Connection> CreateConnection(SocketCommandContext Context)
{
IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
var conn = new Connection();
IAudioClient audioClient = await channel.ConnectAsync();
audioClient.Disconnected += (ex) => Stop(Context.Guild.Id);
conn.AudioClient = audioClient;
if (Clients.ContainsKey(Context.Guild.Id))
Clients.Remove(Context.Guild.Id);
Clients.Add(Context.Guild.Id, conn);
return conn;
}
public async Task Play(SocketCommandContext Context, string arguments)
{
var media = new Media()
{
Message = Context.Message,
Search = arguments,
Name = ""
};
if (Clients.TryGetValue(Context.Guild.Id, out var conn))
{
await Enqueue(Context.Guild.Id, conn, media);
return;
}
conn = await CreateConnection(Context);
await Enqueue(Context.Guild.Id, conn, media);
}
private async Task Enqueue(ulong guildId, Connection conn, Media media)
{
var startPlay = conn.Queue.Count == 0;
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;
}
var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia);
if (mp3Stream == null)
{
await Stop(guildId);
return;
}
var audioClient = Clients[guildId].AudioClient;
var ffmpeg = CreateStream();
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();
}
Task stdin = new Task(() =>
{
using (var input = mp3Stream)
{
try
{
input.CopyTo(ffmpeg.StandardInput.BaseStream);
ffmpeg.StandardInput.Close();
}
catch { }
finally
{
input.Flush();
input.Dispose();
}
}
});
Task stdout = new Task(() =>
{
using (var output = ffmpeg.StandardOutput.BaseStream)
using (var discord = audioClient.CreatePCMStream(AudioApplication.Music))
{
try
{
Clients[guildId].CurrentAudioStream = output;
output.CopyTo(discord);
}
catch { }
finally
{
discord.Flush();
output.Close();
}
}
});
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.TryDeleteAsync();
if (Clients[guildId].Queue.Count > 0)
Clients[guildId].Queue.Dequeue();
await PlayNext(guildId);
}
private Process CreateStream()
{
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;
}
public Task Skip(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
return Task.CompletedTask;
var media = Clients[guildId];
if (media.CurrentAudioStream == null)
return Task.CompletedTask;
media.CurrentAudioStream.Close();
return Task.CompletedTask;
}
public async Task Stop(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
return;
var media = Clients[guildId];
foreach (var v in media.Queue.Skip(1))
{
await v.PlayMessage.TryDeleteAsync();
}
media.Queue.Clear();
if (media.CurrentAudioStream != null)
media.CurrentAudioStream.Close();
}
public async Task Leave(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
return;
await Stop(guildId);
var media = Clients[guildId];
if (media.AudioClient != null)
await media.AudioClient.StopAsync();
Clients.Remove(guildId);
}
}
public class Connection
{
public IAudioClient AudioClient { get; set; }
public Stream? CurrentAudioStream { get; set; }
public Queue<Media> Queue { get; set; } = new Queue<Media>();
}
}