new api, new music code

This commit is contained in:
Kwoth 2016-01-19 06:06:20 +01:00
parent 9e870cfc87
commit f0277b1ba8
4 changed files with 207 additions and 189 deletions

View File

@ -26,9 +26,9 @@ namespace NadekoBot.Modules {
public static IAudioClient Voice; public static IAudioClient Voice;
public static Channel VoiceChannel; public static Channel VoiceChannel;
public static bool Pause = false; public static bool Pause = false;
public static List<YouTubeVideo> SongQueue = new List<YouTubeVideo>(); public static List<StreamRequest> SongQueue = new List<StreamRequest>();
public static YouTubeVideo CurrentSong; public static StreamRequest CurrentSong;
public static bool Exit { public static bool Exit {
get { return exit; } get { return exit; }
@ -49,6 +49,16 @@ namespace NadekoBot.Modules {
public override void Install(ModuleManager manager) { public override void Install(ModuleManager manager) {
var client = NadekoBot.client; var client = NadekoBot.client;
Task.Run(async () => {
while (true) {
if (CurrentSong == null || CurrentSong.State == StreamTaskState.Completed) {
await LoadNextSong();
} else
await Task.Delay(200);
}
});
manager.CreateCommands("!m", cgb => { manager.CreateCommands("!m", cgb => {
//queue all more complex commands //queue all more complex commands
commands.ForEach(cmd => cmd.Init(cgb)); commands.ForEach(cmd => cmd.Init(cgb));
@ -57,8 +67,11 @@ namespace NadekoBot.Modules {
.Alias("next") .Alias("next")
.Description("Goes to the next song in the queue.") .Description("Goes to the next song in the queue.")
.Do(e => { .Do(e => {
if (Voice != null && Exit == false) { if (CurrentSong == null) return;
NextSong = true; CurrentSong.Cancel();
CurrentSong = SongQueue.Take(1).FirstOrDefault();
if (CurrentSong != null) {
CurrentSong.Start();
} }
}); });
@ -66,50 +79,35 @@ namespace NadekoBot.Modules {
.Alias("stop") .Alias("stop")
.Description("Completely stops the music and unbinds the bot from the channel.") .Description("Completely stops the music and unbinds the bot from the channel.")
.Do(e => { .Do(e => {
if (Voice != null && Exit == false) { SongQueue.Clear();
Exit = true; if (CurrentSong != null) {
SongQueue = new List<YouTubeVideo>(); CurrentSong.Cancel();
CurrentSong = null;
} }
}); });
cgb.CreateCommand("p") cgb.CreateCommand("p")
.Alias("pause") .Alias("pause")
.Description("Pauses the song") .Description("Pauses the song")
.Do(async e => { .Do(async e => {
if (Voice != null && Exit == false && CurrentSong != null) { /*if (CurrentSong != null) {
Pause = !Pause; CurrentSong.
if (Pause) { }*/
await e.Send("Pausing. Run the command again to resume."); await e.Send("Not yet implemented.");
} else {
await e.Send("Resuming...");
}
}
}); });
cgb.CreateCommand("q") cgb.CreateCommand("q")
.Alias("yq") .Alias("yq")
.Description("Queue a song using a multi/single word name.\n**Usage**: `!m q Dream Of Venice`") .Description("Queue a song using a multi/single word name.\n**Usage**: `!m q Dream Of Venice`")
.Parameter("Query", ParameterType.Unparsed) .Parameter("Query", ParameterType.Unparsed)
.Do(async e => { .Do(e => {
var youtube = YouTube.Default; SongQueue.Add(new StreamRequest(NadekoBot.client, e, e.GetArg("Query")));
var video = youtube.GetAllVideos(Searches.FindYoutubeUrlByKeywords(e.Args[0]))
.Where(v => v.AdaptiveKind == AdaptiveKind.Audio)
.OrderByDescending(v => v.AudioBitrate).FirstOrDefault();
if (video?.Uri != "" && video.Uri != null) {
SongQueue.Add(video);
await e.Send("**Queued** " + video.FullName);
} else {
await e.Send("Failed to load that song.");
}
}); });
cgb.CreateCommand("lq") cgb.CreateCommand("lq")
.Alias("ls").Alias("lp") .Alias("ls").Alias("lp")
.Description("Lists up to 10 currently queued songs.") .Description("Lists up to 10 currently queued songs.")
.Do(async e => { .Do(async e => {
await e.Send(SongQueue.Count + " videos currently queued."); await e.Send(":musical_note: " + SongQueue.Count + " videos currently queued.");
await e.Send(string.Join("\n", SongQueue.Select(v => v.FullName).Take(10))); await e.Send(string.Join("\n", SongQueue.Select(v => v.Title).Take(10)));
}); });
cgb.CreateCommand("sh") cgb.CreateCommand("sh")
@ -121,88 +119,21 @@ namespace NadekoBot.Modules {
} }
SongQueue.Shuffle(); SongQueue.Shuffle();
await e.Send("Songs shuffled!"); await e.Send(":musical_note: Songs shuffled!");
});
cgb.CreateCommand("radio")
.Alias("music")
.Description("Binds to a voice and text channel in order to play music.")
.Parameter("ChannelName", ParameterType.Unparsed)
.Do(async e => {
if (Voice != null) return;
VoiceChannel = e.Server.FindChannels(e.GetArg("ChannelName").Trim(), ChannelType.Voice).FirstOrDefault();
Voice = await client.Audio().Join(VoiceChannel);
Exit = false;
NextSong = false;
Pause = false;
try {
while (true) {
if (Exit) break;
if (SongQueue.Count == 0 || Pause) { Thread.Sleep(100); continue; }
if (!LoadNextSong()) break;
await Task.Run(async () => {
if (Exit) {
Voice = null;
Exit = false;
await e.Send("Exiting...");
return;
}
var streamer = new AudioStreamer(Music.CurrentSong.Uri);
streamer.Start();
while (streamer.BytesSentToTranscoder < 100 * 0x1000 || streamer.NetworkDone)
await Task.Delay(500);
int blockSize = 1920 * client.Audio().Config.Channels;
byte[] buffer = new byte[blockSize];
var msg = await e.Send("Playing " + Music.CurrentSong.FullName + " [00:00]");
int counter = 0;
int byteCount;
while ((byteCount = streamer.PCMOutput.Read(buffer, 0, blockSize)) > 0) {
Voice.Send(buffer, byteCount);
counter += blockSize;
if (NextSong) {
NextSong = false;
break;
}
if (Exit) {
Exit = false;
return;
}
while (Pause) Thread.Sleep(100);
}
});
}
Voice.Wait();
} catch (Exception ex) { Console.WriteLine(ex.ToString()); }
await Voice.Disconnect();
Voice = null;
VoiceChannel = null;
}); });
}); });
} }
private Stream GetAudioFileStream(string file) { private async Task LoadNextSong() {
Process p = Process.Start(new ProcessStartInfo() {
FileName = "ffmpeg",
Arguments = "-i \"" + Uri.EscapeUriString(file) + "\" -f s16le -ar 48000 -af volume=1 -ac 2 pipe:1 ",
UseShellExecute = false,
RedirectStandardOutput = true
});
return p.StandardOutput.BaseStream;
}
private bool LoadNextSong() {
if (SongQueue.Count == 0) { if (SongQueue.Count == 0) {
CurrentSong = null; CurrentSong = null;
return false; await Task.Delay(200);
return;
} }
CurrentSong = SongQueue[0]; CurrentSong = SongQueue[0];
SongQueue.RemoveAt(0); SongQueue.RemoveAt(0);
return true; CurrentSong.Start();
return;
} }
} }
@ -238,7 +169,7 @@ namespace NadekoBot.Modules {
public StreamTaskState State => streamTask?.State ?? StreamTaskState.Queued; public StreamTaskState State => streamTask?.State ?? StreamTaskState.Queued;
public StreamRequest(DiscordClient client, MessageEventArgs e, string text) { public StreamRequest(DiscordClient client, CommandEventArgs e, string text) {
this.client = client; this.client = client;
Server = e.Server; Server = e.Server;
Channel = e.Channel; Channel = e.Channel;
@ -257,60 +188,30 @@ namespace NadekoBot.Modules {
} }
void ResolveLink() { void ResolveLink() {
var url = RequestText; var query = RequestText;
if (url.IndexOf("soundcloud", StringComparison.OrdinalIgnoreCase) != -1) {
var track = Services.SoundcloudService.GetTrackStreamUrl(url, out Title, out StreamUrl);
Length = TimeSpan.FromMilliseconds(track.Duration);
Title = track.Title;
FileName = Uri.EscapeUriString(Title) + ".mp3";
StartBuffering();
linkResolved = true;
} else if (url.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) != -1 || url.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) != -1) {
try { try {
var infos = DownloadUrlResolver var video = YouTube.Default.GetAllVideos(Searches.FindYoutubeUrlByKeywords(query))
.GetDownloadUrls(url.Trim()) .Where(v => v.AdaptiveKind == AdaptiveKind.Audio)
.Where(i => i.AudioType != AudioType.Unknown) .OrderByDescending(v => v.AudioBitrate).FirstOrDefault();
.ToArray();
if (infos.Length == 0) if (video == null)
throw new Exception("Could not load any video elements"); throw new Exception("Could not load any video elements"); // First one
var info = infos StreamUrl = video.Uri;
.GroupBy(x => x.AudioBitrate) // Create groups for audio bitrates Title = video.Title;
.OrderByDescending(x => x.Key) // Group with max bitrate first var fileName = Title.Replace("\\","_").Replace("/","_");
.Take(1) // Only take one group Path.GetInvalidPathChars().ForEach(c => { fileName = fileName.Replace(c, '_'); });
.SelectMany(x => x) // Unpack group container again FileName = fileName;
.OrderBy(x => x.Resolution) // take vid with smallest resolution
.First(); // First one
StreamUrl = info.DownloadUrl;
Title = info.Title;
FileName = Uri.EscapeUriString(Title) + ".mp4";
StartBuffering(); StartBuffering();
linkResolved = true; linkResolved = true;
Channel.Send(":musical_note: **Queued** " + video.FullName);
} catch (Exception) { } catch (Exception) {
// Send a message to the guy that queued that // Send a message to the guy that queued that
Channel.SendMessage(":warning: " + User.Mention + " Cannot load youtube url: `This video is not available in your country` or the url is corrupted somehow..."); Channel.SendMessage(":warning: " + User.Mention + " Cannot load youtube url: `This video is not available in your country` or the url is corrupted somehow...");
Console.WriteLine("Cannot parse youtube url: " + url); Console.WriteLine("Cannot parse youtube url: " + query);
Cancel(); Cancel();
} }
} else {
// Is it a direct link oO ??
var format = validFormats.FirstOrDefault(f => url.EndsWith(f));
if (format == null) {
Console.WriteLine("Direct link: \"" + url + "\" does not end with a valid extension");
return;
}
StreamUrl = url;
Title = url;
StartBuffering();
linkResolved = true;
}
} }
internal void StartBuffering() { internal void StartBuffering() {
@ -319,7 +220,6 @@ namespace NadekoBot.Modules {
var fullPath = Path.Combine(folder, FileName); var fullPath = Path.Combine(folder, FileName);
FileStream fileStream; FileStream fileStream;
FileStream readStream;
try { try {
if (File.Exists(fullPath) && new FileInfo(fullPath).Length > 1024 * 2) { if (File.Exists(fullPath) && new FileInfo(fullPath).Length > 1024 * 2) {
NetworkDone = true; NetworkDone = true;
@ -339,12 +239,8 @@ namespace NadekoBot.Modules {
return; return;
} }
Task.Run(() => { Task.Run(() => {
AutoResetDelay fileLengthCheckDelay = new AutoResetDelay(500);
int byteCounter = 0; int byteCounter = 0;
bool fileLengthDetermined = false;
try { try {
var webClient = new WebClient(); var webClient = new WebClient();
@ -362,7 +258,7 @@ namespace NadekoBot.Modules {
TotalSourceBytes += read; TotalSourceBytes += read;
fileStream.Write(buffer, 0, read); fileStream.Write(buffer, 0, read);
if (TotalSourceBytes > 1024 * 2 && Length.TotalSeconds < 0.1 && fileLengthCheckDelay.IsReady) { if (TotalSourceBytes > 1024 * 2 && Length.TotalSeconds < 0.1) {
Length = GetFileLength(fullPath); Length = GetFileLength(fullPath);
} }
} }
@ -382,9 +278,6 @@ namespace NadekoBot.Modules {
Stopwatch resolveTimer = Stopwatch.StartNew(); Stopwatch resolveTimer = Stopwatch.StartNew();
if (!linkResolved || bufferingStream == null)
Channel.SendMessage($":musical_note: Resolving link...\r\n:warning: `Keep in mind that other people can 'steal' the bot by just starting a stream command in their own server...`\r\n");
while (resolveTimer.ElapsedMilliseconds < 8000) { while (resolveTimer.ElapsedMilliseconds < 8000) {
if (bufferingStream != null) if (bufferingStream != null)
break; break;
@ -425,21 +318,6 @@ namespace NadekoBot.Modules {
return client.Servers.SelectMany(s => s.VoiceChannels).FirstOrDefault(c => c.Users.Any(u => u.Id == user.Id)); return client.Servers.SelectMany(s => s.VoiceChannels).FirstOrDefault(c => c.Users.Any(u => u.Id == user.Id));
} }
public string GetFormattedTitle() {
if (Length.TotalSeconds < double.Epsilon)
Length = GetFileLength(FileName);
if (Title != DefaultTitle)
return $"**{Title.Replace('*', '°')}** *({Length.ToString()})*";
// put into <> when it contains a domain
if (StreamUrl == null)
return "<" + RequestText + ">";
if (StreamUrl.Contains("http:") || StreamUrl.Contains("https:"))
return "<" + StreamUrl.Trim() + ">";
return StreamUrl;
}
public static TimeSpan GetFileLength(string fileName) { public static TimeSpan GetFileLength(string fileName) {
try { try {
var startInfo = new ProcessStartInfo("ffprobe", $"-i \"{fileName}\" -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1"); var startInfo = new ProcessStartInfo("ffprobe", $"-i \"{fileName}\" -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1");
@ -540,7 +418,7 @@ namespace NadekoBot.Modules {
// How much data is in the final output buffer? // How much data is in the final output buffer?
// We dont want to transcode too much in advance // We dont want to transcode too much in advance
if (available > 0 && availableRingSpace < 1) { if (available > 0) {
int read = await sourceStream.ReadAsync(buffer, 0, (int)Math.Min(available, buffer.LongLength), cancellationToken); int read = await sourceStream.ReadAsync(buffer, 0, (int)Math.Min(available, buffer.LongLength), cancellationToken);
if (read > 0) { if (read > 0) {
// Write to transcoder // Write to transcoder
@ -630,4 +508,115 @@ namespace NadekoBot.Modules {
} }
} }
} }
class StreamTask {
readonly DiscordClient client;
readonly StreamRequest streamRequest;
readonly Stream bufferingStream;
CancellationTokenSource tokenSource;
Task audioTask;
public StreamTaskState State { get; private set; }
public StreamTask(DiscordClient client, StreamRequest streamRequest, Stream bufferingStream) {
this.streamRequest = streamRequest;
this.bufferingStream = bufferingStream;
this.client = client;
State = StreamTaskState.Queued;
}
public void StartStreaming() {
if (State != StreamTaskState.Queued)
return;
State = StreamTaskState.Playing;
tokenSource = new CancellationTokenSource();
audioTask = Task.Run(StreamFunc, tokenSource.Token);
}
public void CancelStreaming() {
if (State != StreamTaskState.Queued && State != StreamTaskState.Playing)
return;
tokenSource?.Cancel(false);
audioTask?.Wait();
State = StreamTaskState.Completed;
}
async Task StreamFunc() {
CancellationToken cancellationToken = tokenSource.Token;
IAudioClient voiceClient = null;
TranscodingTask streamer = null;
try {
uint byteCounter = 0;
// Download and read audio from the url
streamer = new TranscodingTask(streamRequest, bufferingStream);
streamer.Start();
// Wait until we have at least a few kb transcoded or network stream done
while (true) {
if (streamRequest.NetworkDone) {
await Task.Delay(600);
break;
}
if (streamer.ReadyBytesLeft > 5 * 1024)
break;
await Task.Delay(200);
}
if (cancellationToken.IsCancellationRequested)
return;
// Start streaming to voice
await streamRequest.Channel.SendMessage($":musical_note: Playing {streamRequest.Title}");
var audioService = client.Audio();
voiceClient = await audioService.Join(streamRequest.VoiceChannel);
int blockSize = 1920 * audioService.Config.Channels;
byte[] voiceBuffer = new byte[blockSize];
var ringBuffer = streamer.PCMOutput;
Stopwatch timeout = Stopwatch.StartNew();
while (true) {
var readCount = ringBuffer.Read(voiceBuffer, 0, voiceBuffer.Length);
if (readCount == 0) {
if (timeout.ElapsedMilliseconds > 1500) {
Console.WriteLine("Audio stream timed out. Disconnecting.");
break;
}
await Task.Delay(200);
continue;
}
if (cancellationToken.IsCancellationRequested)
return;
timeout.Restart();
byteCounter += (uint)voiceBuffer.Length;
voiceClient.Send(voiceBuffer, 0, voiceBuffer.Length);
}
streamer.Cancel();
voiceClient.Wait();
} catch (Exception ex) {
await streamRequest.Channel.SendMessage($":musical_note: {streamRequest.User.Mention} Something went wrong, please report this. :angry: :anger:");
Console.WriteLine("Exception while playing music: " + ex);
} finally {
if (voiceClient != null) {
State = StreamTaskState.Completed;
streamer?.Cancel();
await voiceClient.Disconnect();
await Task.Delay(500);
}
}
}
}
} }

View File

@ -88,17 +88,17 @@ namespace NadekoBot
})); }));
//install modules //install modules
modules.Install(new Administration(), "Administration", FilterType.Unrestricted); modules.Add(new Administration(), "Administration", ModuleFilter.None);
modules.Install(new Conversations(), "Conversations", FilterType.Unrestricted); modules.Add(new Conversations(), "Conversations", ModuleFilter.None);
modules.Install(new Gambling(), "Gambling", FilterType.Unrestricted); modules.Add(new Gambling(), "Gambling", ModuleFilter.None);
modules.Install(new Games(), "Games", FilterType.Unrestricted); modules.Add(new Games(), "Games", ModuleFilter.None);
modules.Install(new Music(), "Music", FilterType.Unrestricted); modules.Add(new Music(), "Music", ModuleFilter.None);
modules.Install(new Searches(), "Searches", FilterType.Unrestricted); modules.Add(new Searches(), "Searches", ModuleFilter.None);
if(trelloLoaded) if(trelloLoaded)
modules.Install(new Trello(), "Trello", FilterType.Unrestricted); modules.Add(new Trello(), "Trello", ModuleFilter.None);
//run the bot //run the bot
client.Run(async () => client.ExecuteAndWait(async () =>
{ {
await client.Connect(c.Username, c.Password); await client.Connect(c.Username, c.Password);
Console.WriteLine("Connected!"); Console.WriteLine("Connected!");

View File

@ -68,6 +68,26 @@
<HintPath>..\packages\VideoLibrary.1.3.1\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\libvideo.dll</HintPath> <HintPath>..\packages\VideoLibrary.1.3.1\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\libvideo.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Manatee.Json, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c267f67a39449c62, processorArchitecture=MSIL">
<HintPath>..\packages\Manatee.Json.3.2.1\lib\net45\Manatee.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Manatee.StateMachine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=15909d91027a225e, processorArchitecture=MSIL">
<HintPath>..\packages\Manatee.StateMachine.1.1.2\lib\net45\Manatee.StateMachine.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Manatee.Trello, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Manatee.Trello.1.8.2\lib\net45\Manatee.Trello.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Manatee.Trello.ManateeJson, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Manatee.Trello.ManateeJson.1.4.0\lib\net45\Manatee.Trello.ManateeJson.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Manatee.Trello.WebApi, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Manatee.Trello.WebApi.1.0.1\lib\net45\Manatee.Trello.WebApi.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> <HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private> <Private>True</Private>
@ -91,6 +111,14 @@
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Net.Http.Formatting, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Web.Http, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@ -122,6 +150,7 @@
<Compile Include="Modules\Games.cs" /> <Compile Include="Modules\Games.cs" />
<Compile Include="Modules\Music.cs" /> <Compile Include="Modules\Music.cs" />
<Compile Include="Modules\Searches.cs" /> <Compile Include="Modules\Searches.cs" />
<Compile Include="Modules\Trello.cs" />
<Compile Include="NadekoBot.cs" /> <Compile Include="NadekoBot.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StatsCollector.cs" /> <Compile Include="StatsCollector.cs" />

View File

@ -26,7 +26,7 @@ namespace NadekoBot
{ {
this._service = service; this._service = service;
_service.RanCommand += StatsCollector_RanCommand; _service.CommandExecuted += StatsCollector_RanCommand;
//NadekoBot.client.MessageReceived += Client_MessageReceived; //NadekoBot.client.MessageReceived += Client_MessageReceived;
StartCollecting(); StartCollecting();