diff --git a/NadekoBot/Classes/Music/MusicControls.cs b/NadekoBot/Classes/Music/MusicControls.cs index 4bbd0b01..7c51c5c2 100644 --- a/NadekoBot/Classes/Music/MusicControls.cs +++ b/NadekoBot/Classes/Music/MusicControls.cs @@ -14,7 +14,7 @@ namespace NadekoBot.Classes.Music { public List SongQueue = new List(); public StreamRequest CurrentSong; - public bool IsPaused { get; internal set; } + public bool IsPaused { get; internal set; } = false; public IAudioClient VoiceClient; private readonly object _voiceLock = new object(); @@ -23,12 +23,15 @@ namespace NadekoBot.Classes.Music { Task.Run(async () => { while (true) { try { - if (CurrentSong == null) { - if (SongQueue.Count > 0) - LoadNextSong(); + lock (_voiceLock) { + if (CurrentSong == null) { + if (SongQueue.Count > 0) + LoadNextSong(); - } else if (CurrentSong.State == StreamState.Completed) { - LoadNextSong(); + } else if (CurrentSong.State == StreamState.Completed || NextSong) { + NextSong = false; + LoadNextSong(); + } } } catch (Exception e) { Console.WriteLine("Bug in music task run. " + e); @@ -43,38 +46,50 @@ namespace NadekoBot.Classes.Music { } public void LoadNextSong() { - Console.WriteLine("Loading next song."); lock (_voiceLock) { - if (SongQueue.Count == 0) { - CurrentSong = null; - return; - } + CurrentSong?.Stop(); + CurrentSong = null; + if (SongQueue.Count == 0) return; CurrentSong = SongQueue[0]; SongQueue.RemoveAt(0); } - CurrentSong.Start(); - Console.WriteLine("Starting next song."); + try { + CurrentSong?.Start(); + } catch (Exception ex) { + Console.WriteLine($"Starting failed: {ex}"); + CurrentSong?.Stop(); + CurrentSong = null; + } } - internal void RemoveAllSongs() { + internal void Stop() { lock (_voiceLock) { foreach (var kvp in SongQueue) { if(kvp != null) kvp.Cancel(); } SongQueue.Clear(); + LoadNextSong(); VoiceClient.Disconnect(); VoiceClient = null; } } internal StreamRequest CreateStreamRequest(CommandEventArgs e, string query, Channel voiceChannel) { + if (VoiceChannel == null) + throw new ArgumentNullException("Please join a voicechannel."); + StreamRequest sr = null; lock (_voiceLock) { - if (VoiceClient == null) + if (VoiceClient == null) { VoiceClient = NadekoBot.client.Audio().Join(VoiceChannel).Result; - return new StreamRequest(e, query, VoiceClient); + } + sr = new StreamRequest(e, query, this); + SongQueue.Add(sr); } + return sr; } + + internal bool TogglePause() => IsPaused = !IsPaused; } } diff --git a/NadekoBot/Classes/Music/StreamRequest.cs b/NadekoBot/Classes/Music/StreamRequest.cs index 002cc3ac..35270c33 100644 --- a/NadekoBot/Classes/Music/StreamRequest.cs +++ b/NadekoBot/Classes/Music/StreamRequest.cs @@ -29,50 +29,56 @@ namespace NadekoBot.Classes.Music { public User User { get; } public string Query { get; } - public string Title { get; internal set; } = String.Empty; public IAudioClient VoiceClient { get; private set; } private MusicStreamer musicStreamer = null; - public StreamState State => musicStreamer?.State ?? StreamState.Resolving; + public StreamState State => musicStreamer?.State ?? privateState; + private StreamState privateState = StreamState.Resolving; - public StreamRequest(CommandEventArgs e, string query, IAudioClient voiceClient) { + public bool IsPaused => MusicControls.IsPaused; + + private MusicControls MusicControls; + + public StreamRequest(CommandEventArgs e, string query, MusicControls mc) { if (e == null) throw new ArgumentNullException(nameof(e)); if (query == null) throw new ArgumentNullException(nameof(query)); - if (voiceClient == null) - throw new NullReferenceException($"{nameof(voiceClient)} is null, bot didn't join any server."); - - this.VoiceClient = voiceClient; + if (mc.VoiceClient == null) + throw new NullReferenceException($"{nameof(mc.VoiceClient)} is null, bot didn't join any server."); + this.MusicControls = mc; + this.VoiceClient = mc.VoiceClient; this.Server = e.Server; this.Query = query; - ResolveStreamLink(); + Task.Run(() => ResolveStreamLink()); } - private Task ResolveStreamLink() => - Task.Run(() => { + private void ResolveStreamLink() { + VideoInfo video = null; + try { Console.WriteLine("Resolving video link"); - var video = DownloadUrlResolver.GetDownloadUrls(Searches.FindYoutubeUrlByKeywords(Query)) - .Where(v => v.AdaptiveType == AdaptiveType.Audio) - .OrderByDescending(v => v.AudioBitrate).FirstOrDefault(); + video = DownloadUrlResolver.GetDownloadUrls(Searches.FindYoutubeUrlByKeywords(Query)) + .Where(v => v.AdaptiveType == AdaptiveType.Audio) + .OrderByDescending(v => v.AudioBitrate).FirstOrDefault(); if (video == null) // do something with this error throw new Exception("Could not load any video elements based on the query."); Title = video.Title; + } catch (Exception ex) { + privateState = StreamState.Completed; + Console.WriteLine($"Failed resolving the link.{ex.Message}"); + return; + } - musicStreamer = new MusicStreamer(this, video.DownloadUrl, Channel); - if(OnQueued!=null) - OnQueued(); - }); + musicStreamer = new MusicStreamer(this, video.DownloadUrl, Channel); + if (OnQueued != null) + OnQueued(); + } internal string PrintStats() => musicStreamer?.Stats(); - internal void Pause() { - throw new NotImplementedException(); - } - public Action OnQueued = null; public Action OnBuffering = null; public Action OnStarted = null; @@ -84,24 +90,28 @@ namespace NadekoBot.Classes.Music { musicStreamer?.Cancel(); } + internal void Stop() { + musicStreamer?.Stop(); + } + internal Task Start() => Task.Run(async () => { Console.WriteLine("Start called."); - int attemptsLeft = 7; - //wait for up to 7 seconds to resolve a link + int attemptsLeft = 4; + //wait for up to 4 seconds to resolve a link while (State == StreamState.Resolving) { await Task.Delay(1000); Console.WriteLine("Resolving..."); if (--attemptsLeft == 0) { - Console.WriteLine("Resolving timed out."); - return; + throw new TimeoutException("Resolving timed out."); } } try { await musicStreamer.StartPlayback(); } catch (Exception ex) { - Console.WriteLine("Error in start playback." + ex); + Console.WriteLine("Error in start playback." + ex.Message); + privateState = StreamState.Completed; } }); } @@ -113,6 +123,7 @@ namespace NadekoBot.Classes.Music { public StreamState State { get; internal set; } public string Url { get; } private bool IsCanceled { get; set; } + public bool IsPaused => parent.IsPaused; StreamRequest parent; private readonly object _bufferLock = new object(); @@ -152,14 +163,23 @@ namespace NadekoBot.Classes.Music { while (true) { while (buffer.writePos - buffer.readPos > 5.MB() && State != StreamState.Completed) { - if (bufferCancelSource.Token.CanBeCanceled && !bufferCancelSource.IsCancellationRequested) { - bufferCancelSource.Cancel(); - Console.WriteLine("Canceling buffer token"); - } + if (!bufferCancelSource.IsCancellationRequested) { + Console.WriteLine("Canceling buffer token"); + Task.Run(() => bufferCancelSource.Cancel()); + } - await Task.Delay(1000); + await Task.Delay(50); } - + + if (State == StreamState.Completed) { + try { + p.CancelOutputRead(); + p.Close(); + } catch (Exception) { } + Console.WriteLine("Buffering canceled, stream is completed."); + return; + } + if (buffer.readPos > 5.MiB()) { // if buffer is over 5 MiB, create new one Console.WriteLine("Buffer over 5 megs, clearing."); @@ -175,16 +195,7 @@ namespace NadekoBot.Classes.Music { buffer.readPos = newReadPos; buffer.Position = newPos; } - } - - if (State == StreamState.Completed) { - try { - p.CancelOutputRead(); - p.Close(); - } catch (Exception) { } - Console.WriteLine("Buffering canceled, stream is completed."); - return; - } + } var buf = new byte[1024]; int read = 0; @@ -212,6 +223,7 @@ namespace NadekoBot.Classes.Music { internal async Task StartPlayback() { Console.WriteLine("Starting playback."); + if (State == StreamState.Playing) return; State = StreamState.Playing; if (parent.OnBuffering != null) parent.OnBuffering(); @@ -253,22 +265,28 @@ namespace NadekoBot.Classes.Music { } else attempt = 0; + + if (State == StreamState.Completed) { Console.WriteLine("Canceled"); break; } parent.VoiceClient.Send(voiceBuffer, 0, voiceBuffer.Length); + + while (IsPaused) { + await Task.Delay(50); + } } parent.VoiceClient.Wait(); - StopPlayback(); + Stop(); } internal void Cancel() { IsCanceled = true; } - internal void StopPlayback() { + internal void Stop() { Console.WriteLine("Stopping playback"); if (State != StreamState.Completed) { State = StreamState.Completed; diff --git a/NadekoBot/Modules/Music.cs b/NadekoBot/Modules/Music.cs index 993db9b9..cea47b50 100644 --- a/NadekoBot/Modules/Music.cs +++ b/NadekoBot/Modules/Music.cs @@ -13,34 +13,20 @@ namespace NadekoBot.Modules { public static ConcurrentDictionary musicPlayers = new ConcurrentDictionary(); - internal static void CleanMusicPlayers() { - foreach (var mp in musicPlayers - .Where(kvp => kvp.Value.CurrentSong == null - && kvp.Value.SongQueue.Count == 0)) { - var val = mp.Value; - (musicPlayers as System.Collections.IDictionary).Remove(mp.Key); - } - } - internal static string GetMusicStats() { var servers = 0; var queued = 0; - musicPlayers.ForEach(kvp => { - var mp = kvp.Value; - if (mp.SongQueue.Count > 0 || mp.CurrentSong != null) - queued += mp.SongQueue.Count + 1; - servers++; - }); + var stats = musicPlayers.Where(kvp => kvp.Value?.SongQueue.Count > 0 || kvp.Value?.CurrentSong != null); - return $"Playing {queued} songs across {servers} servers."; + return $"Playing {stats.Count()} songs, {stats.Sum(kvp => kvp.Value?.SongQueue?.Count ?? 0)} queued."; } public Music() : base() { - Timer cleaner = new Timer(); + /*Timer cleaner = new Timer(); cleaner.Elapsed += (s, e) => System.Threading.Tasks.Task.Run(() => CleanMusicPlayers()); cleaner.Interval = 10000; cleaner.Start(); - /* + Timer statPrinter = new Timer(); NadekoBot.client.Connected += (s, e) => { if (statPrinter.Enabled) return; @@ -62,8 +48,8 @@ namespace NadekoBot.Modules { .Alias("next") .Description("Goes to the next song in the queue.") .Do(e => { - if (musicPlayers.ContainsKey(e.Server) == false || (musicPlayers[e.Server]?.CurrentSong) == null) return; - musicPlayers[e.Server].CurrentSong.Cancel(); + if (musicPlayers.ContainsKey(e.Server) == false) return; + musicPlayers[e.Server].LoadNextSong(); }); cgb.CreateCommand("s") @@ -72,10 +58,9 @@ namespace NadekoBot.Modules { .Do(e => { if (musicPlayers.ContainsKey(e.Server) == false) return; var player = musicPlayers[e.Server]; - player.RemoveAllSongs(); - if (player.CurrentSong != null) { - player.CurrentSong.Cancel(); - } + player.Stop(); + MusicControls throwAwayValue; + musicPlayers.TryRemove(e.Server, out throwAwayValue); }); cgb.CreateCommand("p") @@ -83,14 +68,10 @@ namespace NadekoBot.Modules { .Description("Pauses the song") .Do(async e => { if (musicPlayers.ContainsKey(e.Server) == false) return; - await e.Send("This feature is coming tomorrow."); - /* - if (musicPlayers[e.Server].Pause()) - if (musicPlayers[e.Server].IsPaused) - await e.Send("Music player Paused"); - else - await e.Send("Music player unpaused."); - */ + if (musicPlayers[e.Server].TogglePause()) + await e.Send("Music player paused."); + else + await e.Send("Music player unpaused."); }); cgb.CreateCommand("q") @@ -99,18 +80,23 @@ namespace NadekoBot.Modules { .Parameter("query", ParameterType.Unparsed) .Do(async e => { if (musicPlayers.ContainsKey(e.Server) == false) - if (musicPlayers.Count > 25) { - await e.Send($"{e.User.Mention}, playlist supports up to 25 songs. If you think this is not enough, contact the owner.:warning:"); + if (!musicPlayers.TryAdd(e.Server, new MusicControls(e.User.VoiceChannel))) { + await e.Send("Failed to create a music player for this server"); return; - } - else - (musicPlayers as System.Collections.IDictionary).Add(e.Server, new MusicControls(e.User.VoiceChannel)); + } var player = musicPlayers[e.Server]; + + if (player.SongQueue.Count > 25) { + await e.Send("Music player supports up to 25 songs atm. Contant the owner if you think this is not enough :warning:"); + } + try { var sr = player.CreateStreamRequest(e, e.GetArg("query"), player.VoiceChannel); + if (sr == null) + throw new NullReferenceException("StreamRequest is null."); Message msg = null; - sr.OnQueued += async() => { + sr.OnQueued += async () => { msg = await e.Send($":musical_note:**Queued** {sr.Title}"); }; sr.OnCompleted += async () => { @@ -126,12 +112,9 @@ namespace NadekoBot.Modules { if (msg != null) msg = await e.Send($":musical_note:**Buffering the song**...{sr.Title}"); }; - lock (player.SongQueue) { - player.SongQueue.Add(sr); - } } catch (Exception ex) { Console.WriteLine(); - await e.Send($"Error. :anger:\n{ex.Message}"); + await e.Send($":anger: {ex.Message}"); return; } }); @@ -171,5 +154,5 @@ namespace NadekoBot.Modules { }); }); } - } + } } diff --git a/NadekoBot/NadekoBot.cs b/NadekoBot/NadekoBot.cs index 64786f80..54f7d0d2 100644 --- a/NadekoBot/NadekoBot.cs +++ b/NadekoBot/NadekoBot.cs @@ -82,7 +82,7 @@ namespace NadekoBot { //reply to personal messages and forward if enabled. client.MessageReceived += Client_MessageReceived; - + //add command service var commands = client.Services.Add(commandService); @@ -110,10 +110,13 @@ namespace NadekoBot { if (loadTrello) modules.Add(new Trello(), "Trello", ModuleFilter.None); + //start the timer for stats + _statsSW.Start(); //run the bot client.ExecuteAndWait(async () => { await client.Connect(c.Username, c.Password); + LoadStats(); Console.WriteLine("-----------------"); Console.WriteLine(GetStats()); Console.WriteLine("-----------------"); @@ -127,12 +130,21 @@ namespace NadekoBot { Console.WriteLine("Exiting..."); Console.ReadKey(); } - - public static string GetStats() => + private static string _statsCache = ""; + private static Stopwatch _statsSW = new Stopwatch(); + public static string GetStats() { + if (_statsSW.ElapsedTicks > 5) { + LoadStats(); + _statsSW.Restart(); + } + return _statsCache; + } + private static void LoadStats() { + _statsCache = "Author: Kwoth" + - $"\nDiscord.Net version: {DiscordConfig.LibVersion}"+ + $"\nDiscord.Net version: {DiscordConfig.LibVersion}" + $"\nRuntime: {client.GetRuntime()}" + - $"\nBot Version: {BotVersion}"+ + $"\nBot Version: {BotVersion}" + $"\nLogged in as: {client.CurrentUser.Name}" + $"\nBot id: {client.CurrentUser.Id}" + $"\nUptime: {GetUptimeString()}" + @@ -141,6 +153,7 @@ namespace NadekoBot { $"\nUsers: {client.Servers.SelectMany(x => x.Users.Select(y => y.Id)).Count()} ({client.Servers.SelectMany(x => x.Users.Select(y => y.Id)).Distinct().Count()} unique) ({client.Servers.SelectMany(x => x.Users.Where(y => y.Status != UserStatus.Offline).Select(y => y.Id)).Distinct().Count()} online)" + $"\nHeap: {Math.Round(GC.GetTotalMemory(true) / (1024.0 * 1024.0), 2).ToString()}MB" + $"\nCommands Ran this session: {commandsRan}"; + } public static string GetUptimeString() { var time = (DateTime.Now - Process.GetCurrentProcess().StartTime); @@ -167,8 +180,8 @@ namespace NadekoBot { } if (ForwardMessages && OwnerUser != null) - await OwnerUser.SendMessage(e.User +": ```\n"+e.Message.Text+"\n```"); - + await OwnerUser.SendMessage(e.User + ": ```\n" + e.Message.Text + "\n```"); + if (repliedRecently = !repliedRecently) { await e.Send("You can type `-h` or `-help` or `@MyName help` in any of the channels I am in and I will send you a message with my commands.\n Or you can find out what i do here: https://github.com/Kwoth/NadekoBot\nYou can also just send me an invite link to a server and I will join it.\nIf you don't want me on your server, you can simply ban me ;(\nBot Creator's server: https://discord.gg/0ehQwTK2RBhxEi0X"); Timer t = new Timer();