From d242952d4a84e1e0186c930e178250bdf3062eef Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 08:15:58 +0200 Subject: [PATCH 01/40] Huge amount of work on the music rework. Around 60% done. Fixed bot getting stuck when server region is changed. --- .../Replacements/ReplacementBuilder.cs | 38 +- .../DataStructures/SyncPrecondition.cs | 23 + src/NadekoBot/Modules/Music/Music.cs | 1229 +++++++++-------- .../Administration/PlayingRotateService.cs | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- .../Services/Impl/SyncPreconditionService.cs | 13 + src/NadekoBot/Services/Music/Exceptions.cs | 12 +- src/NadekoBot/Services/Music/MusicControls.cs | 381 ----- src/NadekoBot/Services/Music/MusicPlayer.cs | 657 +++++++++ src/NadekoBot/Services/Music/MusicQueue.cs | 119 ++ src/NadekoBot/Services/Music/MusicService.cs | 541 ++++---- src/NadekoBot/Services/Music/Song.cs | 466 +++---- src/NadekoBot/Services/Music/SongBuffer.cs | 378 ++--- src/NadekoBot/Services/Music/SongInfo.cs | 85 ++ src/NadekoBot/Services/Music/SongResolver.cs | 111 ++ 15 files changed, 2304 insertions(+), 1753 deletions(-) create mode 100644 src/NadekoBot/DataStructures/SyncPrecondition.cs create mode 100644 src/NadekoBot/Services/Impl/SyncPreconditionService.cs delete mode 100644 src/NadekoBot/Services/Music/MusicControls.cs create mode 100644 src/NadekoBot/Services/Music/MusicPlayer.cs create mode 100644 src/NadekoBot/Services/Music/MusicQueue.cs create mode 100644 src/NadekoBot/Services/Music/SongInfo.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver.cs diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs index b7fabd25..2a7ac6bf 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -74,25 +74,25 @@ namespace NadekoBot.DataStructures.Replacements return this; } - public ReplacementBuilder WithMusic(MusicService ms) - { - _reps.TryAdd("%playing%", () => - { - var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try - { - var mp = ms.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch - { - return "No songs"; - } - }); - _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); - return this; - } + //public ReplacementBuilder WithMusic(MusicService ms) + //{ + // _reps.TryAdd("%playing%", () => + // { + // var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + // if (cnt != 1) return cnt.ToString(); + // try + // { + // var mp = ms.MusicPlayers.FirstOrDefault(); + // return mp.Value.CurrentSong.SongInfo.Title; + // } + // catch + // { + // return "No songs"; + // } + // }); + // _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); + // return this; + //} public ReplacementBuilder WithRngRegex() { diff --git a/src/NadekoBot/DataStructures/SyncPrecondition.cs b/src/NadekoBot/DataStructures/SyncPrecondition.cs new file mode 100644 index 00000000..6dd675e5 --- /dev/null +++ b/src/NadekoBot/DataStructures/SyncPrecondition.cs @@ -0,0 +1,23 @@ +ο»Ώusing Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + //public class SyncPrecondition : PreconditionAttribute + //{ + // public override Task CheckPermissions(ICommandContext context, + // CommandInfo command, + // IServiceProvider services) + // { + + // } + //} + //public enum SyncType + //{ + // Guild + //} +} diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ebec33c5..472acd17 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -15,6 +15,7 @@ using NadekoBot.Services.Database.Models; using System.Threading; using NadekoBot.Services.Music; using NadekoBot.DataStructures; +using System.Collections.Concurrent; namespace NadekoBot.Modules.Music { @@ -35,139 +36,79 @@ namespace NadekoBot.Modules.Music _google = google; _db = db; _music = music; - //it can fail if its currenctly opened or doesn't exist. Either way i don't care - _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } - private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //{ + // var usr = iusr as SocketGuildUser; + // if (usr == null || + // oldState.VoiceChannel == newState.VoiceChannel) + // return Task.CompletedTask; + + // MusicPlayer player; + // if ((player = _music.GetPlayer(usr.Guild.Id)) == null) + // return Task.CompletedTask; + + // try + // { + // //if bot moved + // if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && + // usr.Id == _client.CurrentUser.Id) + // { + // if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // player.TogglePause(); + // else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // player.TogglePause(); + + // return Task.CompletedTask; + // } + + + // //if some other user moved + // if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // player.Paused && + // newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) + // (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // !player.Paused && + // oldState.VoiceChannel.Users.Count == 1)) + // { + // player.TogglePause(); + // return Task.CompletedTask; + // } + + // } + // catch + // { + // // ignored + // } + // return Task.CompletedTask; + //} + + private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { - var usr = iusr as SocketGuildUser; - if (usr == null || - oldState.VoiceChannel == newState.VoiceChannel) - return Task.CompletedTask; - - MusicPlayer player; - if ((player = _music.GetPlayer(usr.Guild.Id)) == null) - return Task.CompletedTask; - - try + var qData = mp.Enqueue(songInfo); + if (qData.Success) { - //if bot moved - if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - usr.Id == _client.CurrentUser.Id) + if (!silent) { - if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - player.TogglePause(); - else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - player.TogglePause(); - - return Task.CompletedTask; + try + { + //var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (qData.Index)).WithMusicIcon()) + .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") + .WithThumbnailUrl(songInfo.Thumbnail) + .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) + .ConfigureAwait(false); + queuedMessage?.DeleteAfter(10); + } + catch + { + // ignored + } // if queued message sending fails, don't attempt to delete it } - - - //if some other user moved - if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - player.Paused && - newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) - (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - !player.Paused && - oldState.VoiceChannel.Users.Count == 1)) - { - player.TogglePause(); - return Task.CompletedTask; - } - - } - catch - { - // ignored - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Next(int skipCount = 1) - { - if (skipCount < 1) - return Task.CompletedTask; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (musicPlayer.PlaybackVoiceChannel == ((IGuildUser)Context.User).VoiceChannel) - { - while (--skipCount > 0) - { - musicPlayer.RemoveSongAt(0); - } - musicPlayer.Next(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Stop() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - { - musicPlayer.Autoplay = false; - musicPlayer.Stop(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Destroy() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - _music.DestroyPlayer(Context.Guild.Id); - - return Task.CompletedTask; - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Pause() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; - musicPlayer.TogglePause(); - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Fairplay() - { - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; - - if (val) - { - await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); } } @@ -175,7 +116,10 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); + var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + await InternalQueue(mp, songInfo, false); + if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); @@ -217,29 +161,16 @@ namespace NadekoBot.Modules.Music { try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudQueue([Remainder] string query) - { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 1) { - Song currentSong; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if ((currentSong = musicPlayer?.CurrentSong) == null) + var mp = await _music.GetOrCreatePlayer(Context); + var (current, songs) = mp.QueueArray(); + + if (!songs.Any()) { await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; @@ -247,33 +178,39 @@ namespace NadekoBot.Modules.Music if (--page < 0) return; - - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + //todo say whether music player is stopped + //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; - var total = musicPlayer.TotalPlaytime; - var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", - (int) total.TotalHours, - total.Minutes, - total.Seconds); - var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; - var lastPage = musicPlayer.Playlist.Count / itemsPerPage; + //var total = musicPlayer.TotalPlaytime; + //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", + // (int)total.TotalHours, + // total.Minutes, + // total.Seconds); + //var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; + var lastPage = songs.Length / itemsPerPage; Func printAction = curPage => { var startAt = itemsPerPage * curPage; var number = 0 + startAt; - var desc = string.Join("\n", musicPlayer.Playlist + var desc = string.Join("\n", songs .Skip(startAt) .Take(itemsPerPage) - .Select(v => $"`{++number}.` {v.PrettyFullName}")); - - desc = $"`πŸ”Š` {currentSong.PrettyFullName}\n\n" + desc; + .Select(v => + { + if(number++ == current) + return $"**β‡’**`{number}.` {v.PrettyFullName}"; + else + return $"`{number}.` {v.PrettyFullName}"; + })); //todo v.prettyfullname instead of title - if (musicPlayer.RepeatSong) - desc = "πŸ”‚ " + GetText("repeating_cur_song") +"\n\n" + desc; - else if (musicPlayer.RepeatPlaylist) - desc = "πŸ” " + GetText("repeating_playlist")+"\n\n" + desc; + desc = $"`πŸ”Š` {songs[current].PrettyFullName}\n\n" + desc; + + if (mp.RepeatCurrentSong) + desc = "πŸ”‚ " + GetText("repeating_cur_song") + "\n\n" + desc; + //else if (musicPlayer.RepeatPlaylist) + // desc = "πŸ” " + GetText("repeating_playlist") + "\n\n" + desc; @@ -281,12 +218,12 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) .WithDescription(desc) - .WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + - $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + - (musicPlayer.FairPlay - ? "βœ”οΈ" + GetText("fairplay") - : "βœ–οΈ" + GetText("fairplay")) + " | " + - (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) + //.WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + + // $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + + // (musicPlayer.FairPlay + // ? "βœ”οΈ" + GetText("fairplay") + // : "βœ–οΈ" + GetText("fairplay")) + " | " + + // (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) .WithOkColor(); return embed; @@ -296,41 +233,52 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task NowPlaying() + public async Task Next(int skipCount = 1) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (skipCount < 1) return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var mp = await _music.GetOrCreatePlayer(Context); - var embed = new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) - .WithDescription(currentSong.PrettyName) - .WithThumbnailUrl(currentSong.Thumbnail) - .WithFooter(ef => ef.WithText(musicPlayer.PrettyVolume + " | " + currentSong.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + mp.Next(); + } - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Stop() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task Destroy() + { + _music.DestroyPlayer(Context.Guild.Id); + return Task.CompletedTask; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Pause() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.TogglePause(); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Volume(int val) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); if (val < 0 || val > 100) { await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); return; } - var volume = musicPlayer.SetVolume(val); - await ReplyConfirmLocalized("volume_set", volume).ConfigureAwait(false); + mp.SetVolume(val); + await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -350,401 +298,40 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ShufflePlaylist() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - if (musicPlayer.Playlist.Count < 2) - return; - - musicPlayer.Shuffle(); - await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Playlist([Remainder] string playlist) - { - - var arg = playlist; - if (string.IsNullOrWhiteSpace(arg)) - return; - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); - if (plId == null) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); - if (!ids.Any()) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var count = ids.Count(); - var msg = await Context.Channel.SendMessageAsync("🎡 " + GetText("attempting_to_queue", - Format.Bold(count.ToString()))).ConfigureAwait(false); - - var cancelSource = new CancellationTokenSource(); - - var gusr = (IGuildUser)Context.User; - while (ids.Any() && !cancelSource.IsCancellationRequested) - { - var tasks = Task.WhenAll(ids.Take(5).Select(async id => - { - if (cancelSource.Token.IsCancellationRequested) - return; - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { try { cancelSource.Cancel(); } catch { } } - })); - - await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); - ids = ids.Skip(5); - } - - await msg.ModifyAsync(m => m.Content = "βœ… " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudPl([Remainder] string pl) - { - - pl = pl?.Trim(); - - if (string.IsNullOrWhiteSpace(pl)) - return; - - using (var http = new HttpClient()) - { - var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - foreach (var svideo in scvids.Skip(1)) - { - try - { - musicPlayer.AddSong(new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Normal, - Query = svideo.TrackLink, - }), ((IGuildUser)Context.User).Username); - } - catch (PlaylistFullException) { break; } - } - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task LocalPl([Remainder] string directory) - { - - var arg = directory; - if (string.IsNullOrWhiteSpace(arg)) - return; - var dir = new DirectoryInfo(arg); - var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) - .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); - var gusr = (IGuildUser)Context.User; - foreach (var file in fileEnum) - { - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); - } - catch (PlaylistFullException) - { - break; - } - catch - { - // ignored - } - } - await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Radio(string radioLink) - { - - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Local([Remainder] string path) - { - - var arg = path; - if (string.IsNullOrWhiteSpace(arg)) - return; - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); - - } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ - - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] - public Task SongRemove(int num) + public async Task SongRemove(int index) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; + var mp = await _music.GetOrCreatePlayer(Context); + try + { + var song = mp.RemoveAt(index - 1); + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .WithErrorColor(); - musicPlayer.RemoveSongAt(num - 1); - return Task.CompletedTask; + await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (ArgumentOutOfRangeException) + { + //todo error message + } } + public enum All { All } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(1)] - public async Task SongRemove(string all) + public async Task SongRemove(All all) { - if (all.Trim().ToUpperInvariant() != "ALL") - return; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.ClearQueue(); + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(true); await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task MoveSong([Remainder] string fromto) - { - if (string.IsNullOrWhiteSpace(fromto)) - return; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - fromto = fromto?.Trim(); - var fromtoArr = fromto.Split('>'); - - int n1; - int n2; - - var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); - - if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || - !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || - n1 > playlist.Count || n2 > playlist.Count) - { - await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); - return; - } - - var s = playlist[n1 - 1]; - playlist.Insert(n2 - 1, s); - var nn1 = n2 < n1 ? n1 : n1 - 1; - playlist.RemoveAt(nn1); - - var embed = new EmbedBuilder() - .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") - .WithUrl(s.SongUrl) - .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - //await channel.SendConfirmAsync($"🎡Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxQueue(uint size = 0) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - musicPlayer.MaxQueueSize = size; - - if(size == 0) - await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxPlaytime(uint seconds) - { - if (seconds < 15 && seconds != 0) - return; - - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.MaxPlaytimeSeconds = seconds; - if (seconds == 0) - await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ReptCurSong() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - var currentValue = musicPlayer.ToggleRepeatSong(); - - if (currentValue) - await Context.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithAuthor(eab => eab.WithMusicIcon().WithName("πŸ”‚ " + GetText("repeating_track"))) - .WithDescription(currentSong.PrettyName) - .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); - else - await Context.Channel.SendConfirmAsync("πŸ”‚ " + GetText("repeating_track_stopped")) - .ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RepeatPl() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentValue = musicPlayer.ToggleRepeatPlaylist(); - if(currentValue) - await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Save([Remainder] string name) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - var curSong = musicPlayer.CurrentSong; - var songs = musicPlayer.Playlist.Append(curSong) - .Select(s => new PlaylistSong() - { - Provider = s.SongInfo.Provider, - ProviderType = s.SongInfo.ProviderType, - Title = s.SongInfo.Title, - Uri = s.SongInfo.Uri, - Query = s.SongInfo.Query, - }).ToList(); - - MusicPlaylist playlist; - using (var uow = _db.UnitOfWork) - { - playlist = new MusicPlaylist - { - Name = name, - Author = Context.User.Username, - AuthorId = Context.User.Id, - Songs = songs, - }; - uow.MusicPlaylists.Add(playlist); - await uow.CompleteAsync().ConfigureAwait(false); - } - - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithTitle(GetText("playlist_saved")) - .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) - .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Load([Remainder] int id) - { - MusicPlaylist mpl; - using (var uow = _db.UnitOfWork) - { - mpl = uow.MusicPlaylists.GetWithSongs(id); - } - - if (mpl == null) - { - await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); - return; - } - IUserMessage msg = null; - try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - foreach (var item in mpl.Songs) - { - var usr = (IGuildUser)Context.User; - try - { - await _music.QueueSong(usr, (ITextChannel)Context.Channel, usr.VoiceChannel, item.Query, true, item.ProviderType).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { break; } - } - if (msg != null) - await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Playlists([Remainder] int num = 1) @@ -767,7 +354,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task DeletePlaylist([Remainder] int id) @@ -803,69 +390,489 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Goto(int time) + public async Task Save([Remainder] string name) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); - if (time < 0) - return; + var songs = mp.QueueArray().Songs + .Select(s => new PlaylistSong() + { + Provider = s.Provider, + ProviderType = s.ProviderType, + Title = s.Title, + Uri = s.Uri, + Query = s.Query, + }).ToList(); - var currentSong = musicPlayer.CurrentSong; + MusicPlaylist playlist; + using (var uow = _db.UnitOfWork) + { + playlist = new MusicPlaylist + { + Name = name, + Author = Context.User.Username, + AuthorId = Context.User.Id, + Songs = songs, + }; + uow.MusicPlaylists.Add(playlist); + await uow.CompleteAsync().ConfigureAwait(false); + } - if (currentSong == null) - return; - - //currentSong.PrintStatusMessage = false; - var gotoSong = currentSong.Clone(); - gotoSong.SkipTo = time; - musicPlayer.AddSong(gotoSong, 0); - musicPlayer.Next(); - - var minutes = (time / 60).ToString(); - var seconds = (time % 60).ToString(); - - if (minutes.Length == 1) - minutes = "0" + minutes; - if (seconds.Length == 1) - seconds = "0" + seconds; - - await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithTitle(GetText("playlist_saved")) + .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) + .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); } + private readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Autoplay() + public async Task Load([Remainder] int id) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (!PlaylistLoadBlacklist.Add(Context.Guild.Id)) return; + try + { + var mp = await _music.GetOrCreatePlayer(Context); + MusicPlaylist mpl; + using (var uow = _db.UnitOfWork) + { + mpl = uow.MusicPlaylists.GetWithSongs(id); + } - if (!musicPlayer.ToggleAutoplay()) - await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + if (mpl == null) + { + await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); + return; + } + IUserMessage msg = null; + try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + foreach (var item in mpl.Songs) + { + try + { + //todo fix for all + if (item.ProviderType == MusicType.Normal) + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + } + catch (SongNotFoundException) { } + catch { break; } + } + if (msg != null) + await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); + } + finally + { + PlaylistLoadBlacklist.TryRemove(Context.Guild.Id); + } } + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Fairplay() + //{ + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; + + // if (val) + // { + // await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); + // } + // else + // { + // await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudQueue([Remainder] string query) + //{ + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task NowPlaying() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var embed = new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) + .WithDescription(currentSong.PrettyName) + .WithThumbnailUrl(currentSong.Thumbnail) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + /*currentSong.PrettyFullTime +*/ $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task ShufflePlaylist() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // if (musicPlayer.Playlist.Count < 2) + // return; + + // musicPlayer.Shuffle(); + // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Playlist([Remainder] string playlist) + //{ + + // var arg = playlist; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); + // if (plId == null) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + // if (!ids.Any()) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var count = ids.Count(); + // var msg = await Context.Channel.SendMessageAsync("🎡 " + GetText("attempting_to_queue", + // Format.Bold(count.ToString()))).ConfigureAwait(false); + + // var cancelSource = new CancellationTokenSource(); + + // var gusr = (IGuildUser)Context.User; + // while (ids.Any() && !cancelSource.IsCancellationRequested) + // { + // var tasks = Task.WhenAll(ids.Take(5).Select(async id => + // { + // if (cancelSource.Token.IsCancellationRequested) + // return; + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); + // } + // catch (SongNotFoundException) { } + // catch { try { cancelSource.Cancel(); } catch { } } + // })); + + // await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); + // ids = ids.Skip(5); + // } + + // await msg.ModifyAsync(m => m.Content = "βœ… " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudPl([Remainder] string pl) + //{ + + // pl = pl?.Trim(); + + // if (string.IsNullOrWhiteSpace(pl)) + // return; + + // using (var http = new HttpClient()) + // { + // var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // foreach (var svideo in scvids.Skip(1)) + // { + // try + // { + // musicPlayer.AddSong(new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Normal, + // Query = svideo.TrackLink, + // }), ((IGuildUser)Context.User).Username); + // } + // catch (PlaylistFullException) { break; } + // } + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task LocalPl([Remainder] string directory) + //{ + + // var arg = directory; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // var dir = new DirectoryInfo(arg); + // var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) + // .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); + // var gusr = (IGuildUser)Context.User; + // foreach (var file in fileEnum) + // { + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); + // } + // catch (PlaylistFullException) + // { + // break; + // } + // catch + // { + // // ignored + // } + // } + // await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Radio(string radioLink) + //{ + + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task Local([Remainder] string path) + //{ + + // var arg = path; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Move() + //{ + + // MusicPlayer musicPlayer; + // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; + // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) + // return; + // await musicPlayer.MoveToVoiceChannel(voiceChannel); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task MoveSong([Remainder] string fromto) + //{ + // if (string.IsNullOrWhiteSpace(fromto)) + // return; + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // fromto = fromto?.Trim(); + // var fromtoArr = fromto.Split('>'); + + // int n1; + // int n2; + + // var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); + + // if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || + // !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || + // n1 > playlist.Count || n2 > playlist.Count) + // { + // await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); + // return; + // } + + // var s = playlist[n1 - 1]; + // playlist.Insert(n2 - 1, s); + // var nn1 = n2 < n1 ? n1 : n1 - 1; + // playlist.RemoveAt(nn1); + + // var embed = new EmbedBuilder() + // .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") + // .WithUrl(s.SongUrl) + // .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) + // .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) + // .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + // .WithColor(NadekoBot.OkColor); + // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + + // //await channel.SendConfirmAsync($"🎡Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); + + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxQueue(uint size = 0) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // musicPlayer.MaxQueueSize = size; + + // if(size == 0) + // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxPlaytime(uint seconds) + //{ + // if (seconds < 15 && seconds != 0) + // return; + + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // musicPlayer.MaxPlaytimeSeconds = seconds; + // if (seconds == 0) + // await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ReptCurSong() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + var currentValue = mp.ToggleRepeatSong(); + + if (currentValue) + await Context.Channel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithMusicIcon().WithName("πŸ”‚ " + GetText("repeating_track"))) + .WithDescription(currentSong.PrettyName) + .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); + else + await Context.Channel.SendConfirmAsync("πŸ”‚ " + GetText("repeating_track_stopped")) + .ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task RepeatPl() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // var currentValue = musicPlayer.ToggleRepeatPlaylist(); + // if(currentValue) + // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Goto(int time) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + + // if (time < 0) + // return; + + // var currentSong = musicPlayer.CurrentSong; + + // if (currentSong == null) + // return; + + // //currentSong.PrintStatusMessage = false; + // var gotoSong = currentSong.Clone(); + // gotoSong.SkipTo = time; + // musicPlayer.AddSong(gotoSong, 0); + // musicPlayer.Next(); + + // var minutes = (time / 60).ToString(); + // var seconds = (time % 60).ToString(); + + // if (minutes.Length == 1) + // minutes = "0" + minutes; + // if (seconds.Length == 1) + // seconds = "0" + seconds; + + // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Autoplay() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // if (!musicPlayer.ToggleAutoplay()) + // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + //} + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] public async Task SetMusicChannel() { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - { - await ReplyErrorLocalized("no_player").ConfigureAwait(false); - return; - } + var mp = await _music.GetOrCreatePlayer(Context); - musicPlayer.OutputTextChannel = (ITextChannel)Context.Channel; + mp.OutputTextChannel = (ITextChannel)Context.Channel; await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false); } - } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 9e33aef7..d4e09ad1 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Services.Administration _rep = new ReplacementBuilder() .WithClient(client) .WithStats(client) - .WithMusic(music) + //.WithMusic(music) .Build(); _t = new Timer(async (objState) => diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 4b9b6bac..ed7ec815 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.52"; + public const string BotVersion = "1.53"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/Impl/SyncPreconditionService.cs b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs new file mode 100644 index 00000000..30d5b48a --- /dev/null +++ b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs @@ -0,0 +1,13 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Impl +{ + public class SyncPreconditionService + { + + } +} diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs index 1dbe8ad7..af195c15 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,7 +2,7 @@ namespace NadekoBot.Services.Music { - class PlaylistFullException : Exception + public class PlaylistFullException : Exception { public PlaylistFullException(string message) : base(message) { @@ -10,11 +10,19 @@ namespace NadekoBot.Services.Music public PlaylistFullException() : base("Queue is full.") { } } - class SongNotFoundException : Exception + public class SongNotFoundException : Exception { public SongNotFoundException(string message) : base(message) { } public SongNotFoundException() : base("Song is not found.") { } } + public class NotInVoiceChannelException : Exception + { + public NotInVoiceChannelException(string message) : base(message) + { + } + + public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } + } } diff --git a/src/NadekoBot/Services/Music/MusicControls.cs b/src/NadekoBot/Services/Music/MusicControls.cs deleted file mode 100644 index 4e688351..00000000 --- a/src/NadekoBot/Services/Music/MusicControls.cs +++ /dev/null @@ -1,381 +0,0 @@ -ο»Ώusing Discord; -using Discord.Audio; -using NadekoBot.Extensions; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NLog; -using NadekoBot.Services.Database.Models; - -namespace NadekoBot.Services.Music -{ - public enum StreamState - { - Resolving, - Queued, - Playing, - Completed - } - - public class MusicPlayer - { - private IAudioClient AudioClient { get; set; } - - /// - /// Player will prioritize different queuer name - /// over the song position in the playlist - /// - public bool FairPlay { get; set; } = false; - - /// - /// Song will stop playing after this amount of time. - /// To prevent people queueing radio or looped songs - /// while other people want to listen to other songs too. - /// - public uint MaxPlaytimeSeconds { get; set; } = 0; - - - // this should be written better - public TimeSpan TotalPlaytime => - _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? - TimeSpan.MaxValue : - new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); - - /// - /// Users who recently got their music wish - /// - private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - - private readonly List _playlist = new List(); - private readonly Logger _log; - private readonly IGoogleApiService _google; - - public IReadOnlyCollection Playlist => _playlist; - - public Song CurrentSong { get; private set; } - public CancellationTokenSource SongCancelSource { get; private set; } - private CancellationToken CancelToken { get; set; } - - public bool Paused { get; set; } - - public float Volume { get; private set; } - - public event Action OnCompleted = delegate { }; - public event Action OnStarted = delegate { }; - public event Action OnPauseChanged = delegate { }; - - public IVoiceChannel PlaybackVoiceChannel { get; private set; } - public ITextChannel OutputTextChannel { get; set; } - - private bool Destroyed { get; set; } - public bool RepeatSong { get; private set; } - public bool RepeatPlaylist { get; private set; } - public bool Autoplay { get; set; } - public uint MaxQueueSize { get; set; } = 0; - - private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); - - public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; - - public event Action SongRemoved = delegate { }; - - public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) - { - _log = LogManager.GetCurrentClassLogger(); - _google = google; - - OutputTextChannel = outputChannel; - Volume = defaultVolume ?? 1.0f; - - PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - - Task.Run(async () => - { - try - { - while (!Destroyed) - { - try - { - if (ActionQueue.TryDequeue(out Action action)) - { - action(); - } - } - finally - { - await Task.Delay(100).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - _log.Warn("Action queue crashed"); - _log.Warn(ex); - } - }).ConfigureAwait(false); - - var t = new Thread(async () => - { - while (!Destroyed) - { - try - { - CurrentSong = GetNextSong(); - - if (CurrentSong == null) - continue; - - while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || - AudioClient?.ConnectionState == ConnectionState.Connecting) - { - _log.Info("Waiting for Audio client"); - await Task.Delay(200).ConfigureAwait(false); - } - - if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) - AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); - - var index = _playlist.IndexOf(CurrentSong); - if (index != -1) - RemoveSongAt(index, true); - - OnStarted(this, CurrentSong); - try - { - await CurrentSong.Play(AudioClient, CancelToken); - } - catch (OperationCanceledException) - { - } - finally - { - OnCompleted(this, CurrentSong); - } - - - if (RepeatPlaylist & !RepeatSong) - AddSong(CurrentSong, CurrentSong.QueuerName); - - if (RepeatSong) - AddSong(CurrentSong, 0); - - } - catch (Exception ex) - { - _log.Warn("Music thread almost crashed."); - _log.Warn(ex); - await Task.Delay(3000).ConfigureAwait(false); - } - finally - { - if (!CancelToken.IsCancellationRequested) - { - SongCancelSource.Cancel(); - } - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - CurrentSong = null; - await Task.Delay(300).ConfigureAwait(false); - } - } - }); - - t.Start(); - } - - public void Next() - { - ActionQueue.Enqueue(() => - { - Paused = false; - SongCancelSource.Cancel(); - }); - } - - public void Stop() - { - ActionQueue.Enqueue(() => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - _playlist.Clear(); - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - public void TogglePause() => OnPauseChanged(Paused = !Paused); - - public int SetVolume(int volume) - { - if (volume < 0) - volume = 0; - if (volume > 100) - volume = 100; - - Volume = volume / 100.0f; - return volume; - } - - private Song GetNextSong() - { - if (!FairPlay) - { - return _playlist.FirstOrDefault(); - } - var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) - ?? _playlist.FirstOrDefault(); - - if (song == null) - return null; - - if (RecentlyPlayedUsers.Contains(song.QueuerName)) - { - RecentlyPlayedUsers.Clear(); - } - - RecentlyPlayedUsers.Add(song.QueuerName); - return song; - } - - public void Shuffle() - { - ActionQueue.Enqueue(() => - { - var oldPlaylist = _playlist.ToArray(); - _playlist.Clear(); - _playlist.AddRange(oldPlaylist.Shuffle()); - }); - } - - public void AddSong(Song s, string username) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ThrowIfQueueFull(); - ActionQueue.Enqueue(() => - { - s.MusicPlayer = this; - s.QueuerName = username.TrimTo(10); - _playlist.Add(s); - }); - } - - public void AddSong(Song s, int index) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Insert(index, s); - }); - } - - public void RemoveSong(Song s) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Remove(s); - }); - } - - public void RemoveSongAt(int index, bool silent = false) - { - ActionQueue.Enqueue(() => - { - if (index < 0 || index >= _playlist.Count) - return; - var song = _playlist.ElementAtOrDefault(index); - if (_playlist.Remove(song) && !silent) - { - SongRemoved(song, index); - } - - }); - } - - public void ClearQueue() - { - ActionQueue.Enqueue(() => - { - _playlist.Clear(); - }); - } - - public async Task UpdateSongDurationsAsync() - { - var curSong = CurrentSong; - var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - s.TotalTime == TimeSpan.Zero) - .ToArray(); - if (curSong != null) - { - Array.Resize(ref toUpdate, toUpdate.Length + 1); - toUpdate[toUpdate.Length - 1] = curSong; - } - var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - .Distinct(); - - var durations = await _google.GetVideoDurationsAsync(ids); - - toUpdate.ForEach(s => - { - foreach (var kvp in durations) - { - if (s.SongInfo.Query.EndsWith(kvp.Key)) - { - s.TotalTime = kvp.Value; - return; - } - } - }); - } - - public void Destroy() - { - ActionQueue.Enqueue(async () => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - Destroyed = true; - _playlist.Clear(); - - try { await AudioClient.StopAsync(); } catch { } - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - //public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) - //{ - // if (audioClient?.ConnectionState != ConnectionState.Connected) - // throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); - // PlaybackVoiceChannel = voiceChannel; - // audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); - //} - - public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; - - public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; - - public bool ToggleAutoplay() => Autoplay = !Autoplay; - - public void ThrowIfQueueFull() - { - if (MaxQueueSize == 0) - return; - if (_playlist.Count >= MaxQueueSize) - throw new PlaylistFullException(); - } - } -} diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs new file mode 100644 index 00000000..64c3aa22 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -0,0 +1,657 @@ +ο»Ώusing Discord; +using Discord.Audio; +using System; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using System.Diagnostics; + +namespace NadekoBot.Services.Music +{ + public enum StreamState + { + Resolving, + Queued, + Playing, + Completed + } + public class MusicPlayer : IDisposable + { + private readonly Task _player; + private readonly IVoiceChannel VoiceChannel; + private readonly Logger _log; + + private MusicQueue Queue { get; } = new MusicQueue(); + + public bool Exited { get; set; } = false; + public bool Stopped { get; private set; } = false; + public float Volume { get; private set; } = 1.0f; + public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; + private TaskCompletionSource pauseTaskSource { get; set; } = null; + + private CancellationTokenSource SongCancelSource { get; set; } + public ITextChannel OutputTextChannel { get; set; } + public (int Index, SongInfo Current) Current + { + get + { + if (Stopped) + return (0, null); + return Queue.Current; + } + } + + public bool RepeatCurrentSong { get; private set; } + + private IAudioClient _audioClient; + private readonly object locker = new object(); + + #region events + public event Action OnStarted; + public event Action OnCompleted; + public event Action OnPauseChanged; + #endregion + + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) + { + _log = LogManager.GetCurrentClassLogger(); + this.Volume = volume; + this.VoiceChannel = vch; + this.SongCancelSource = new CancellationTokenSource(); + this.OutputTextChannel = output; + + _player = Task.Run(async () => + { + while (!Exited) + { + CancellationToken cancelToken; + (int Index, SongInfo Song) data; + lock (locker) + { + data = Queue.Current; + cancelToken = SongCancelSource.Token; + } + try + { + _log.Info("Checking for songs"); + if (data.Song == null) + continue; + + _log.Info("Connecting"); + + + _log.Info("Starting"); + var p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + var ac = await GetAudioClient(); + if (ac == null) + continue; + var pcm = ac.CreatePCMStream(AudioApplication.Music); + + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try + { + while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken); + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + + OnCompleted?.Invoke(this, data.Song); + } + } + finally + { + _log.Info("Next song"); + do + { + await Task.Delay(100); + } + while (Stopped && !Exited); + if(!RepeatCurrentSong) + Queue.Next(); + } + } + }, SongCancelSource.Token); + } + + private async Task GetAudioClient(bool reconnect = false) + { + if (_audioClient == null || + _audioClient.ConnectionState != ConnectionState.Connected || + reconnect) + try + { + _audioClient = await VoiceChannel.ConnectAsync(); + } + catch + { + return null; + } + return _audioClient; + } + + public (bool Success, int Index) Enqueue(SongInfo song) + { + _log.Info("Adding song"); + Queue.Add(song); + return (true, Queue.Count); + } + + public void Next() + { + lock (locker) + { + Stopped = false; + Unpause(); + } + CancelCurrentSong(); + } + + public void Stop(bool clearQueue = false) + { + lock (locker) + { + Stopped = true; + Queue.ResetCurrent(); + if (clearQueue) + Queue.Clear(); + Unpause(); + } + CancelCurrentSong(); + } + + private void Unpause() + { + if (pauseTaskSource != null) + { + pauseTaskSource.TrySetResult(true); + pauseTaskSource = null; + } + } + + public void TogglePause() + { + lock (locker) + { + if (pauseTaskSource == null) + pauseTaskSource = new TaskCompletionSource(); + else + { + Unpause(); + } + } + OnPauseChanged?.Invoke(this, pauseTaskSource != null); + } + + public void SetVolume(int volume) + { + if (volume < 0 || volume > 100) + throw new ArgumentOutOfRangeException(nameof(volume)); + Volume = ((float)volume) / 100; + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + var cur = Queue.Current; + if (cur.Index == index) + Next(); + return Queue.RemoveAt(index); + } + } + + private void CancelCurrentSong() + { + lock (locker) + { + var cs = SongCancelSource; + SongCancelSource = new CancellationTokenSource(); + cs.Cancel(); + } + } + + public void ClearQueue() + { + lock (locker) + { + Queue.Clear(); + } + } + + public (int CurrentIndex, SongInfo[] Songs) QueueArray() + => Queue.ToArray(); + + //aidiakapi ftw + public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) + { + if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; + + // 16-bit precision for the multiplication + var volumeFixed = (int)Math.Round(volume * 65536d); + + var count = audioSamples.Length / 2; + + fixed (byte* srcBytes = audioSamples) + { + var src = (short*)srcBytes; + + for (var i = count; i != 0; i--, src++) + *src = (short)(((*src) * volumeFixed) >> 16); + } + + return audioSamples; + } + + public bool ToggleRepeatSong() + { + lock (locker) + { + return RepeatCurrentSong = !RepeatCurrentSong; + } + } + + public void Dispose() + { + _log.Info("Disposing"); + lock (locker) + { + Exited = true; + Unpause(); + } + CancelCurrentSong(); + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; + } + + + //private IAudioClient AudioClient { get; set; } + + ///// + ///// Player will prioritize different queuer name + ///// over the song position in the playlist + ///// + //public bool FairPlay { get; set; } = false; + + ///// + ///// Song will stop playing after this amount of time. + ///// To prevent people queueing radio or looped songs + ///// while other people want to listen to other songs too. + ///// + //public uint MaxPlaytimeSeconds { get; set; } = 0; + + + //// this should be written better + //public TimeSpan TotalPlaytime => + // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? + // TimeSpan.MaxValue : + // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); + + ///// + ///// Users who recently got their music wish + ///// + //private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + + //private readonly List _playlist = new List(); + //private readonly Logger _log; + //private readonly IGoogleApiService _google; + + //public IReadOnlyCollection Playlist => _playlist; + + //public Song CurrentSong { get; private set; } + //public CancellationTokenSource SongCancelSource { get; private set; } + //private CancellationToken CancelToken { get; set; } + + //public bool Paused { get; set; } + + //public float Volume { get; private set; } + + //public event Action OnCompleted = delegate { }; + //public event Action OnStarted = delegate { }; + //public event Action OnPauseChanged = delegate { }; + + //public IVoiceChannel PlaybackVoiceChannel { get; private set; } + //public ITextChannel OutputTextChannel { get; set; } + + //private bool Destroyed { get; set; } + //public bool RepeatSong { get; private set; } + //public bool RepeatPlaylist { get; private set; } + //public bool Autoplay { get; set; } + //public uint MaxQueueSize { get; set; } = 0; + + //private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); + + //public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; + + //public event Action SongRemoved = delegate { }; + + //public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) + //{ + // _log = LogManager.GetCurrentClassLogger(); + // _google = google; + + // OutputTextChannel = outputChannel; + // Volume = defaultVolume ?? 1.0f; + + // PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + + // Task.Run(async () => + // { + // try + // { + // while (!Destroyed) + // { + // try + // { + // if (ActionQueue.TryDequeue(out Action action)) + // { + // action(); + // } + // } + // finally + // { + // await Task.Delay(100).ConfigureAwait(false); + // } + // } + // } + // catch (Exception ex) + // { + // _log.Warn("Action queue crashed"); + // _log.Warn(ex); + // } + // }).ConfigureAwait(false); + + // var t = new Thread(async () => + // { + // while (!Destroyed) + // { + // try + // { + // CurrentSong = GetNextSong(); + + // if (CurrentSong == null) + // continue; + + // while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || + // AudioClient?.ConnectionState == ConnectionState.Connecting) + // { + // _log.Info("Waiting for Audio client"); + // await Task.Delay(200).ConfigureAwait(false); + // } + + // if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) + // AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); + + // var index = _playlist.IndexOf(CurrentSong); + // if (index != -1) + // RemoveSongAt(index, true); + + // OnStarted(this, CurrentSong); + // try + // { + // await CurrentSong.Play(AudioClient, CancelToken); + // } + // catch (OperationCanceledException) + // { + // } + // finally + // { + // OnCompleted(this, CurrentSong); + // } + + + // if (RepeatPlaylist & !RepeatSong) + // AddSong(CurrentSong, CurrentSong.QueuerName); + + // if (RepeatSong) + // AddSong(CurrentSong, 0); + + // } + // catch (Exception ex) + // { + // _log.Warn("Music thread almost crashed."); + // _log.Warn(ex); + // await Task.Delay(3000).ConfigureAwait(false); + // } + // finally + // { + // if (!CancelToken.IsCancellationRequested) + // { + // SongCancelSource.Cancel(); + // } + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + // CurrentSong = null; + // await Task.Delay(300).ConfigureAwait(false); + // } + // } + // }); + + // t.Start(); + //} + + //public void Next() + //{ + // ActionQueue.Enqueue(() => + // { + // Paused = false; + // SongCancelSource.Cancel(); + // }); + //} + + //public void Stop() + //{ + // ActionQueue.Enqueue(() => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // _playlist.Clear(); + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + //public void TogglePause() => OnPauseChanged(Paused = !Paused); + + //public int SetVolume(int volume) + //{ + // if (volume < 0) + // volume = 0; + // if (volume > 100) + // volume = 100; + + // Volume = volume / 100.0f; + // return volume; + //} + + //private Song GetNextSong() + //{ + // if (!FairPlay) + // { + // return _playlist.FirstOrDefault(); + // } + // var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) + // ?? _playlist.FirstOrDefault(); + + // if (song == null) + // return null; + + // if (RecentlyPlayedUsers.Contains(song.QueuerName)) + // { + // RecentlyPlayedUsers.Clear(); + // } + + // RecentlyPlayedUsers.Add(song.QueuerName); + // return song; + //} + + //public void Shuffle() + //{ + // ActionQueue.Enqueue(() => + // { + // var oldPlaylist = _playlist.ToArray(); + // _playlist.Clear(); + // _playlist.AddRange(oldPlaylist.Shuffle()); + // }); + //} + + //public void AddSong(Song s, string username) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ThrowIfQueueFull(); + // ActionQueue.Enqueue(() => + // { + // s.MusicPlayer = this; + // s.QueuerName = username.TrimTo(10); + // _playlist.Add(s); + // }); + //} + + //public void AddSong(Song s, int index) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Insert(index, s); + // }); + //} + + //public void RemoveSong(Song s) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Remove(s); + // }); + //} + + //public void RemoveSongAt(int index, bool silent = false) + //{ + // ActionQueue.Enqueue(() => + // { + // if (index < 0 || index >= _playlist.Count) + // return; + // var song = _playlist.ElementAtOrDefault(index); + // if (_playlist.Remove(song) && !silent) + // { + // SongRemoved(song, index); + // } + + // }); + //} + + //public void ClearQueue() + //{ + // ActionQueue.Enqueue(() => + // { + // _playlist.Clear(); + // }); + //} + + //public async Task UpdateSongDurationsAsync() + //{ + // var curSong = CurrentSong; + // var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && + // s.TotalTime == TimeSpan.Zero) + // .ToArray(); + // if (curSong != null) + // { + // Array.Resize(ref toUpdate, toUpdate.Length + 1); + // toUpdate[toUpdate.Length - 1] = curSong; + // } + // var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) + // .Distinct(); + + // var durations = await _google.GetVideoDurationsAsync(ids); + + // toUpdate.ForEach(s => + // { + // foreach (var kvp in durations) + // { + // if (s.SongInfo.Query.EndsWith(kvp.Key)) + // { + // s.TotalTime = kvp.Value; + // return; + // } + // } + // }); + //} + + //public void Destroy() + //{ + // ActionQueue.Enqueue(async () => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // Destroyed = true; + // _playlist.Clear(); + + // try { await AudioClient.StopAsync(); } catch { } + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + ////public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) + ////{ + //// if (audioClient?.ConnectionState != ConnectionState.Connected) + //// throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); + //// PlaybackVoiceChannel = voiceChannel; + //// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); + ////} + + //public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; + + //public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; + + //public bool ToggleAutoplay() => Autoplay = !Autoplay; + + //public void ThrowIfQueueFull() + //{ + // if (MaxQueueSize == 0) + // return; + // if (_playlist.Count >= MaxQueueSize) + // throw new PlaylistFullException(); + //} + } +} diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs new file mode 100644 index 00000000..50219dc8 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -0,0 +1,119 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class MusicQueue : IDisposable + { + private LinkedList Songs { get; } = new LinkedList(); + private int _currentIndex = 0; + private int CurrentIndex + { + get + { + return _currentIndex; + } + set + { + lock (locker) + { + if (Songs.Count == 0) + _currentIndex = 0; + else + _currentIndex = value %= Songs.Count; + } + } + } + public (int Index, SongInfo Song) Current + { + get + { + var cur = CurrentIndex; + return (cur, Songs.ElementAtOrDefault(cur)); + } + } + + private readonly object locker = new object(); + private TaskCompletionSource nextSource { get; } = new TaskCompletionSource(); + public int Count + { + get + { + lock (locker) + { + return Songs.Count; + } + } + } + + public void Add(SongInfo song) + { + lock (locker) + { + Songs.AddLast(song); + } + } + + public void Next() + { + CurrentIndex++; + } + + public void Dispose() + { + Clear(); + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + if (index < 0 || index >= Songs.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var current = Songs.First; + for (int i = 0; i < Songs.Count; i++) + { + if (i == index) + { + Songs.Remove(current); + if (CurrentIndex != 0) + { + if (CurrentIndex >= index) + { + --CurrentIndex; + } + } + break; + } + } + return current.Value; + } + } + + public void Clear() + { + lock (locker) + { + Songs.Clear(); + CurrentIndex = 0; + } + } + + public (int, SongInfo[]) ToArray() + { + lock (locker) + { + return (CurrentIndex, Songs.ToArray()); + } + } + + public void ResetCurrent() + { + CurrentIndex = 0; + } + } +} diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 9ea7a958..eb2a5016 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -11,6 +11,7 @@ using System.IO; using VideoLibrary; using System.Net.Http; using System.Collections.Generic; +using Discord.Commands; namespace NadekoBot.Services.Music { @@ -29,8 +30,8 @@ namespace NadekoBot.Services.Music public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, - NadekoStrings strings, ILocalization localization, DbService db, + public MusicService(IGoogleApiService google, + NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { _google = google; @@ -48,28 +49,49 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - public MusicPlayer GetPlayer(ulong guildId) + // public MusicPlayer GetPlayer(ulong guildId) + // { + // MusicPlayers.TryGetValue(guildId, out var player); + // return player; + // } + public float GetDefaultVolume(ulong guildId) { - MusicPlayers.TryGetValue(guildId, out var player); - return player; + return _defaultVolumes.GetOrAdd(guildId, (id) => + { + using (var uow = _db.UnitOfWork) + { + return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; + } + }); } - public MusicPlayer GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) + public Task GetOrCreatePlayer(ICommandContext context) + { + var gUsr = (IGuildUser)context.User; + var txtCh = (ITextChannel)context.Channel; + var vCh = gUsr.VoiceChannel; + return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh); + } + + public async Task GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) { string GetText(string text, params object[] replacements) => _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - return MusicPlayers.GetOrAdd(guildId, server => + if (voiceCh == null || voiceCh.Guild != textCh.Guild) { - var vol = _defaultVolumes.GetOrAdd(guildId, (id) => + if (textCh != null) { - using (var uow = _db.UnitOfWork) - { - return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; - } - }); - - var mp = new MusicPlayer(voiceCh, textCh, vol, _google); + await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); + } + throw new ArgumentException(nameof(voiceCh)); + } + + return MusicPlayers.GetOrAdd(guildId, _ => + { + var vol = GetDefaultVolume(guildId); + var mp = new MusicPlayer(this, voiceCh, textCh, vol); + IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; mp.OnCompleted += async (s, song) => @@ -91,30 +113,30 @@ namespace NadekoBot.Services.Music // ignored } - if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) - { - var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); - if (relatedVideos.Count > 0) - await QueueSong(await textCh.Guild.GetCurrentUserAsync(), - textCh, - voiceCh, - relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], - true).ConfigureAwait(false); - } + //todo autoplay should be independent from event handlers + //if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) + //{ + // var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); + // if (relatedVideos.Count > 0) + // await QueueSong(await textCh.Guild.GetCurrentUserAsync(), + // textCh, + // voiceCh, + // relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], + // true).ConfigureAwait(false); + //} } catch { // ignored } }; - mp.OnStarted += async (player, song) => { - try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } - catch - { - // ignored - } + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } + //catch + //{ + // // ignored + //} var sender = player; if (sender == null) return; @@ -125,7 +147,7 @@ namespace NadekoBot.Services.Music playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo))) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo))) .ConfigureAwait(false); } catch @@ -133,7 +155,7 @@ namespace NadekoBot.Services.Music // ignored } }; - mp.OnPauseChanged += async (paused) => + mp.OnPauseChanged += async (player, paused) => { try { @@ -150,291 +172,228 @@ namespace NadekoBot.Services.Music // ignored } }; + //mp.SongRemoved += async (song, index) => + //{ + // try + // { + // var embed = new EmbedBuilder() + // .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + // .WithDescription(song.PrettyName) + // .WithFooter(ef => ef.WithText(song.PrettyInfo)) + // .WithErrorColor(); - mp.SongRemoved += async (song, index) => - { - try - { - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo)) - .WithErrorColor(); + // await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - - } - catch - { - // ignored - } - }; + // } + // catch + // { + // // ignored + // } + //}; return mp; }); } - - public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { - string GetText(string text, params object[] replacements) => - _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + query.ThrowIfNull(nameof(query)); - if (voiceCh == null || voiceCh.Guild != textCh.Guild) - { - if (!silent) - await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); - throw new ArgumentNullException(nameof(voiceCh)); - } - if (string.IsNullOrWhiteSpace(query) || query.Length < 3) - throw new ArgumentException("Invalid song query.", nameof(query)); + SongInfo sinfo; - var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - Song resolvedSong; - try - { - musicPlayer.ThrowIfQueueFull(); - resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); + sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false); - if (resolvedSong == null) - throw new SongNotFoundException(); + return sinfo; + } - musicPlayer.AddSong(resolvedSong, queuer.Username); - } - catch (PlaylistFullException) + public async Task ResolveYoutubeSong(string query, string queuerName) + { + var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(link)) + throw new OperationCanceledException("Not a valid youtube query."); + var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 256) + .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."); + //var m = Regex.Match(query, @"\?t=(?\d*)"); + //int gotoTime = 0; + //if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new SongInfo { - try - { - await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); - } - catch - { - // ignored - } - throw; - } - if (!silent) - { - try - { - //var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) - .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") - .WithThumbnailUrl(resolvedSong.Thumbnail) - .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) - .ConfigureAwait(false); - queuedMessage?.DeleteAfter(10); - } - catch - { - // ignored - } // if queued message sending fails, don't attempt to delete it - } + Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + Provider = "YouTube", + Uri = await video.GetUriAsync().ConfigureAwait(false), + Query = link, + ProviderType = MusicType.Normal, + QueuerName = queuerName + }; + return song; } public void DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) - mp.Destroy(); + mp.Dispose(); } + // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + // { + // string GetText(string text, params object[] replacements) => + // _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); + //if (string.IsNullOrWhiteSpace(query) || query.Length< 3) + // throw new ArgumentException("Invalid song query.", nameof(query)); - if (musicType != MusicType.Local && IsRadioLink(query)) - { - musicType = MusicType.Radio; - query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; - } + // var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); + // Song resolvedSong; + // try + // { + // musicPlayer.ThrowIfQueueFull(); + // resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); - try - { - switch (musicType) - { - case MusicType.Local: - return new Song(new SongInfo - { - Uri = "\"" + Path.GetFullPath(query) + "\"", - Title = Path.GetFileNameWithoutExtension(query), - Provider = "Local File", - ProviderType = musicType, - Query = query, - }); - case MusicType.Radio: - return new Song(new SongInfo - { - Uri = query, - Title = $"{query}", - Provider = "Radio Stream", - ProviderType = musicType, - Query = query - }) - { TotalTime = TimeSpan.MaxValue }; - } - if (_sc.IsSoundCloudLink(query)) - { - var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = musicType, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // if (resolvedSong == null) + // throw new SongNotFoundException(); - if (musicType == MusicType.Soundcloud) - { - var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Soundcloud, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // musicPlayer.AddSong(resolvedSong, queuer.Username); + // } + // catch (PlaylistFullException) + // { + // try + // { + // await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); + // } + // catch + // { + // // ignored + // } + // throw; + // } + // if (!silent) + // { + // try + // { + // //var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + // var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() + // .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) + // .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") + // .WithThumbnailUrl(resolvedSong.Thumbnail) + // .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) + // .ConfigureAwait(false); + // queuedMessage?.DeleteAfter(10); + // } + // catch + // { + // // ignored + // } // if queued message sending fails, don't attempt to delete it + // } + // } - var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(link)) - throw new OperationCanceledException("Not a valid youtube query."); - var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 256) - .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."); - var m = Regex.Match(query, @"\?t=(?\d*)"); - int gotoTime = 0; - if (m.Captures.Count > 0) - int.TryParse(m.Groups["t"].ToString(), out gotoTime); - var song = new Song(new SongInfo - { - Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - Provider = "YouTube", - Uri = await video.GetUriAsync().ConfigureAwait(false), - Query = link, - ProviderType = musicType, - }); - song.SkipTo = gotoTime; - return song; - } - catch (Exception ex) - { - _log.Warn($"Failed resolving the link.{ex.Message}"); - _log.Warn(ex); - return null; - } - } - private async Task HandleStreamContainers(string query) - { - string file = null; - try - { - using (var http = new HttpClient()) - { - file = await http.GetStringAsync(query).ConfigureAwait(false); - } - } - catch - { - return query; - } - if (query.Contains(".pls")) - { - //File1=http://armitunes.com:8000/ - //Regex.Match(query) - try - { - var m = Regex.Match(file, "File1=(?.*?)\\n"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .pls:\n{file}"); - return null; - } - } - if (query.Contains(".m3u")) - { - /* -# This is a comment - C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - */ - try - { - var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .m3u:\n{file}"); - return null; - } - } - if (query.Contains(".asx")) - { - // - try - { - var m = Regex.Match(file, ".*?)\""); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .asx:\n{file}"); - return null; - } - } - if (query.Contains(".xspf")) - { - /* - - - - file:///mp3s/song_1.mp3 - */ - try - { - var m = Regex.Match(file, "(?.*?)"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .xspf:\n{file}"); - return null; - } - } - return query; - } - private bool IsRadioLink(string query) => - (query.StartsWith("http") || - query.StartsWith("ww")) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); + // private async Task HandleStreamContainers(string query) + // { + // string file = null; + // try + // { + // using (var http = new HttpClient()) + // { + // file = await http.GetStringAsync(query).ConfigureAwait(false); + // } + // } + // catch + // { + // return query; + // } + // if (query.Contains(".pls")) + // { + // //File1=http://armitunes.com:8000/ + // //Regex.Match(query) + // try + // { + // var m = Regex.Match(file, "File1=(?.*?)\\n"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .pls:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".m3u")) + // { + // /* + //# This is a comment + // C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + // C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .m3u:\n{file}"); + // return null; + // } + + // } + // if (query.Contains(".asx")) + // { + // // + // try + // { + // var m = Regex.Match(file, ".*?)\""); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .asx:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".xspf")) + // { + // /* + // + // + // + // file:///mp3s/song_1.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?.*?)"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .xspf:\n{file}"); + // return null; + // } + // } + + // return query; + // } + + // private bool IsRadioLink(string query) => + // (query.StartsWith("http") || + // query.StartsWith("ww")) + // && + // (query.Contains(".pls") || + // query.Contains(".m3u") || + // query.Contains(".asx") || + // query.Contains(".xspf")); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 187b3b6e..074238d6 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -1,296 +1,246 @@ -ο»Ώusing Discord.Audio; -using NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +ο»Ώusing NadekoBot.Extensions; using System.Net; using Discord; using NadekoBot.Services.Database.Models; +using System; namespace NadekoBot.Services.Music { - public class SongInfo - { - public string Provider { get; set; } - public MusicType ProviderType { get; set; } - public string Query { get; set; } - public string Title { get; set; } - public string Uri { get; set; } - public string AlbumArt { get; set; } - } + //public class Song + //{ + // public SongInfo SongInfo { get; } + // public MusicPlayer MusicPlayer { get; set; } - public class Song - { - public SongInfo SongInfo { get; } - public MusicPlayer MusicPlayer { get; set; } + // private string _queuerName; + // public string QueuerName { get{ + // return Format.Sanitize(_queuerName); + // } set { _queuerName = value; } } - private string _queuerName; - public string QueuerName { get{ - return Format.Sanitize(_queuerName); - } set { _queuerName = value; } } + // public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; + // public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); - public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; - public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); + // private const int _milliseconds = 20; + // private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; + // private const int _frameBytes = 3840; //16-bit, 2 channels - private const int _milliseconds = 20; - private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; - private const int _frameBytes = 3840; //16-bit, 2 channels + // private ulong BytesSent { get; set; } - private ulong BytesSent { get; set; } + // //pwetty - //pwetty + // public string PrettyProvider => + // $"{(SongInfo.Provider ?? "???")}"; - public string PrettyProvider => - $"{(SongInfo.Provider ?? "???")}"; + // public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; - public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + // public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; - public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; + // public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; - public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + // public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; - public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; + // public string PrettyCurrentTime { + // get { + // var time = CurrentTime.ToString(@"mm\:ss"); + // var hrs = (int)CurrentTime.TotalHours; - public string PrettyCurrentTime { - get { - var time = CurrentTime.ToString(@"mm\:ss"); - var hrs = (int)CurrentTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // else + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - else - return time; - } - } + // public string PrettyTotalTime { + // get + // { + // if (TotalTime == TimeSpan.Zero) + // return "(?)"; + // if (TotalTime == TimeSpan.MaxValue) + // return "∞"; + // var time = TotalTime.ToString(@"mm\:ss"); + // var hrs = (int)TotalTime.TotalHours; - public string PrettyTotalTime { - get - { - if (TotalTime == TimeSpan.Zero) - return "(?)"; - if (TotalTime == TimeSpan.MaxValue) - return "∞"; - var time = TotalTime.ToString(@"mm\:ss"); - var hrs = (int)TotalTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - return time; - } - } + // public string Thumbnail { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Radio: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + // case MusicType.Normal: + // //todo 50 have videoid in songinfo from the start + // var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); + // return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + // case MusicType.Local: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + // case MusicType.Soundcloud: + // return SongInfo.AlbumArt; + // default: + // return ""; + // } + // } + // } - public string Thumbnail { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Radio: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.Normal: - //todo 50 have videoid in songinfo from the start - var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); - return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; - case MusicType.Local: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links - case MusicType.Soundcloud: - return SongInfo.AlbumArt; - default: - return ""; - } - } - } + // public string SongUrl { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Normal: + // return SongInfo.Query; + // case MusicType.Soundcloud: + // return SongInfo.Query; + // case MusicType.Local: + // return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; + // case MusicType.Radio: + // return $"https://google.com/search?q={SongInfo.Title}"; + // default: + // return ""; + // } + // } + // } - public string SongUrl { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Normal: - return SongInfo.Query; - case MusicType.Soundcloud: - return SongInfo.Query; - case MusicType.Local: - return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; - case MusicType.Radio: - return $"https://google.com/search?q={SongInfo.Title}"; - default: - return ""; - } - } - } + // private readonly Logger _log; - public int SkipTo { get; set; } + // public Song(SongInfo songInfo) + // { + // SongInfo = songInfo; + // _log = LogManager.GetCurrentClassLogger(); + // } - private readonly Logger _log; + // public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) + // { + // BytesSent = (ulong) SkipTo * 3840 * 50; + // var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); - public Song(SongInfo songInfo) - { - SongInfo = songInfo; - _log = LogManager.GetCurrentClassLogger(); - } + // var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); + // var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); - public Song Clone() - { - var s = new Song(SongInfo) - { - MusicPlayer = MusicPlayer, - QueuerName = QueuerName - }; - return s; - } + // try + // { + // var attempt = 0; - public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) - { - BytesSent = (ulong) SkipTo * 3840 * 50; - var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); + // var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy + // var finished = false; + // var count = 0; + // var sw = new Stopwatch(); + // var slowconnection = false; + // sw.Start(); + // while (!finished) + // { + // var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); + // if (t != prebufferingTask) + // { + // count++; + // if (count == 10) + // { + // slowconnection = true; + // prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); + // _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); + // continue; + // } - var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); - var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); + // if (inStream.BufferingCompleted && count == 1) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // else + // { + // continue; + // } + // } + // else if (prebufferingTask.IsCanceled) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // finished = true; + // } + // sw.Stop(); + // _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); - try - { - var attempt = 0; + // var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); - var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy - var finished = false; - var count = 0; - var sw = new Stopwatch(); - var slowconnection = false; - sw.Start(); - while (!finished) - { - var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); - if (t != prebufferingTask) - { - count++; - if (count == 10) - { - slowconnection = true; - prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); - _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); - continue; - } + // int nextTime = Environment.TickCount + _milliseconds; - if (inStream.BufferingCompleted && count == 1) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - else - { - continue; - } - } - else if (prebufferingTask.IsCanceled) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - finished = true; - } - sw.Stop(); - _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); + // byte[] buffer = new byte[_frameBytes]; + // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason + // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime + // { + // //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); + // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + // //await inStream.CopyToAsync(voiceClient.OutputStream); + // if (read < _frameBytes) + // _log.Debug("read {0}", read); + // unchecked + // { + // BytesSent += (ulong)read; + // } + // if (read < _frameBytes) + // { + // if (read == 0) + // { + // if (inStream.BufferingCompleted) + // break; + // if (attempt++ == 20) + // { + // MusicPlayer.SongCancelSource.Cancel(); + // break; + // } + // if (slowconnection) + // { + // _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); + // await Task.Delay(1000, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // else + // { + // await Task.Delay(100, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // } + // else + // attempt = 0; + // } + // else + // attempt = 0; - int nextTime = Environment.TickCount + _milliseconds; - - byte[] buffer = new byte[_frameBytes]; - while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason - !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime - { - //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); - var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - //await inStream.CopyToAsync(voiceClient.OutputStream); - if (read < _frameBytes) - _log.Debug("read {0}", read); - unchecked - { - BytesSent += (ulong)read; - } - if (read < _frameBytes) - { - if (read == 0) - { - if (inStream.BufferingCompleted) - break; - if (attempt++ == 20) - { - MusicPlayer.SongCancelSource.Cancel(); - break; - } - if (slowconnection) - { - _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - - await Task.Delay(1000, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - else - { - await Task.Delay(100, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - } - else - attempt = 0; - } - else - attempt = 0; - - while (MusicPlayer.Paused) - { - await Task.Delay(200, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } + // while (MusicPlayer.Paused) + // { + // await Task.Delay(200, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } - buffer = AdjustVolume(buffer, MusicPlayer.Volume); - if (read != _frameBytes) continue; - nextTime = unchecked(nextTime + _milliseconds); - int delayMillis = unchecked(nextTime - Environment.TickCount); - if (delayMillis > 0) - await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); - await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); - } - } - finally - { - await bufferTask; - inStream.Dispose(); - } - } + // buffer = AdjustVolume(buffer, MusicPlayer.Volume); + // if (read != _frameBytes) continue; + // nextTime = unchecked(nextTime + _milliseconds); + // int delayMillis = unchecked(nextTime - Environment.TickCount); + // if (delayMillis > 0) + // await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); + // await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); + // } + // } + // finally + // { + // await bufferTask; + // inStream.Dispose(); + // } + // } - private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) - { - while (!inStream.BufferingCompleted && inStream.Length < size) - { - await Task.Delay(100, cancelToken); - } - _log.Debug("Buffering successfull"); - } - - //aidiakapi ftw - public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; - - // 16-bit precision for the multiplication - var volumeFixed = (int)Math.Round(volume * 65536d); - - var count = audioSamples.Length / 2; - - fixed (byte* srcBytes = audioSamples) - { - var src = (short*)srcBytes; - - for (var i = count; i != 0; i--, src++) - *src = (short)(((*src) * volumeFixed) >> 16); - } - - return audioSamples; - } - } + // private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) + // { + // while (!inStream.BufferingCompleted && inStream.Length < size) + // { + // await Task.Delay(100, cancelToken); + // } + // _log.Debug("Buffering successfull"); + // } + //} } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index ff3a0ed2..f11c2cce 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,219 +1,219 @@ -ο»Ώusing NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +ο»Ώ//using NadekoBot.Extensions; +//using NLog; +//using System; +//using System.Diagnostics; +//using System.IO; +//using System.Threading; +//using System.Threading.Tasks; -namespace NadekoBot.Services.Music -{ - /// - /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. - /// It also help for large music by deleting files that are already seen. - /// - class SongBuffer : Stream - { - public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) - { - MusicPlayer = musicPlayer; - Basename = basename; - SongInfo = songInfo; - SkipTo = skipTo; - MaxFileSize = maxFileSize; - CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - _log = LogManager.GetCurrentClassLogger(); - } +//namespace NadekoBot.Services.Music +//{ +// /// +// /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. +// /// It also help for large music by deleting files that are already seen. +// /// +// class SongBuffer : Stream +// { +// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) +// { +// MusicPlayer = musicPlayer; +// Basename = basename; +// SongInfo = songInfo; +// SkipTo = skipTo; +// MaxFileSize = maxFileSize; +// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// _log = LogManager.GetCurrentClassLogger(); +// } - MusicPlayer MusicPlayer { get; } +// MusicPlayer MusicPlayer { get; } - private string Basename { get; } +// private string Basename { get; } - private SongInfo SongInfo { get; } +// private SongInfo SongInfo { get; } - private int SkipTo { get; } +// private int SkipTo { get; } - private int MaxFileSize { get; } = 2.MiB(); +// private int MaxFileSize { get; } = 2.MiB(); - private long FileNumber = -1; +// private long FileNumber = -1; - private long NextFileToRead = 0; +// private long NextFileToRead = 0; - public bool BufferingCompleted { get; private set; } = false; +// public bool BufferingCompleted { get; private set; } = false; - private ulong CurrentBufferSize = 0; +// private ulong CurrentBufferSize = 0; - private FileStream CurrentFileStream; - private Logger _log; +// private FileStream CurrentFileStream; +// private Logger _log; - public Task BufferSong(CancellationToken cancelToken) => - Task.Run(async () => - { - Process p = null; - FileStream outStream = null; - try - { - p = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = false, - CreateNoWindow = true, - }); +// public Task BufferSong(CancellationToken cancelToken) => +// Task.Run(async () => +// { +// Process p = null; +// FileStream outStream = null; +// try +// { +// p = Process.Start(new ProcessStartInfo +// { +// FileName = "ffmpeg", +// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", +// UseShellExecute = false, +// RedirectStandardOutput = true, +// RedirectStandardError = false, +// CreateNoWindow = true, +// }); - byte[] buffer = new byte[81920]; - int currentFileSize = 0; - ulong prebufferSize = 100ul.MiB(); +// byte[] buffer = new byte[81920]; +// int currentFileSize = 0; +// ulong prebufferSize = 100ul.MiB(); - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - while (!p.HasExited) //Also fix low bandwidth - { - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); - if (currentFileSize >= MaxFileSize) - { - try - { - outStream.Dispose(); - } - catch { } - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - currentFileSize = bytesRead; - } - else - { - currentFileSize += bytesRead; - } - CurrentBufferSize += Convert.ToUInt64(bytesRead); - await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - while (CurrentBufferSize > prebufferSize) - await Task.Delay(100, cancelToken); - } - BufferingCompleted = true; - } - catch (System.ComponentModel.Win32Exception) - { - var oldclr = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(@"You have not properly installed or configured FFMPEG. -Please install and configure FFMPEG to play music. -Check the guides for your platform on how to setup ffmpeg correctly: - Windows Guide: https://goo.gl/OjKk8F - Linux Guide: https://goo.gl/ShjCUo"); - Console.ForegroundColor = oldclr; - } - catch (Exception ex) - { - Console.WriteLine($"Buffering stopped: {ex.Message}"); - } - finally - { - if (outStream != null) - outStream.Dispose(); - Console.WriteLine($"Buffering done."); - if (p != null) - { - try - { - p.Kill(); - } - catch { } - p.Dispose(); - } - } - }); +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// while (!p.HasExited) //Also fix low bandwidth +// { +// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); +// if (currentFileSize >= MaxFileSize) +// { +// try +// { +// outStream.Dispose(); +// } +// catch { } +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// currentFileSize = bytesRead; +// } +// else +// { +// currentFileSize += bytesRead; +// } +// CurrentBufferSize += Convert.ToUInt64(bytesRead); +// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); +// while (CurrentBufferSize > prebufferSize) +// await Task.Delay(100, cancelToken); +// } +// BufferingCompleted = true; +// } +// catch (System.ComponentModel.Win32Exception) +// { +// var oldclr = Console.ForegroundColor; +// Console.ForegroundColor = ConsoleColor.Red; +// Console.WriteLine(@"You have not properly installed or configured FFMPEG. +//Please install and configure FFMPEG to play music. +//Check the guides for your platform on how to setup ffmpeg correctly: +// Windows Guide: https://goo.gl/OjKk8F +// Linux Guide: https://goo.gl/ShjCUo"); +// Console.ForegroundColor = oldclr; +// } +// catch (Exception ex) +// { +// Console.WriteLine($"Buffering stopped: {ex.Message}"); +// } +// finally +// { +// if (outStream != null) +// outStream.Dispose(); +// Console.WriteLine($"Buffering done."); +// if (p != null) +// { +// try +// { +// p.Kill(); +// } +// catch { } +// p.Dispose(); +// } +// } +// }); - /// - /// Return the next file to read, and delete the old one - /// - /// Name of the file to read - private string GetNextFile() - { - string filename = Basename + "-" + NextFileToRead; +// /// +// /// Return the next file to read, and delete the old one +// /// +// /// Name of the file to read +// private string GetNextFile() +// { +// string filename = Basename + "-" + NextFileToRead; - if (NextFileToRead != 0) - { - try - { - CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); - File.Delete(Basename + "-" + (NextFileToRead - 1)); - } - catch { } - } - NextFileToRead++; - return filename; - } +// if (NextFileToRead != 0) +// { +// try +// { +// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); +// File.Delete(Basename + "-" + (NextFileToRead - 1)); +// } +// catch { } +// } +// NextFileToRead++; +// return filename; +// } - private bool IsNextFileReady() - { - return NextFileToRead <= FileNumber; - } +// private bool IsNextFileReady() +// { +// return NextFileToRead <= FileNumber; +// } - private void CleanFiles() - { - for (long i = NextFileToRead - 1; i <= FileNumber; i++) - { - try - { - File.Delete(Basename + "-" + i); - } - catch { } - } - } +// private void CleanFiles() +// { +// for (long i = NextFileToRead - 1; i <= FileNumber; i++) +// { +// try +// { +// File.Delete(Basename + "-" + i); +// } +// catch { } +// } +// } - //Stream part +// //Stream part - public override bool CanRead => true; +// public override bool CanRead => true; - public override bool CanSeek => false; +// public override bool CanSeek => false; - public override bool CanWrite => false; +// public override bool CanWrite => false; - public override long Length => (long)CurrentBufferSize; +// public override long Length => (long)CurrentBufferSize; - public override long Position { get; set; } = 0; +// public override long Position { get; set; } = 0; - public override void Flush() { } +// public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) - { - int read = CurrentFileStream.Read(buffer, offset, count); - if (read < count) - { - if (!BufferingCompleted || IsNextFileReady()) - { - CurrentFileStream.Dispose(); - CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - read += CurrentFileStream.Read(buffer, read + offset, count - read); - } - if (read < count) - Array.Clear(buffer, read, count - read); - } - return read; - } +// public override int Read(byte[] buffer, int offset, int count) +// { +// int read = CurrentFileStream.Read(buffer, offset, count); +// if (read < count) +// { +// if (!BufferingCompleted || IsNextFileReady()) +// { +// CurrentFileStream.Dispose(); +// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// read += CurrentFileStream.Read(buffer, read + offset, count - read); +// } +// if (read < count) +// Array.Clear(buffer, read, count - read); +// } +// return read; +// } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } +// public override long Seek(long offset, SeekOrigin origin) +// { +// throw new NotImplementedException(); +// } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } +// public override void SetLength(long value) +// { +// throw new NotImplementedException(); +// } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } +// public override void Write(byte[] buffer, int offset, int count) +// { +// throw new NotImplementedException(); +// } - public new void Dispose() - { - CurrentFileStream.Dispose(); - MusicPlayer.SongCancelSource.Cancel(); - CleanFiles(); - base.Dispose(); - } - } -} \ No newline at end of file +// public new void Dispose() +// { +// CurrentFileStream.Dispose(); +// MusicPlayer.SongCancelSource.Cancel(); +// CleanFiles(); +// base.Dispose(); +// } +// } +//} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs new file mode 100644 index 00000000..4ad63b4a --- /dev/null +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -0,0 +1,85 @@ +ο»Ώusing Discord; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using System; +using System.Net; +using System.Text.RegularExpressions; + +namespace NadekoBot.Services.Music +{ + public class SongInfo + { + public string Provider { get; set; } + public MusicType ProviderType { get; set; } + public string Query { get; set; } + public string Title { get; set; } + public string Uri { get; set; } + public string AlbumArt { get; set; } + public string QueuerName { get; set; } + public TimeSpan TotalTime = TimeSpan.Zero; + + public string PrettyProvider => (Provider ?? "???"); + //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**"; + public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`"; + public string PrettyTotalTime + { + get + { + if (TotalTime == TimeSpan.Zero) + return "(?)"; + if (TotalTime == TimeSpan.MaxValue) + return "∞"; + var time = TotalTime.ToString(@"mm\:ss"); + var hrs = (int)TotalTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + return time; + } + } + + public string SongUrl + { + get + { + switch (ProviderType) + { + case MusicType.Normal: + return Query; + case MusicType.Soundcloud: + return Query; + case MusicType.Local: + return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }"; + case MusicType.Radio: + return $"https://google.com/search?q={Title}"; + default: + return ""; + } + } + } + private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); + public string Thumbnail + { + get + { + switch (ProviderType) + { + case MusicType.Radio: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + case MusicType.Normal: + //todo have videoid in songinfo from the start + var videoId = videoIdRegex.Match(Query); + return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + case MusicType.Local: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + case MusicType.Soundcloud: + return AlbumArt; + default: + return ""; + } + } + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver.cs b/src/NadekoBot/Services/Music/SongResolver.cs new file mode 100644 index 00000000..06fe5194 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver.cs @@ -0,0 +1,111 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class SongResolver + { + // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) + // { + // if (string.IsNullOrWhiteSpace(query)) + // throw new ArgumentNullException(nameof(query)); + + // if (musicType != MusicType.Local && IsRadioLink(query)) + // { + // musicType = MusicType.Radio; + // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; + // } + + // try + // { + // switch (musicType) + // { + // case MusicType.Local: + // return new Song(new SongInfo + // { + // Uri = "\"" + Path.GetFullPath(query) + "\"", + // Title = Path.GetFileNameWithoutExtension(query), + // Provider = "Local File", + // ProviderType = musicType, + // Query = query, + // }); + // case MusicType.Radio: + // return new Song(new SongInfo + // { + // Uri = query, + // Title = $"{query}", + // Provider = "Radio Stream", + // ProviderType = musicType, + // Query = query + // }) + // { TotalTime = TimeSpan.MaxValue }; + // } + // if (_sc.IsSoundCloudLink(query)) + // { + // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = musicType, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // if (musicType == MusicType.Soundcloud) + // { + // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Soundcloud, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + // if (string.IsNullOrWhiteSpace(link)) + // throw new OperationCanceledException("Not a valid youtube query."); + // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + // var video = videos + // .Where(v => v.AudioBitrate < 256) + // .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."); + // var m = Regex.Match(query, @"\?t=(?\d*)"); + // int gotoTime = 0; + // if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + // var song = new Song(new SongInfo + // { + // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + // Provider = "YouTube", + // Uri = await video.GetUriAsync().ConfigureAwait(false), + // Query = link, + // ProviderType = musicType, + // }); + // song.SkipTo = gotoTime; + // return song; + // } + // catch (Exception ex) + // { + // _log.Warn($"Failed resolving the link.{ex.Message}"); + // _log.Warn(ex); + // return null; + // } + // } + } +} From f8ad6dda507ea0b7d173a218ec226707d450521c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 08:16:06 +0200 Subject: [PATCH 02/40] small changes --- src/NadekoBot/Modules/Music/Music.cs | 5 ++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 27 +++++++++++++------- src/NadekoBot/Services/Music/MusicQueue.cs | 5 +++- src/NadekoBot/Services/Music/MusicService.cs | 6 ++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 472acd17..a8b7ae0a 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -253,10 +253,9 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public Task Destroy() + public async Task Destroy() { - _music.DestroyPlayer(Context.Guild.Id); - return Task.CompletedTask; + await _music.DestroyPlayer(Context.Guild.Id); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 64c3aa22..b4bc4009 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -15,7 +15,7 @@ namespace NadekoBot.Services.Music Playing, Completed } - public class MusicPlayer : IDisposable + public class MusicPlayer { private readonly Task _player; private readonly IVoiceChannel VoiceChannel; @@ -92,7 +92,12 @@ namespace NadekoBot.Services.Music }); var ac = await GetAudioClient(); if (ac == null) + { + await Task.Delay(900); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts continue; + } var pcm = ac.CreatePCMStream(AudioApplication.Music); OnStarted?.Invoke(this, data.Song); @@ -175,8 +180,8 @@ namespace NadekoBot.Services.Music { Stopped = false; Unpause(); + CancelCurrentSong(); } - CancelCurrentSong(); } public void Stop(bool clearQueue = false) @@ -188,8 +193,8 @@ namespace NadekoBot.Services.Music if (clearQueue) Queue.Clear(); Unpause(); + CancelCurrentSong(); } - CancelCurrentSong(); } private void Unpause() @@ -283,18 +288,22 @@ namespace NadekoBot.Services.Music } } - public void Dispose() + public async Task Destroy() { - _log.Info("Disposing"); + _log.Info("Destroying"); lock (locker) { + Stop(); Exited = true; Unpause(); + + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; } - CancelCurrentSong(); - OnCompleted = null; - OnPauseChanged = null; - OnStarted = null; + var ac = _audioClient; + if (ac != null) + await ac.StopAsync(); } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 50219dc8..cf3a0818 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -113,7 +113,10 @@ namespace NadekoBot.Services.Music public void ResetCurrent() { - CurrentIndex = 0; + lock (locker) + { + CurrentIndex = 0; + } } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index eb2a5016..79d787fc 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using Discord; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; -using System.Text.RegularExpressions; using NLog; using System.IO; using VideoLibrary; -using System.Net.Http; using System.Collections.Generic; using Discord.Commands; @@ -235,10 +233,10 @@ namespace NadekoBot.Services.Music return song; } - public void DestroyPlayer(ulong id) + public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) - mp.Dispose(); + await mp.Destroy(); } // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) From 9889baf8bd977f9b7c4f8b34a505ba86750075e6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 17:16:03 +0200 Subject: [PATCH 03/40] Poopy buffer is back ^_^ Music lag fixes... --- .../DataStructures/PoopyRingBuffer.cs | 145 ++++++++++++++++++ src/NadekoBot/Modules/Music/Music.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 103 +++++++------ src/NadekoBot/Services/Music/MusicService.cs | 5 - src/NadekoBot/Services/Music/SongBuffer.cs | 83 +++++++++- 5 files changed, 277 insertions(+), 60 deletions(-) create mode 100644 src/NadekoBot/DataStructures/PoopyRingBuffer.cs diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs new file mode 100644 index 00000000..4f9db9be --- /dev/null +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -0,0 +1,145 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + public class PoopyRingBuffer : IDisposable + { + // readpos == writepos means empty + // writepos == readpos - 1 means full + + private readonly byte[] buffer; + private readonly object posLock = new object(); + public int Capacity { get; } + + private volatile int _readPos = 0; + private int ReadPos + { + get => _readPos; + set { lock (posLock) _readPos = value; } + } + private volatile int _writePos = 0; + private int WritePos + { + get => _writePos; + set { lock (posLock) _writePos = value; } + } + private int Length + { + get + { + lock (posLock) + { + return ReadPos <= WritePos ? + WritePos - ReadPos : + Capacity - (ReadPos - WritePos); + } + } + } + + public int RemainingCapacity + { + get { lock (posLock) return Capacity - Length - 1; } + } + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + public PoopyRingBuffer(int capacity = 38400) + { + this.Capacity = capacity + 1; + this.buffer = new byte[this.Capacity]; + } + + public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) => Task.Run(async () => + { + await _locker.WaitAsync(cancelToken); + try + { + Console.WriteLine("Reading {0}", toRead); + if (WritePos == ReadPos) + return 0; + + if (toRead > Length) + toRead = Length; + + if (WritePos > ReadPos) + { + Buffer.BlockCopy(buffer, ReadPos, b, offset, toRead); + ReadPos += toRead; + } + else + { + var toEnd = Capacity - ReadPos; + var firstRead = toRead > toEnd ? + toEnd : + toRead; + Buffer.BlockCopy(buffer, ReadPos, b, offset, firstRead); + ReadPos += firstRead; + var secondRead = toRead - firstRead; + if (secondRead > 0) + { + Buffer.BlockCopy(buffer, 0, b, offset + firstRead, secondRead); + ReadPos = secondRead; + } + } + Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + return toRead; + } + finally + { + _locker.Release(); + } + }); + + public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => + { + while (toWrite > RemainingCapacity) + await Task.Delay(100, cancelToken); + + await _locker.WaitAsync(cancelToken); + try + { + Console.WriteLine("Writing {0}", toWrite); + if (WritePos < ReadPos) + { + Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); + WritePos += toWrite; + } + else + { + var toEnd = Capacity - WritePos; + var firstWrite = toWrite > toEnd ? + toEnd : + toWrite; + Buffer.BlockCopy(b, offset, buffer, WritePos, firstWrite); + var secondWrite = toWrite - firstWrite; + if (secondWrite > 0) + { + Buffer.BlockCopy(b, offset + firstWrite, buffer, 0, secondWrite); + WritePos = secondWrite; + } + else + { + WritePos += firstWrite; + if (WritePos == Capacity) + WritePos = 0; + } + } + Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + return toWrite; + } + finally + { + _locker.Release(); + } + }); + + public void Dispose() + { + } + } +} diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a8b7ae0a..3681d631 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -451,6 +451,7 @@ namespace NadekoBot.Modules.Music { try { + await Task.Yield(); //todo fix for all if (item.ProviderType == MusicType.Normal) await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b4bc4009..97581b2f 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -81,59 +81,58 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); - var p = Process.Start(new ProcessStartInfo + using (var b = new SongBuffer(data.Song.Uri, "")) { - FileName = "ffmpeg", - Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = false, - CreateNoWindow = true, - }); - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - var pcm = ac.CreatePCMStream(AudioApplication.Music); + var bufferSuccess = await b.StartBuffering(cancelToken); - OnStarted?.Invoke(this, data.Song); + if (bufferSuccess == false) + continue; - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try - { - while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + var ac = await GetAudioClient(); + if (ac == null) { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken); - - await (pauseTaskSource?.Task ?? Task.CompletedTask); + await Task.Delay(900); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + var pcm = ac.CreatePCMStream(AudioApplication.Music); - OnCompleted?.Invoke(this, data.Song); + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try + { + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + + OnCompleted?.Invoke(this, data.Song); + } } } finally @@ -141,7 +140,7 @@ namespace NadekoBot.Services.Music _log.Info("Next song"); do { - await Task.Delay(100); + await Task.Delay(500); } while (Stopped && !Exited); if(!RepeatCurrentSong) @@ -158,6 +157,14 @@ namespace NadekoBot.Services.Music reconnect) try { + try + { + await _audioClient?.StopAsync(); + _audioClient?.Dispose(); + } + catch + { + } _audioClient = await VoiceChannel.ConnectAsync(); } catch diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 79d787fc..986fd0fd 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -47,11 +47,6 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - // public MusicPlayer GetPlayer(ulong guildId) - // { - // MusicPlayers.TryGetValue(guildId, out var player); - // return player; - // } public float GetDefaultVolume(ulong guildId) { return _defaultVolumes.GetOrAdd(guildId, (id) => diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f11c2cce..c5c08175 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,10 +1,79 @@ -ο»Ώ//using NadekoBot.Extensions; -//using NLog; -//using System; -//using System.Diagnostics; -//using System.IO; -//using System.Threading; -//using System.Threading.Tasks; +ο»Ώusing NadekoBot.DataStructures; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class SongBuffer : IDisposable + { + const int maxReadSize = 3840; + private Process p; + private PoopyRingBuffer _outStream = new PoopyRingBuffer(); + + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + + public string SongUri { get; private set; } + + public SongBuffer(string songUri, string skipTo) + { + this.SongUri = songUri; + + this.p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + } + + public Task StartBuffering(CancellationToken cancelToken) + { + var toReturn = new TaskCompletionSource(); + var _ = Task.Run(async () => + { + try + { + byte[] buffer = new byte[3840]; + while (!this.p.HasExited || cancelToken.IsCancellationRequested) + { + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); + + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) + toReturn.TrySetResult(true); + } + } + catch + { + toReturn.TrySetResult(false); + //ignored + } + }, cancelToken); + + return toReturn.Task; + } + + public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) + { + return _outStream.ReadAsync(b, offset, toRead, cancelToken); + } + + public void Dispose() + { + try { this.p.Kill(); } + catch { } + _outStream.Dispose(); + this.p.Dispose(); + } + } +} //namespace NadekoBot.Services.Music //{ From 3731994061391e4caf134e427e5b5da20813b2d2 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 21:22:11 +0200 Subject: [PATCH 04/40] Lot more work, fixes, addition, untested new implementations... --- .../DataStructures/PoopyRingBuffer.cs | 2 +- src/NadekoBot/Modules/Music/Music.cs | 139 +++++++++--------- src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 136 ++++++++++++----- src/NadekoBot/Services/Music/MusicQueue.cs | 30 +++- src/NadekoBot/Services/Music/MusicService.cs | 24 ++- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- src/NadekoBot/Services/Music/SongInfo.cs | 1 + .../_strings/ResponseStrings.en-US.json | 4 +- 9 files changed, 229 insertions(+), 111 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 4f9db9be..4c8764db 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -48,7 +48,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 38400) + public PoopyRingBuffer(int capacity = 3640 * 200) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 3681d631..c82e59cc 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -111,11 +111,15 @@ namespace NadekoBot.Modules.Music } } } + //todo add play command. .play = .n, .play whatever = .q whatever + + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { + //todo add a notice that player is stopped if user queues a song while it is var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); await InternalQueue(mp, songInfo, false); @@ -209,11 +213,9 @@ namespace NadekoBot.Modules.Music if (mp.RepeatCurrentSong) desc = "πŸ”‚ " + GetText("repeating_cur_song") + "\n\n" + desc; - //else if (musicPlayer.RepeatPlaylist) - // desc = "πŸ” " + GetText("repeating_playlist") + "\n\n" + desc; - - - + else if (mp.Shuffle) + desc = "πŸ”€ " + GetText("shuffling_playlist") + "\n\n" + desc; + var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) @@ -307,7 +309,7 @@ namespace NadekoBot.Modules.Music { var song = mp.RemoveAt(index - 1); var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon()) .WithDescription(song.PrettyName) .WithFooter(ef => ef.WithText(song.PrettyInfo)) .WithErrorColor(); @@ -519,22 +521,18 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task ShufflePlaylist() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - // if (musicPlayer.Playlist.Count < 2) - // return; - - // musicPlayer.Shuffle(); - // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - //} + //todo test shuffle + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ShufflePlaylist() + { + var mp = await _music.GetOrCreatePlayer(Context); + var val = mp.ToggleShuffle(); + if(val) + await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false); + else + await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -687,17 +685,22 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Move() + { + var vch = ((IGuildUser)Context.User).VoiceChannel; - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} + if (vch == null) + return; + + var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + + if (mp == null) + return; + //todo test move + mp.SetVoiceChannel(vch); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -745,21 +748,21 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SetMaxQueue(uint size = 0) - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxQueue(uint size = 0) + { + if (size < 0) + return; + var mp = await _music.GetOrCreatePlayer(Context); - // musicPlayer.MaxQueueSize = size; + mp.SetMaxQueueSize(size); - // if(size == 0) - // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - //} + if (size == 0) + await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + else + await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -800,19 +803,17 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task RepeatPl() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // var currentValue = musicPlayer.ToggleRepeatPlaylist(); - // if(currentValue) - // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - //} + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RepeatPl() + { + var mp = await _music.GetOrCreatePlayer(Context); + var currentValue = mp.ToggleRepeatPlaylist(); + if (currentValue) + await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -849,19 +850,17 @@ namespace NadekoBot.Modules.Music // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Autoplay() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Autoplay() + { + var mp = await _music.GetOrCreatePlayer(Context); - // if (!musicPlayer.ToggleAutoplay()) - // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); - //} + if (!mp.ToggleAutoplay()) + await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 200487b2..d75cd963 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -183,7 +183,7 @@ namespace NadekoBot #endregion var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); - var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); + var musicService = new MusicService(Client, GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); #region Games diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 97581b2f..d6cac926 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Services.Music public class MusicPlayer { private readonly Task _player; - private readonly IVoiceChannel VoiceChannel; + private IVoiceChannel VoiceChannel { get; set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); @@ -42,9 +42,13 @@ namespace NadekoBot.Services.Music } public bool RepeatCurrentSong { get; private set; } + public bool Shuffle { get; private set; } + public bool Autoplay { get; private set; } + public bool RepeatPlaylist { get; private set; } = true; private IAudioClient _audioClient; private readonly object locker = new object(); + private MusicService _musicService; #region events public event Action OnStarted; @@ -59,6 +63,7 @@ namespace NadekoBot.Services.Music this.VoiceChannel = vch; this.SongCancelSource = new CancellationTokenSource(); this.OutputTextChannel = output; + this._musicService = musicService; _player = Task.Run(async () => { @@ -96,42 +101,43 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music); - - OnStarted?.Invoke(this, data.Song); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try + using (var pcm = ac.CreatePCMStream(AudioApplication.Music)) { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); - await (pauseTaskSource?.Task ?? Task.CompletedTask); + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); - OnCompleted?.Invoke(this, data.Song); + OnCompleted?.Invoke(this, data.Song); + } } } } @@ -143,8 +149,35 @@ namespace NadekoBot.Services.Music await Task.Delay(500); } while (Stopped && !Exited); - if(!RepeatCurrentSong) - Queue.Next(); + if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) + { + if (Shuffle) + Queue.Random(); //if shuffle is set, set current song index to a random number + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (Queue.Count == data.Index && Autoplay && data.Song?.Provider == "YouTube") + { + try + { + //todo test autoplay + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch { } + } + else if (Queue.Count == data.Index && !RepeatPlaylist) + { + //todo test repeatplaylist + Stop(); + } + else + { + Queue.Next(); + } + } + } } } }, SongCancelSource.Token); @@ -313,6 +346,41 @@ namespace NadekoBot.Services.Music await ac.StopAsync(); } + public bool ToggleShuffle() + { + lock (locker) + { + return Shuffle = !Shuffle; + } + } + + public bool ToggleAutoplay() + { + lock (locker) + { + return Autoplay = !Autoplay; + } + } + + public bool ToggleRepeatPlaylist() + { + lock (locker) + { + return RepeatPlaylist = !RepeatPlaylist; + } + } + + public void SetMaxQueueSize(uint size) + { + Queue.SetMaxQueueSize(size); + } + + public void SetVoiceChannel(IVoiceChannel vch) + { + VoiceChannel = vch; + Next(); + } + //private IAudioClient AudioClient { get; set; } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index cf3a0818..dd5986aa 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -1,4 +1,5 @@ -ο»Ώusing System; +ο»Ώusing NadekoBot.Extensions; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -49,17 +50,23 @@ namespace NadekoBot.Services.Music } } + public uint maxQueueSize { get; private set; } + public void Add(SongInfo song) { + song.ThrowIfNull(nameof(song)); lock (locker) { + if(CurrentIndex >= maxQueueSize) + throw new PlaylistFullException(); Songs.AddLast(song); } } public void Next() { - CurrentIndex++; + lock(locker) + CurrentIndex++; } public void Dispose() @@ -118,5 +125,24 @@ namespace NadekoBot.Services.Music CurrentIndex = 0; } } + + public void Random() + { + lock (locker) + { + CurrentIndex = new NadekoRandom().Next(Songs.Count); + } + } + + public void SetMaxQueueSize(uint size) + { + if (size < 0) + throw new ArgumentOutOfRangeException(nameof(size)); + + lock (locker) + { + maxQueueSize = size; + } + } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 986fd0fd..ccc70dbc 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -10,6 +10,7 @@ using System.IO; using VideoLibrary; using System.Collections.Generic; using Discord.Commands; +using Discord.WebSocket; namespace NadekoBot.Services.Music { @@ -25,13 +26,15 @@ namespace NadekoBot.Services.Music private readonly SoundCloudApiService _sc; private readonly IBotCredentials _creds; private readonly ConcurrentDictionary _defaultVolumes; + private readonly DiscordSocketClient _client; public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, + public MusicService(DiscordSocketClient client, IGoogleApiService google, NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { + _client = client; _google = google; _strings = strings; _localization = localization; @@ -187,6 +190,25 @@ namespace NadekoBot.Services.Music }); } + public MusicPlayer GetPlayerOrDefault(ulong guildId) + { + if (MusicPlayers.TryGetValue(guildId, out var mp)) + return mp; + else + return null; + } + + public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch) + { + var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray(); + if (!related.Any()) + return; + + var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.Normal); + var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); + mp.Enqueue(si); + } + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { query.ThrowIfNull(nameof(query)); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index c5c08175..5f4c8c91 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -b:a 128k -ac 2 pipe:1 -loglevel quiet -nostdin", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 4ad63b4a..28d407ee 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -4,6 +4,7 @@ using NadekoBot.Services.Database.Models; using System; using System.Net; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace NadekoBot.Services.Music { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index e7a3b885..378ac8a3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -442,12 +442,14 @@ "music_repeating_playlist": "Repeating playlist", "music_repeating_track": "Repeating track", "music_repeating_track_stopped": "Current track repeat stopped.", + "music_shuffling_playlist": "Shuffling songs", "music_resumed": "Music playback resumed.", "music_rpl_disabled": "Repeat playlist disabled.", "music_rpl_enabled": "Repeat playlist enabled.", "music_set_music_channel": "I will now output playing, finished, paused and removed songs in this channel.", "music_skipped_to": "Skipped to `{0}:{1}`", - "music_songs_shuffled": "Songs shuffled", + "music_songs_shuffle_enable": "Songs will shuffle from now on.", + "music_songs_shuffle_disable": "Songs will no longer shuffle.", "music_song_moved": "Song moved", "music_song_not_found": "No song found.", "music_time_format": "{0}h {1}m {2}s", From 9bb72d99769419b9e42467bc99c4bb7d75c97c28 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 08:03:24 +0200 Subject: [PATCH 05/40] Shuffle will now show in .lq at the top, and instead of shuffling playlist, it will randomly jump to a song in the playlist. " --- src/NadekoBot/Modules/Music/Music.cs | 22 +++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 68 +++++++++---------- src/NadekoBot/Services/Music/MusicQueue.cs | 2 +- .../_strings/ResponseStrings.en-US.json | 1 + 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c82e59cc..11448b60 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -1,18 +1,14 @@ ο»Ώusing Discord.Commands; using Discord.WebSocket; using NadekoBot.Services; -using System.IO; using Discord; using System.Threading.Tasks; using NadekoBot.Attributes; using System; using System.Linq; using NadekoBot.Extensions; -using System.Net.Http; -using Newtonsoft.Json.Linq; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using System.Threading; using NadekoBot.Services.Music; using NadekoBot.DataStructures; using System.Collections.Concurrent; @@ -102,24 +98,26 @@ namespace NadekoBot.Modules.Music .WithThumbnailUrl(songInfo.Thumbnail) .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) .ConfigureAwait(false); + if (mp.Stopped) + { + (await ReplyErrorLocalized("music_queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); + } queuedMessage?.DeleteAfter(10); } catch { // ignored - } // if queued message sending fails, don't attempt to delete it + } } } } + //todo add play command. .play = .n, .play whatever = .q whatever - - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - //todo add a notice that player is stopped if user queues a song while it is var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); await InternalQueue(mp, songInfo, false); @@ -207,7 +205,7 @@ namespace NadekoBot.Modules.Music return $"**β‡’**`{number}.` {v.PrettyFullName}"; else return $"`{number}.` {v.PrettyFullName}"; - })); //todo v.prettyfullname instead of title + })); desc = $"`πŸ”Š` {songs[current].PrettyFullName}\n\n" + desc; @@ -687,7 +685,7 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Move() + public void Move() { var vch = ((IGuildUser)Context.User).VoiceChannel; @@ -748,6 +746,7 @@ namespace NadekoBot.Modules.Music //} + //todo test smq [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) @@ -782,6 +781,7 @@ namespace NadekoBot.Modules.Music // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); //} + //todo test rcs [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() @@ -803,6 +803,7 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); } + //todo test rpl [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task RepeatPl() @@ -862,6 +863,7 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); } + //todo test output text channel [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index d6cac926..24da1242 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -96,48 +96,46 @@ namespace NadekoBot.Services.Music var ac = await GetAudioClient(); if (ac == null) { - await Task.Delay(900); + await Task.Delay(900, cancelToken); // just wait some time, maybe bot doesn't even have perms to join that voice channel, // i don't want to spam connection attempts continue; } - using (var pcm = ac.CreatePCMStream(AudioApplication.Music)) + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 250); + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try { - OnStarted?.Invoke(this, data.Song); + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try - { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) - { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - - OnCompleted?.Invoke(this, data.Song); - } + OnCompleted?.Invoke(this, data.Song); } } } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index dd5986aa..0715ef2f 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -57,7 +57,7 @@ namespace NadekoBot.Services.Music song.ThrowIfNull(nameof(song)); lock (locker) { - if(CurrentIndex >= maxQueueSize) + if(maxQueueSize !=0 && CurrentIndex >= maxQueueSize) throw new PlaylistFullException(); Songs.AddLast(song); } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 378ac8a3..ab8aea41 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -437,6 +437,7 @@ "music_queued_song": "Queued song", "music_queue_cleared": "Music queue cleared.", "music_queue_full": "Queue is full at {0}/{0}.", + "music_queue_stopped": "Player is stopped. Use {0} command to start playing.", "music_removed_song": "Removed song", "music_repeating_cur_song": "Repeating current song", "music_repeating_playlist": "Repeating playlist", From 42658355b1f122c09889f83af936d16b2cc3ce7c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 09:55:26 +0200 Subject: [PATCH 06/40] music debug stuff --- .../DataStructures/PoopyRingBuffer.cs | 5 ++-- src/NadekoBot/Modules/Music/Music.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 1 + src/NadekoBot/Services/Music/SongBuffer.cs | 27 ++++++++++++++++--- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 4c8764db..0a2ca39f 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -48,7 +48,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 3640 * 200) + public PoopyRingBuffer(int capacity = 3840 * 300) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; @@ -59,7 +59,6 @@ namespace NadekoBot.DataStructures await _locker.WaitAsync(cancelToken); try { - Console.WriteLine("Reading {0}", toRead); if (WritePos == ReadPos) return 0; @@ -129,7 +128,7 @@ namespace NadekoBot.DataStructures WritePos = 0; } } - Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + Console.WriteLine("Readpos: {0} WritePos: {1} ({2})", ReadPos, WritePos, ReadPos - WritePos); return toWrite; } finally diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 11448b60..a8906e62 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -165,6 +165,7 @@ namespace NadekoBot.Modules.Music } } + //todo, page should default to the page the current song is on [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 1) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 24da1242..535cfe34 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -117,6 +117,7 @@ namespace NadekoBot.Services.Music await (pauseTaskSource?.Task ?? Task.CompletedTask); } + _log.Info(">>>>>>>>>READ 0<<<<<<<<<<"); } catch (OperationCanceledException) { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5f4c8c91..1c73da10 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -42,17 +42,36 @@ namespace NadekoBot.Services.Music byte[] buffer = new byte[3840]; while (!this.p.HasExited || cancelToken.IsCancellationRequested) { - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); - + var toRead = buffer.Length; + var remCap = _outStream.RemainingCapacity; + if (remCap < 3840) + { + if (_outStream.RemainingCapacity == 0) + { + Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); + await Task.Delay(10); + continue; + } + toRead = remCap; + } + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) + { + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(50); + } await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) - toReturn.TrySetResult(true); + if(toReturn.TrySetResult(true)) + Console.WriteLine("Prebuffering finished"); } + Console.WriteLine("FFMPEG killed or song canceled"); } catch { - toReturn.TrySetResult(false); + if(toReturn.TrySetResult(false)) + Console.WriteLine("Prebuffering failed"); //ignored } }, cancelToken); From e792e7b39e2256e3df28c97279a7e035028176a1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:22:47 +0200 Subject: [PATCH 07/40] removed -nostdin ffmpeg argument --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 1c73da10..5fe27a52 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -b:a 128k -ac 2 pipe:1 -loglevel quiet -nostdin", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, From f2d1b821d04173126c4d74c89a6b73aa68f19330 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:33:47 +0200 Subject: [PATCH 08/40] Trying to pinpoint song skipping --- src/NadekoBot/Services/Music/SongBuffer.cs | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5fe27a52..e657d6a4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet -nostdin", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[3840]; - while (!this.p.HasExited || cancelToken.IsCancellationRequested) + while (cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; @@ -54,13 +54,20 @@ namespace NadekoBot.Services.Music } toRead = remCap; } - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) + do { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(50); - } - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + if(p.HasExited) + Console.WriteLine("FFMPEG has exited, I'm in the read/write loop"); + + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) + { + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(20); + } + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + + } while (p.HasExited && !cancelToken.IsCancellationRequested); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) if(toReturn.TrySetResult(true)) From 4d52566250d912bcf4ba3fccad970bb54afcc433 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:35:46 +0200 Subject: [PATCH 09/40] woops --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index e657d6a4..65d29111 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[3840]; - while (cancelToken.IsCancellationRequested) + while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; From bbe3ac66e3a60aadb998d739da5ed3b4c91222b1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:44:30 +0200 Subject: [PATCH 10/40] Try reading more from ffmpeg --- src/NadekoBot/Services/Music/SongBuffer.cs | 31 +++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 65d29111..7c2a68ff 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -9,7 +9,7 @@ namespace NadekoBot.Services.Music { public class SongBuffer : IDisposable { - const int maxReadSize = 3840; + const int readSize = 38400; private Process p; private PoopyRingBuffer _outStream = new PoopyRingBuffer(); @@ -39,38 +39,33 @@ namespace NadekoBot.Services.Music { try { - byte[] buffer = new byte[3840]; + byte[] buffer = new byte[readSize]; while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; - if (remCap < 3840) + if (remCap < readSize) { if (_outStream.RemainingCapacity == 0) { Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - await Task.Delay(10); + await Task.Delay(20); continue; } toRead = remCap; } - do + + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) { - if(p.HasExited) - Console.WriteLine("FFMPEG has exited, I'm in the read/write loop"); - - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(20); - } - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - - } while (p.HasExited && !cancelToken.IsCancellationRequested); + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(20); + continue; + } + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) - if(toReturn.TrySetResult(true)) + if (toReturn.TrySetResult(true)) Console.WriteLine("Prebuffering finished"); } Console.WriteLine("FFMPEG killed or song canceled"); From 7de15bf444a73b0fda2f2eed86b2ac6eecbfc30e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:58:04 +0200 Subject: [PATCH 11/40] magic numbers --- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 ++-- src/NadekoBot/Services/Music/SongBuffer.cs | 23 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 535cfe34..1ac0cbad 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -101,10 +101,10 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 250); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 1000); OnStarted?.Invoke(this, data.Song); - byte[] buffer = new byte[3840]; + byte[] buffer = new byte[38400]; int bytesRead = 0; try { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 7c2a68ff..64fe4750 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -43,17 +43,17 @@ namespace NadekoBot.Services.Music while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; - var remCap = _outStream.RemainingCapacity; - if (remCap < readSize) - { - if (_outStream.RemainingCapacity == 0) - { - Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - await Task.Delay(20); - continue; - } - toRead = remCap; - } + //var remCap = _outStream.RemainingCapacity; + //if (remCap < readSize) + //{ + // if (_outStream.RemainingCapacity == 0) + // { + // Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); + // await Task.Delay(20); + // continue; + // } + // toRead = remCap; + //} int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); if (bytesRead == 0) @@ -88,6 +88,7 @@ namespace NadekoBot.Services.Music public void Dispose() { + Console.WriteLine("DISPOSING"); try { this.p.Kill(); } catch { } _outStream.Dispose(); From 0e73372c23330558d742aea7ed129762b9abb21e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 13:19:05 +0200 Subject: [PATCH 12/40] buffer is now 50MB --- .../DataStructures/PoopyRingBuffer.cs | 8 +++--- src/NadekoBot/Services/Music/SongBuffer.cs | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 0a2ca39f..214ee110 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -1,5 +1,7 @@ -ο»Ώusing System; +ο»Ώusing NadekoBot.Extensions; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -48,7 +50,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 3840 * 300) + public PoopyRingBuffer(int capacity = 50_000_000) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; @@ -97,7 +99,7 @@ namespace NadekoBot.DataStructures public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => { while (toWrite > RemainingCapacity) - await Task.Delay(100, cancelToken); + await Task.Delay(50, cancelToken); await _locker.WaitAsync(cancelToken); try diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 64fe4750..1d9fa9ef 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,12 +24,17 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet -nostdin", + Arguments = $"-ss 0 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = false, + RedirectStandardError = true, CreateNoWindow = true, }); + var t = Task.Run(() => + { + this.p.BeginErrorReadLine(); + this.p.WaitForExit(); + }); } public Task StartBuffering(CancellationToken cancelToken) @@ -40,7 +45,8 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[readSize]; - while (!cancelToken.IsCancellationRequested) + int bytesRead = -1; + while (!cancelToken.IsCancellationRequested && bytesRead != 0) { var toRead = buffer.Length; //var remCap = _outStream.RemainingCapacity; @@ -54,14 +60,8 @@ namespace NadekoBot.Services.Music // } // toRead = remCap; //} - - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(20); - continue; - } + + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) @@ -70,8 +70,9 @@ namespace NadekoBot.Services.Music } Console.WriteLine("FFMPEG killed or song canceled"); } - catch + catch (Exception ex) { + Console.WriteLine(ex); if(toReturn.TrySetResult(false)) Console.WriteLine("Prebuffering failed"); //ignored From 5015b6ad95d8d594fd8607db1033659ef15df70d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 13:53:09 +0200 Subject: [PATCH 13/40] cleanup --- .../DataStructures/PoopyRingBuffer.cs | 9 ++-- .../DataStructures/SearchImageCacher.cs | 5 ++- .../Modules/Administration/Administration.cs | 1 - .../Modules/Searches/Commands/LoLCommands.cs | 2 - src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 5 --- src/NadekoBot/Services/Music/Song.cs | 1 - src/NadekoBot/Services/Music/SongBuffer.cs | 44 +++++++++---------- 8 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 214ee110..b0b9f19c 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -87,7 +87,6 @@ namespace NadekoBot.DataStructures ReadPos = secondRead; } } - Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); return toRead; } finally @@ -99,12 +98,14 @@ namespace NadekoBot.DataStructures public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => { while (toWrite > RemainingCapacity) - await Task.Delay(50, cancelToken); + await Task.Delay(1000, cancelToken); // wait a lot, buffer should be large anyway + + if (toWrite == 0) + return; await _locker.WaitAsync(cancelToken); try { - Console.WriteLine("Writing {0}", toWrite); if (WritePos < ReadPos) { Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); @@ -130,8 +131,6 @@ namespace NadekoBot.DataStructures WritePos = 0; } } - Console.WriteLine("Readpos: {0} WritePos: {1} ({2})", ReadPos, WritePos, ReadPos - WritePos); - return toWrite; } finally { diff --git a/src/NadekoBot/DataStructures/SearchImageCacher.cs b/src/NadekoBot/DataStructures/SearchImageCacher.cs index 1296c311..c9795ecf 100644 --- a/src/NadekoBot/DataStructures/SearchImageCacher.cs +++ b/src/NadekoBot/DataStructures/SearchImageCacher.cs @@ -1,6 +1,7 @@ ο»Ώusing NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json; +using NLog; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -19,9 +20,11 @@ namespace NadekoBot.DataStructures private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); private readonly SortedSet _cache; + private readonly Logger _log; public SearchImageCacher() { + _log = LogManager.GetCurrentClassLogger(); _rng = new NadekoRandom(); _cache = new SortedSet(); } @@ -85,7 +88,7 @@ namespace NadekoBot.DataStructures public async Task DownloadImages(string tag, bool isExplicit, DapiSearchType type) { - Console.WriteLine($"Loading extra images from {type}"); + _log.Info($"Loading extra images from {type}"); tag = tag?.Replace(" ", "_").ToLowerInvariant(); if (isExplicit) tag = "rating%3Aexplicit+" + tag; diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 56a0affc..129970e1 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -136,7 +136,6 @@ namespace NadekoBot.Modules.Administration } catch (Exception ex) { - Console.WriteLine(ex); await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index d7697a5c..0225725c 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -173,7 +173,6 @@ namespace NadekoBot.Modules.Searches // .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"]; // if (general == null) // { -// //Console.WriteLine("General is null."); // return; // } // //get build data for this role @@ -309,7 +308,6 @@ namespace NadekoBot.Modules.Searches // } // catch (Exception ex) // { -// //Console.WriteLine(ex); // await channel.SendMessageAsync("πŸ’’ Failed retreiving data for that champion.").ConfigureAwait(false); // } // }); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index d75cd963..00bf8319 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -352,7 +352,7 @@ namespace NadekoBot #if GLOBAL_NADEKO isPublicNadeko = true; #endif - //Console.WriteLine(string.Join(", ", CommandService.Commands + //_log.Info(string.Join(", ", CommandService.Commands // .Distinct(x => x.Name + x.Module.Name) // .SelectMany(x => x.Aliases) // .GroupBy(x => x) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 1ac0cbad..b31831e8 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -78,13 +78,9 @@ namespace NadekoBot.Services.Music } try { - _log.Info("Checking for songs"); if (data.Song == null) continue; - _log.Info("Connecting"); - - _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { @@ -117,7 +113,6 @@ namespace NadekoBot.Services.Music await (pauseTaskSource?.Task ?? Task.CompletedTask); } - _log.Info(">>>>>>>>>READ 0<<<<<<<<<<"); } catch (OperationCanceledException) { diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 074238d6..a9567f9c 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -172,7 +172,6 @@ namespace NadekoBot.Services.Music // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime // { - // //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); // //await inStream.CopyToAsync(voiceClient.OutputStream); // if (read < _frameBytes) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 1d9fa9ef..953e640a 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,4 +1,5 @@ ο»Ώusing NadekoBot.DataStructures; +using NLog; using System; using System.Diagnostics; using System.IO; @@ -14,11 +15,13 @@ namespace NadekoBot.Services.Music private PoopyRingBuffer _outStream = new PoopyRingBuffer(); private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly Logger _log; public string SongUri { get; private set; } public SongBuffer(string songUri, string skipTo) { + _log = LogManager.GetCurrentClassLogger(); this.SongUri = songUri; this.p = Process.Start(new ProcessStartInfo @@ -48,34 +51,31 @@ namespace NadekoBot.Services.Music int bytesRead = -1; while (!cancelToken.IsCancellationRequested && bytesRead != 0) { - var toRead = buffer.Length; - //var remCap = _outStream.RemainingCapacity; - //if (remCap < readSize) - //{ - // if (_outStream.RemainingCapacity == 0) - // { - // Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - // await Task.Delay(20); - // continue; - // } - // toRead = remCap; - //} - - bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f) if (toReturn.TrySetResult(true)) - Console.WriteLine("Prebuffering finished"); + _log.Info("Prebuffering finished"); } - Console.WriteLine("FFMPEG killed or song canceled"); + _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); + } + catch (System.ComponentModel.Win32Exception) + { + _log.Error(@"You have not properly installed or configured FFMPEG. +Please install and configure FFMPEG to play music. +Check the guides for your platform on how to setup ffmpeg correctly: + Windows Guide: https://goo.gl/OjKk8F + Linux Guide: https://goo.gl/ShjCUo"); } catch (Exception ex) { - Console.WriteLine(ex); - if(toReturn.TrySetResult(false)) - Console.WriteLine("Prebuffering failed"); - //ignored + _log.Info(ex); + } + finally + { + if (toReturn.TrySetResult(false)) + _log.Info("Prebuffering failed"); } }, cancelToken); @@ -89,7 +89,6 @@ namespace NadekoBot.Services.Music public void Dispose() { - Console.WriteLine("DISPOSING"); try { this.p.Kill(); } catch { } _outStream.Dispose(); @@ -203,7 +202,6 @@ namespace NadekoBot.Services.Music // { // if (outStream != null) // outStream.Dispose(); -// Console.WriteLine($"Buffering done."); // if (p != null) // { // try From 8e1c20624d7d40148056c7fa97c6940262f8fe6f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 14:49:37 +0200 Subject: [PATCH 14/40] Fixed bugs, added .play command which acts as .n 1 when used without arguments or as .q command when used with serach query --- .../DataStructures/PoopyRingBuffer.cs | 7 +--- .../Modules/Administration/Administration.cs | 2 +- src/NadekoBot/Modules/Music/Music.cs | 34 +++++++++++++----- src/NadekoBot/NadekoBot.cs | 3 -- src/NadekoBot/Resources/CommandStrings.resx | 9 +++++ src/NadekoBot/Services/Music/Exceptions.cs | 6 ++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 25 ++++++++----- src/NadekoBot/Services/Music/MusicQueue.cs | 35 ++++++++++--------- src/NadekoBot/Services/Music/SongBuffer.cs | 1 + 9 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index b0b9f19c..28b391e6 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -1,9 +1,4 @@ -ο»Ώusing NadekoBot.Extensions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +ο»Ώusing System; using System.Threading; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 129970e1..fafd9f48 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -134,7 +134,7 @@ namespace NadekoBot.Modules.Administration await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a8906e62..493e3c08 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -84,7 +84,16 @@ namespace NadekoBot.Modules.Music private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { - var qData = mp.Enqueue(songInfo); + (bool Success, int Index) qData; + try + { + qData = mp.Enqueue(songInfo); + } + catch (QueueFullException) + { + await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); + throw; + } if (qData.Success) { if (!silent) @@ -111,8 +120,16 @@ namespace NadekoBot.Modules.Music } } } - - //todo add play command. .play = .n, .play whatever = .q whatever + //todo test play + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task Play([Remainder]string query = null) + { + if (string.IsNullOrWhiteSpace(query)) + try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } + else + return Next(); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -120,7 +137,7 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); - await InternalQueue(mp, songInfo, false); + try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { @@ -241,7 +258,7 @@ namespace NadekoBot.Modules.Music var mp = await _music.GetOrCreatePlayer(Context); - mp.Next(); + mp.Next(skipCount); } [NadekoCommand, Usage, Description, Aliases] @@ -520,7 +537,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - //todo test shuffle + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ShufflePlaylist() @@ -746,8 +763,7 @@ namespace NadekoBot.Modules.Music //} - - //todo test smq + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) @@ -756,7 +772,7 @@ namespace NadekoBot.Modules.Music return; var mp = await _music.GetOrCreatePlayer(Context); - mp.SetMaxQueueSize(size); + mp.MaxQueueSize = size; if (size == 0) await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 00bf8319..855c5652 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -212,8 +212,6 @@ namespace NadekoBot var pokemonService = new PokemonService(); #endregion - - //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() .Add(Localization) @@ -269,7 +267,6 @@ namespace NadekoBot .Add(this) .Build(); - CommandHandler.AddServices(Services); //setup typereaders diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 8ddcf04d..d8977412 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1467,6 +1467,15 @@ `{0}n` or `{0}n 5` + + play start + + + If no arguments are specified, acts as `{0}next 1` command. If you specify a search query, acts as a `{0}q` command + + + `{0}play` or `{0}play Dream Of Venice` + stop s diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs index af195c15..8d4dab72 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,12 +2,12 @@ namespace NadekoBot.Services.Music { - public class PlaylistFullException : Exception + public class QueueFullException : Exception { - public PlaylistFullException(string message) : base(message) + public QueueFullException(string message) : base(message) { } - public PlaylistFullException() : base("Queue is full.") { } + public QueueFullException() : base("Queue is full.") { } } public class SongNotFoundException : Exception diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b31831e8..e49e8967 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -45,6 +45,11 @@ namespace NadekoBot.Services.Music public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } public bool RepeatPlaylist { get; private set; } = true; + public uint MaxQueueSize + { + get => Queue.MaxQueueSize; + set => Queue.MaxQueueSize = value; + } private IAudioClient _audioClient; private readonly object locker = new object(); @@ -137,7 +142,6 @@ namespace NadekoBot.Services.Music } finally { - _log.Info("Next song"); do { await Task.Delay(500); @@ -146,7 +150,10 @@ namespace NadekoBot.Services.Music if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) { if (Shuffle) + { + _log.Info("Random song"); Queue.Random(); //if shuffle is set, set current song index to a random number + } else { //if last song, and autoplay is enabled, and if it's a youtube song @@ -155,19 +162,25 @@ namespace NadekoBot.Services.Music { try { + _log.Info("Loading related song"); //todo test autoplay await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); Queue.Next(); } - catch { } + catch + { + _log.Info("Loading related song failed."); + } } else if (Queue.Count == data.Index && !RepeatPlaylist) { //todo test repeatplaylist + _log.Info("Stopping because repeatplaylist is disabled"); Stop(); } else { + _log.Info("Next song"); Queue.Next(); } } @@ -208,10 +221,11 @@ namespace NadekoBot.Services.Music return (true, Queue.Count); } - public void Next() + public void Next(int skipCount) { lock (locker) { + Queue.Next(skipCount - 1); Stopped = false; Unpause(); CancelCurrentSong(); @@ -364,11 +378,6 @@ namespace NadekoBot.Services.Music } } - public void SetMaxQueueSize(uint size) - { - Queue.SetMaxQueueSize(size); - } - public void SetVoiceChannel(IVoiceChannel vch) { VoiceChannel = vch; diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 0715ef2f..9156d0a7 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -50,23 +50,37 @@ namespace NadekoBot.Services.Music } } - public uint maxQueueSize { get; private set; } + private uint _maxQueueSize; + public uint MaxQueueSize + { + get => _maxQueueSize; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + lock (locker) + { + _maxQueueSize = value; + } + } + } public void Add(SongInfo song) { song.ThrowIfNull(nameof(song)); lock (locker) { - if(maxQueueSize !=0 && CurrentIndex >= maxQueueSize) - throw new PlaylistFullException(); + if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize) + throw new QueueFullException(); Songs.AddLast(song); } } - public void Next() + public void Next(int skipCount = 1) { lock(locker) - CurrentIndex++; + CurrentIndex += skipCount; } public void Dispose() @@ -133,16 +147,5 @@ namespace NadekoBot.Services.Music CurrentIndex = new NadekoRandom().Next(Songs.Count); } } - - public void SetMaxQueueSize(uint size) - { - if (size < 0) - throw new ArgumentOutOfRangeException(nameof(size)); - - lock (locker) - { - maxQueueSize = size; - } - } } } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 953e640a..033ac9fd 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -68,6 +68,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: Windows Guide: https://goo.gl/OjKk8F Linux Guide: https://goo.gl/ShjCUo"); } + catch (OperationCanceledException) { } catch (Exception ex) { _log.Info(ex); From 45e4816033e4d061661bd24fabb97edbf78c6fea Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 15:44:00 +0200 Subject: [PATCH 15/40] (Re)added shuffle, rpl, rcs and autoplay to the top of the .lq --- src/NadekoBot/Modules/Music/Music.cs | 14 ++++++++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 25 ++++++++++++------- .../_strings/ResponseStrings.en-US.json | 1 + 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 493e3c08..c1280613 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -227,10 +227,18 @@ namespace NadekoBot.Modules.Music desc = $"`πŸ”Š` {songs[current].PrettyFullName}\n\n" + desc; + if (mp.RepeatCurrentSong) desc = "πŸ”‚ " + GetText("repeating_cur_song") + "\n\n" + desc; else if (mp.Shuffle) desc = "πŸ”€ " + GetText("shuffling_playlist") + "\n\n" + desc; + else + { + if(mp.Autoplay) + desc = "β†ͺ " + GetText("autoplaying") + "\n\n" + desc; + if (mp.RepeatPlaylist) + desc = "πŸ” " + GetText("repeating_playlist") + "\n\n" + desc; + } var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) @@ -797,8 +805,7 @@ namespace NadekoBot.Modules.Music // else // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); //} - - //todo test rcs + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() @@ -819,8 +826,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.SendConfirmAsync("πŸ”‚ " + GetText("repeating_track_stopped")) .ConfigureAwait(false); } - - //todo test rpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task RepeatPl() diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index e49e8967..5dd87f67 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -61,6 +61,9 @@ namespace NadekoBot.Services.Music public event Action OnPauseChanged; #endregion + + private bool manualSkip = false; + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); @@ -80,6 +83,7 @@ namespace NadekoBot.Services.Music { data = Queue.Current; cancelToken = SongCancelSource.Token; + manualSkip = false; } try { @@ -142,12 +146,10 @@ namespace NadekoBot.Services.Music } finally { - do - { - await Task.Delay(500); - } - while (Stopped && !Exited); - if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + if (!RepeatCurrentSong || manualSkip) { if (Shuffle) { @@ -172,9 +174,8 @@ namespace NadekoBot.Services.Music _log.Info("Loading related song failed."); } } - else if (Queue.Count == data.Index && !RepeatPlaylist) + else if (Queue.Count - 1 == data.Index && !RepeatPlaylist && !manualSkip) { - //todo test repeatplaylist _log.Info("Stopping because repeatplaylist is disabled"); Stop(); } @@ -185,6 +186,11 @@ namespace NadekoBot.Services.Music } } } + do + { + await Task.Delay(500); + } + while (Stopped && !Exited); } } }, SongCancelSource.Token); @@ -221,10 +227,11 @@ namespace NadekoBot.Services.Music return (true, Queue.Count); } - public void Next(int skipCount) + public void Next(int skipCount = 1) { lock (locker) { + manualSkip = true; Queue.Next(skipCount - 1); Stopped = false; Unpause(); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index ab8aea41..a83d8442 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -404,6 +404,7 @@ "music_attempting_to_queue": "Attempting to queue {0} songs...", "music_autoplay_disabled": "Autoplay disabled.", "music_autoplay_enabled": "Autoplay enabled.", + "music_autoplaying": "Auto-playing.", "music_defvol_set": "Default volume set to {0}%", "music_dir_queue_complete": "Directory queue complete.", "music_fairplay": "fairplay", From b33e4bdd80c3b90f74c42d0f7f159ef12412179f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:20:25 +0200 Subject: [PATCH 16/40] fixed .play --- src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 3 ++- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c1280613..c2dc3614 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -125,7 +125,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public Task Play([Remainder]string query = null) { - if (string.IsNullOrWhiteSpace(query)) + if (!string.IsNullOrWhiteSpace(query)) try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } else return Next(); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 5dd87f67..577800b6 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -232,7 +232,8 @@ namespace NadekoBot.Services.Music lock (locker) { manualSkip = true; - Queue.Next(skipCount - 1); + // if player is stopped, and user uses .n, it should play current song. + // It's a bit weird, but that's the least annoying solution Stopped = false; Unpause(); CancelCurrentSong(); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 033ac9fd..fa356bb4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss 0 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, From 322424b0a1f94d30072550c6ab88f32ec7d92366 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:27:30 +0200 Subject: [PATCH 17/40] .move fixed, but it will cancel current song when moving --- src/NadekoBot/Modules/Music/Music.cs | 5 ++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c2dc3614..8ca46e0a 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -120,7 +120,7 @@ namespace NadekoBot.Modules.Music } } } - //todo test play + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public Task Play([Remainder]string query = null) @@ -722,7 +722,7 @@ namespace NadekoBot.Modules.Music if (mp == null) return; - //todo test move + mp.SetVoiceChannel(vch); } @@ -886,7 +886,6 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); } - //todo test output text channel [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 577800b6..97ef8ee7 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -63,6 +63,7 @@ namespace NadekoBot.Services.Music private bool manualSkip = false; + private bool newVoiceChannel = false; public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { @@ -200,7 +201,8 @@ namespace NadekoBot.Services.Music { if (_audioClient == null || _audioClient.ConnectionState != ConnectionState.Connected || - reconnect) + reconnect || + newVoiceChannel) try { try @@ -211,6 +213,7 @@ namespace NadekoBot.Services.Music catch { } + newVoiceChannel = false; _audioClient = await VoiceChannel.ConnectAsync(); } catch @@ -389,6 +392,7 @@ namespace NadekoBot.Services.Music public void SetVoiceChannel(IVoiceChannel vch) { VoiceChannel = vch; + newVoiceChannel = true; Next(); } From 1d1b7de20aa818a705afce837e2f1075b828759a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:54:09 +0200 Subject: [PATCH 18/40] Fixed playing small songs --- src/NadekoBot/Modules/Music/Music.cs | 17 ++++++++++------- src/NadekoBot/Services/Music/MusicPlayer.cs | 16 ++++++++++++---- src/NadekoBot/Services/Music/SongBuffer.cs | 6 +++--- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 8ca46e0a..a56d5cfa 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -109,7 +109,7 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); if (mp.Stopped) { - (await ReplyErrorLocalized("music_queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); + (await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); } queuedMessage?.DeleteAfter(10); } @@ -227,18 +227,21 @@ namespace NadekoBot.Modules.Music desc = $"`πŸ”Š` {songs[current].PrettyFullName}\n\n" + desc; - + var add = ""; if (mp.RepeatCurrentSong) - desc = "πŸ”‚ " + GetText("repeating_cur_song") + "\n\n" + desc; + add += "πŸ”‚ " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) - desc = "πŸ”€ " + GetText("shuffling_playlist") + "\n\n" + desc; + add += "πŸ”€ " + GetText("shuffling_playlist") + "\n"; else { - if(mp.Autoplay) - desc = "β†ͺ " + GetText("autoplaying") + "\n\n" + desc; if (mp.RepeatPlaylist) - desc = "πŸ” " + GetText("repeating_playlist") + "\n\n" + desc; + add += "πŸ” " + GetText("repeating_playlist") + "\n"; + if (mp.Autoplay) + add += "β†ͺ " + GetText("autoplaying") + "\n"; } + + if (!string.IsNullOrWhiteSpace(add)) + desc += add + "\n"; var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 97ef8ee7..48a037be 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -94,10 +94,18 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { - var bufferSuccess = await b.StartBuffering(cancelToken); - - if (bufferSuccess == false) + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } var ac = await GetAudioClient(); if (ac == null) @@ -161,7 +169,7 @@ namespace NadekoBot.Services.Music { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix - if (Queue.Count == data.Index && Autoplay && data.Song?.Provider == "YouTube") + if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.Provider == "YouTube") { try { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index fa356bb4..0e507344 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -48,13 +48,13 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[readSize]; - int bytesRead = -1; - while (!cancelToken.IsCancellationRequested && bytesRead != 0) + int bytesRead = 1; + while (!cancelToken.IsCancellationRequested && !this.p.HasExited && bytesRead > 0) { bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f) + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f || bytesRead == 0) if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); } From 728aeab8093687ecf38654c5a12a49df434bd506 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 17:15:34 +0200 Subject: [PATCH 19/40] Fixes. .lq will say at the top if it's stopped. .lq will also default to the page current song is playing from --- src/NadekoBot/Modules/Music/Music.cs | 19 ++++++++++++------- .../Services/Games/ChatterbotService.cs | 1 - src/NadekoBot/Services/Music/MusicPlayer.cs | 3 ++- src/NadekoBot/Services/Music/MusicService.cs | 12 ------------ .../_strings/ResponseStrings.en-US.json | 1 + 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a56d5cfa..fbd01028 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -181,11 +181,10 @@ namespace NadekoBot.Modules.Music try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } } - - //todo, page should default to the page the current song is on + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task ListQueue(int page = 1) + public async Task ListQueue(int page = 0) { var mp = await _music.GetOrCreatePlayer(Context); var (current, songs) = mp.QueueArray(); @@ -195,14 +194,18 @@ namespace NadekoBot.Modules.Music await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; } - - if (--page < 0) + + if (--page < -1) return; - //todo say whether music player is stopped //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; + if (page == -1) + page = current / itemsPerPage; + + //if page is 0 (-1 after this decrement) that means default to the page current song is playing from + //var total = musicPlayer.TotalPlaytime; //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", // (int)total.TotalHours, @@ -228,6 +231,8 @@ namespace NadekoBot.Modules.Music desc = $"`πŸ”Š` {songs[current].PrettyFullName}\n\n" + desc; var add = ""; + if (mp.Stopped) + add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; if (mp.RepeatCurrentSong) add += "πŸ”‚ " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) @@ -345,7 +350,7 @@ namespace NadekoBot.Modules.Music } catch (ArgumentOutOfRangeException) { - //todo error message + await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index 833eef0b..7f9c8251 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -104,7 +104,6 @@ namespace NadekoBot.Services.Games { if (pc.Verbose) { - //todo move this to permissions var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } _log.Info(returnMsg); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 48a037be..e9f10ab3 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -174,7 +174,6 @@ namespace NadekoBot.Services.Music try { _log.Info("Loading related song"); - //todo test autoplay await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); Queue.Next(); } @@ -245,6 +244,8 @@ namespace NadekoBot.Services.Music manualSkip = true; // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution + if (!Stopped) + Queue.Next(skipCount - 1); Stopped = false; Unpause(); CancelCurrentSong(); diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index ccc70dbc..03f7b0d9 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -108,18 +108,6 @@ namespace NadekoBot.Services.Music { // ignored } - - //todo autoplay should be independent from event handlers - //if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) - //{ - // var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); - // if (relatedVideos.Count > 0) - // await QueueSong(await textCh.Guild.GetCurrentUserAsync(), - // textCh, - // voiceCh, - // relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], - // true).ConfigureAwait(false); - //} } catch { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index a83d8442..705b3c34 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -440,6 +440,7 @@ "music_queue_full": "Queue is full at {0}/{0}.", "music_queue_stopped": "Player is stopped. Use {0} command to start playing.", "music_removed_song": "Removed song", + "music_removed_song_error": "Song on that index doesn't exist", "music_repeating_cur_song": "Repeating current song", "music_repeating_playlist": "Repeating playlist", "music_repeating_track": "Repeating track", From 196f40e648b888a25bdd2e92f24c12b89fd5ad4d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 19:00:25 +0200 Subject: [PATCH 20/40] readded .sq and .lo and .lopl, also .lopl will now explicitly avoid files with .jpg and .png extension (usually album images) --- src/NadekoBot/Modules/Music/Music.cs | 208 +++++----- .../Services/Database/Models/PlaylistSong.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 8 +- src/NadekoBot/Services/Music/MusicService.cs | 369 +++++++++--------- src/NadekoBot/Services/Music/SongInfo.cs | 8 +- 5 files changed, 301 insertions(+), 294 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index fbd01028..a1078a0f 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -12,6 +12,9 @@ using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NadekoBot.DataStructures; using System.Collections.Concurrent; +using System.IO; +using System.Net.Http; +using Newtonsoft.Json.Linq; namespace NadekoBot.Modules.Music { @@ -36,6 +39,8 @@ namespace NadekoBot.Modules.Music //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } + //todo when someone drags nadeko from one voice channel to another + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) //{ // var usr = iusr as SocketGuildUser; @@ -84,6 +89,13 @@ namespace NadekoBot.Modules.Music private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { + if (songInfo == null) + { + if(!silent) + await ReplyErrorLocalized("song_not_found").ConfigureAwait(false); + return; + } + (bool Success, int Index) qData; try { @@ -137,6 +149,7 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) @@ -486,9 +499,8 @@ namespace NadekoBot.Modules.Music try { await Task.Yield(); - //todo fix for all - if (item.ProviderType == MusicType.Normal) - await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); } catch (SongNotFoundException) { } catch { break; } @@ -524,17 +536,43 @@ namespace NadekoBot.Modules.Music // } //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SoundCloudQueue([Remainder] string query) - //{ - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); - // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - // { - // Context.Message.DeleteAfter(10); - // } - //} + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudQueue([Remainder] string query) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } + //todo test soundcloudpl + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudPl([Remainder] string pl) + { + pl = pl?.Trim(); + + if (string.IsNullOrWhiteSpace(pl)) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + + using (var http = new HttpClient()) + { + var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); + + foreach (var svideo in scvids) + { + try + { + await InternalQueue(mp, await _music.SongInfoFromSVideo(svideo, Context.User.ToString()), true); + } + catch { break; } + } + } + } + + //todo fix playlist sync stuff [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task NowPlaying() @@ -619,103 +657,59 @@ namespace NadekoBot.Modules.Music // await msg.ModifyAsync(m => m.Content = "βœ… " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SoundCloudPl([Remainder] string pl) - //{ - // pl = pl?.Trim(); + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Radio(string radioLink) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } - // if (string.IsNullOrWhiteSpace(pl)) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task Local([Remainder] string path) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(path, Context.User.ToString(), MusicType.Local); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } + //todo test localpl + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task LocalPl([Remainder] string dirPath) + { + if (string.IsNullOrWhiteSpace(dirPath)) + return; - // using (var http = new HttpClient()) - // { - // var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - - // foreach (var svideo in scvids.Skip(1)) - // { - // try - // { - // musicPlayer.AddSong(new Song(new SongInfo - // { - // Title = svideo.FullName, - // Provider = "SoundCloud", - // Uri = await svideo.StreamLink(), - // ProviderType = MusicType.Normal, - // Query = svideo.TrackLink, - // }), ((IGuildUser)Context.User).Username); - // } - // catch (PlaylistFullException) { break; } - // } - // } - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //[OwnerOnly] - //public async Task LocalPl([Remainder] string directory) - //{ - - // var arg = directory; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // var dir = new DirectoryInfo(arg); - // var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) - // .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); - // var gusr = (IGuildUser)Context.User; - // foreach (var file in fileEnum) - // { - // try - // { - // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); - // } - // catch (PlaylistFullException) - // { - // break; - // } - // catch - // { - // // ignored - // } - // } - // await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Radio(string radioLink) - //{ - - // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - // { - // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - // return; - // } - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); - // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - // { - // Context.Message.DeleteAfter(10); - // } - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //[OwnerOnly] - //public async Task Local([Remainder] string path) - //{ - - // var arg = path; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); - - //} + DirectoryInfo dir; + try { dir = new DirectoryInfo(dirPath); } catch { return; } + var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) + .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png"); + foreach (var file in fileEnum) + { + try + { + var song = await _music.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); + await InternalQueue(mp, song, true).ConfigureAwait(false); + } + catch (QueueFullException) + { + break; + } + catch (Exception ex) + { + _log.Warn(ex); + break; + } + } + await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -776,8 +770,6 @@ namespace NadekoBot.Modules.Music // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); // //await channel.SendConfirmAsync($"🎡Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - //} [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Services/Database/Models/PlaylistSong.cs b/src/NadekoBot/Services/Database/Models/PlaylistSong.cs index d1b09f9d..f938d242 100644 --- a/src/NadekoBot/Services/Database/Models/PlaylistSong.cs +++ b/src/NadekoBot/Services/Database/Models/PlaylistSong.cs @@ -12,7 +12,7 @@ public enum MusicType { Radio, - Normal, + YouTube, Local, Soundcloud } diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index e9f10ab3..38babb9c 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -115,10 +115,10 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 1000); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 200); OnStarted?.Invoke(this, data.Song); - byte[] buffer = new byte[38400]; + byte[] buffer = new byte[3840]; int bytesRead = 0; try { @@ -127,7 +127,7 @@ namespace NadekoBot.Services.Music var vol = Volume; if (vol != 1) AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); await (pauseTaskSource?.Task ?? Task.CompletedTask); } @@ -169,7 +169,7 @@ namespace NadekoBot.Services.Music { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix - if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.Provider == "YouTube") + if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) { try { diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 03f7b0d9..a7f9ef46 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -11,6 +11,8 @@ using VideoLibrary; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; +using System.Text.RegularExpressions; +using System.Net.Http; namespace NadekoBot.Services.Music { @@ -156,24 +158,7 @@ namespace NadekoBot.Services.Music // ignored } }; - //mp.SongRemoved += async (song, index) => - //{ - // try - // { - // var embed = new EmbedBuilder() - // .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) - // .WithDescription(song.PrettyName) - // .WithFooter(ef => ef.WithText(song.PrettyInfo)) - // .WithErrorColor(); - - // await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - - // } - // catch - // { - // // ignored - // } - //}; + return mp; }); } @@ -192,27 +177,109 @@ namespace NadekoBot.Services.Music if (!related.Any()) return; - var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.Normal); + var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube); + if (si == null) + throw new SongNotFoundException(); var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); mp.Enqueue(si); } - public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType? musicType = null) { query.ThrowIfNull(nameof(query)); - SongInfo sinfo; - - sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false); + SongInfo sinfo = null; + switch (musicType) + { + case MusicType.YouTube: + sinfo = await ResolveYoutubeSong(query, queuerName); + break; + case MusicType.Radio: + try { sinfo = ResolveRadioSong(IsRadioLink(query) ? await HandleStreamContainers(query) : query, queuerName); } catch { }; + break; + case MusicType.Local: + sinfo = ResolveLocalSong(query, queuerName); + break; + case MusicType.Soundcloud: + sinfo = await ResolveSoundCloudSong(query, queuerName); + break; + case null: + if (_sc.IsSoundCloudLink(query)) + sinfo = await ResolveSoundCloudSong(query, queuerName); + else if (IsRadioLink(query)) + sinfo = ResolveRadioSong(await HandleStreamContainers(query), queuerName); + else + try + { + sinfo = await ResolveYoutubeSong(query, queuerName); + } + catch + { + sinfo = null; + } + break; + } return sinfo; } + public async Task ResolveSoundCloudSong(string query, string queuerName) + { + var svideo = !_sc.IsSoundCloudLink(query) ? + await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false): + await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + + if (svideo == null) + return null; + return await SongInfoFromSVideo(svideo, queuerName); + } + + public async Task SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) => + new SongInfo + { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = await svideo.StreamLink().ConfigureAwait(false), + ProviderType = MusicType.Soundcloud, + Query = svideo.TrackLink, + AlbumArt = svideo.artwork_url, + QueuerName = queuerName + }; + + public SongInfo ResolveLocalSong(string query, string queuerName) + { + return new SongInfo + { + Uri = "\"" + Path.GetFullPath(query) + "\"", + Title = Path.GetFileNameWithoutExtension(query), + Provider = "Local File", + ProviderType = MusicType.Local, + Query = query, + QueuerName = queuerName + }; + } + + public SongInfo ResolveRadioSong(string query, string queuerName) + { + return new SongInfo + { + Uri = query, + Title = query, + Provider = "Radio Stream", + ProviderType = MusicType.Radio, + Query = query, + QueuerName = queuerName + }; + } + public async Task ResolveYoutubeSong(string query, string queuerName) { var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); if (string.IsNullOrWhiteSpace(link)) - throw new OperationCanceledException("Not a valid youtube query."); + { + _log.Info("No song found."); + return null; + } var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); var video = videos @@ -221,7 +288,7 @@ namespace NadekoBot.Services.Music .FirstOrDefault(); if (video == null) // do something with this error - throw new Exception("Could not load any video elements based on the query."); + _log.Info("Could not load any video elements based on the query."); //var m = Regex.Match(query, @"\?t=(?\d*)"); //int gotoTime = 0; //if (m.Captures.Count > 0) @@ -232,171 +299,119 @@ namespace NadekoBot.Services.Music Provider = "YouTube", Uri = await video.GetUriAsync().ConfigureAwait(false), Query = link, - ProviderType = MusicType.Normal, + ProviderType = MusicType.YouTube, QueuerName = queuerName }; return song; } + private bool IsRadioLink(string query) => + (query.StartsWith("http") || + query.StartsWith("ww")) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) await mp.Destroy(); } - // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) - // { - // string GetText(string text, params object[] replacements) => - // _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); + private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); + private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); + private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); - //if (string.IsNullOrWhiteSpace(query) || query.Length< 3) - // throw new ArgumentException("Invalid song query.", nameof(query)); + private async Task HandleStreamContainers(string query) + { + string file = null; + try + { + using (var http = new HttpClient()) + { + file = await http.GetStringAsync(query).ConfigureAwait(false); + } + } + catch + { + return query; + } + if (query.Contains(".pls")) + { + //File1=http://armitunes.com:8000/ + //Regex.Match(query) + try + { + var m = plsRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .pls:\n{file}"); + return null; + } + } + if (query.Contains(".m3u")) + { + /* +# This is a comment + C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + */ + try + { + var m = m3uRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .m3u:\n{file}"); + return null; + } - // var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - // Song resolvedSong; - // try - // { - // musicPlayer.ThrowIfQueueFull(); - // resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); + } + if (query.Contains(".asx")) + { + // + try + { + var m = asxRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .asx:\n{file}"); + return null; + } + } + if (query.Contains(".xspf")) + { + /* + + + + file:///mp3s/song_1.mp3 + */ + try + { + var m = xspfRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .xspf:\n{file}"); + return null; + } + } - // if (resolvedSong == null) - // throw new SongNotFoundException(); - - // musicPlayer.AddSong(resolvedSong, queuer.Username); - // } - // catch (PlaylistFullException) - // { - // try - // { - // await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); - // } - // catch - // { - // // ignored - // } - // throw; - // } - // if (!silent) - // { - // try - // { - // //var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - // var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() - // .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) - // .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") - // .WithThumbnailUrl(resolvedSong.Thumbnail) - // .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) - // .ConfigureAwait(false); - // queuedMessage?.DeleteAfter(10); - // } - // catch - // { - // // ignored - // } // if queued message sending fails, don't attempt to delete it - // } - // } - - - - - - - // private async Task HandleStreamContainers(string query) - // { - // string file = null; - // try - // { - // using (var http = new HttpClient()) - // { - // file = await http.GetStringAsync(query).ConfigureAwait(false); - // } - // } - // catch - // { - // return query; - // } - // if (query.Contains(".pls")) - // { - // //File1=http://armitunes.com:8000/ - // //Regex.Match(query) - // try - // { - // var m = Regex.Match(file, "File1=(?.*?)\\n"); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .pls:\n{file}"); - // return null; - // } - // } - // if (query.Contains(".m3u")) - // { - // /* - //# This is a comment - // C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - // C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - // */ - // try - // { - // var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .m3u:\n{file}"); - // return null; - // } - - // } - // if (query.Contains(".asx")) - // { - // // - // try - // { - // var m = Regex.Match(file, ".*?)\""); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .asx:\n{file}"); - // return null; - // } - // } - // if (query.Contains(".xspf")) - // { - // /* - // - // - // - // file:///mp3s/song_1.mp3 - // */ - // try - // { - // var m = Regex.Match(file, "(?.*?)"); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .xspf:\n{file}"); - // return null; - // } - // } - - // return query; - // } - - // private bool IsRadioLink(string query) => - // (query.StartsWith("http") || - // query.StartsWith("ww")) - // && - // (query.Contains(".pls") || - // query.Contains(".m3u") || - // query.Contains(".asx") || - // query.Contains(".xspf")); + return query; + } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 28d407ee..22accf2f 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -47,7 +47,7 @@ namespace NadekoBot.Services.Music { switch (ProviderType) { - case MusicType.Normal: + case MusicType.YouTube: return Query; case MusicType.Soundcloud: return Query; @@ -60,6 +60,7 @@ namespace NadekoBot.Services.Music } } } + private string videoId = null; private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); public string Thumbnail { @@ -69,9 +70,8 @@ namespace NadekoBot.Services.Music { case MusicType.Radio: return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.Normal: - //todo have videoid in songinfo from the start - var videoId = videoIdRegex.Match(Query); + case MusicType.YouTube: + videoId = videoId ?? videoIdRegex.Match(Query)?.ToString(); return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; case MusicType.Local: return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links From 9f3c04c93e47addd3125e380266746d3e5276e19 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 19:20:04 +0200 Subject: [PATCH 21/40] you can now load only 1 playlist at a time using .load, because it's expensive --- src/NadekoBot/Modules/Music/Music.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a1078a0f..15f7f6a3 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -470,7 +470,7 @@ namespace NadekoBot.Modules.Music .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); } - private readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + private static readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -544,8 +544,7 @@ namespace NadekoBot.Modules.Music var song = await _music.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); await InternalQueue(mp, song, false).ConfigureAwait(false); } - - //todo test soundcloudpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SoundCloudPl([Remainder] string pl) @@ -560,19 +559,26 @@ namespace NadekoBot.Modules.Music using (var http = new HttpClient()) { var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - + IUserMessage msg = null; + try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { } foreach (var svideo in scvids) { try { + await Task.Yield(); await InternalQueue(mp, await _music.SongInfoFromSVideo(svideo, Context.User.ToString()), true); } - catch { break; } + catch (Exception ex) + { + _log.Warn(ex); + break; + } } + if (msg != null) + await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); } } - - //todo fix playlist sync stuff + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task NowPlaying() @@ -676,7 +682,7 @@ namespace NadekoBot.Modules.Music var song = await _music.ResolveSong(path, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, false).ConfigureAwait(false); } - //todo test localpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] @@ -695,6 +701,7 @@ namespace NadekoBot.Modules.Music { try { + await Task.Yield(); var song = await _music.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, true).ConfigureAwait(false); } From 3c9b68e7398da404e1f3546a96553bb1278dcff8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 20:58:45 +0200 Subject: [PATCH 22/40] Cleanup, .smp is reimplemented, and will now show in .lq too --- src/NadekoBot/Modules/Music/Music.cs | 36 +++++++++---------- src/NadekoBot/Resources/CommandStrings.resx | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 ++++++- src/NadekoBot/Services/Music/SongBuffer.cs | 1 + .../_strings/ResponseStrings.en-US.json | 1 + 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 15f7f6a3..ab131a0f 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -246,6 +246,9 @@ namespace NadekoBot.Modules.Music var add = ""; if (mp.Stopped) add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; + var mps = mp.MaxPlaytimeSeconds; + if (mps > 0) + add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("g"))) + "\n"; if (mp.RepeatCurrentSong) add += "πŸ”‚ " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) @@ -259,7 +262,7 @@ namespace NadekoBot.Modules.Music } if (!string.IsNullOrWhiteSpace(add)) - desc += add + "\n"; + desc = add + "\n" + desc; var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) @@ -795,24 +798,21 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SetMaxPlaytime(uint seconds) - //{ - // if (seconds < 15 && seconds != 0) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxPlaytime(uint seconds) + { + if (seconds < 15 && seconds != 0) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + mp.MaxPlaytimeSeconds = seconds; + if (seconds == 0) + await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); + else + await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); + } - // var channel = (ITextChannel)Context.Channel; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // musicPlayer.MaxPlaytimeSeconds = seconds; - // if (seconds == 0) - // await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index d8977412..78297dd9 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1198,7 +1198,7 @@ `{0}drawnew` or `{0}drawnew 5` - playlistshuffle plsh + shuffle plsh Shuffles the current playlist. diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 38babb9c..10d4d2ac 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -50,6 +50,13 @@ namespace NadekoBot.Services.Music get => Queue.MaxQueueSize; set => Queue.MaxQueueSize = value; } + public uint MaxPlaytimeSeconds { get; set; } + + + const int _frameBytes = 3840; + const float _miliseconds = 20.0f; + public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); + private int _bytesSent = 0; private IAudioClient _audioClient; private readonly object locker = new object(); @@ -78,6 +85,7 @@ namespace NadekoBot.Services.Music { while (!Exited) { + _bytesSent = 0; CancellationToken cancelToken; (int Index, SongInfo Song) data; lock (locker) @@ -122,12 +130,14 @@ namespace NadekoBot.Services.Music int bytesRead = 0; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { var vol = Volume; if (vol != 1) AdjustVolume(buffer, vol); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } await (pauseTaskSource?.Task ?? Task.CompletedTask); } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 0e507344..9f26671d 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -69,6 +69,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: Linux Guide: https://goo.gl/ShjCUo"); } catch (OperationCanceledException) { } + catch (InvalidOperationException) { } // when ffmpeg is disposed catch (Exception ex) { _log.Info(ex); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 705b3c34..2703d2fd 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -455,6 +455,7 @@ "music_songs_shuffle_disable": "Songs will no longer shuffle.", "music_song_moved": "Song moved", "music_song_not_found": "No song found.", + "music_song_skips_after": "Songs will skip after {0}", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "To position", "music_unlimited": "unlimited", From a609e17717411a6afa46fc7cf5f7b1520dd949af Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 12:40:12 +0200 Subject: [PATCH 23/40] .play help update, readded pausing when nobody is in voice channel, and also cleaning up music players if bot is kicked or leaves the server --- src/NadekoBot/Modules/Music/Music.cs | 267 +++++---- src/NadekoBot/Resources/CommandStrings.resx | 6 +- .../Administration/PlayingRotateService.cs | 1 - src/NadekoBot/Services/Music/MusicPlayer.cs | 565 +++++------------- src/NadekoBot/Services/Music/MusicQueue.cs | 4 +- src/NadekoBot/Services/Music/MusicService.cs | 10 +- .../_strings/ResponseStrings.en-US.json | 4 +- 7 files changed, 306 insertions(+), 551 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ab131a0f..51713c80 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -36,56 +36,62 @@ namespace NadekoBot.Modules.Music _db = db; _music = music; - //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + _client.LeftGuild += _client_LeftGuild; } - //todo when someone drags nadeko from one voice channel to another + private Task _client_LeftGuild(SocketGuild arg) + { + var t = _music.DestroyPlayer(arg.Id); + return Task.CompletedTask; + } - //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) - //{ - // var usr = iusr as SocketGuildUser; - // if (usr == null || - // oldState.VoiceChannel == newState.VoiceChannel) - // return Task.CompletedTask; + private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + { + var t = Task.Run(() => + { + var usr = iusr as SocketGuildUser; + if (usr == null || + oldState.VoiceChannel == newState.VoiceChannel) + return; - // MusicPlayer player; - // if ((player = _music.GetPlayer(usr.Guild.Id)) == null) - // return Task.CompletedTask; + var player = _music.GetPlayerOrDefault(usr.Guild.Id); - // try - // { - // //if bot moved - // if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - // usr.Id == _client.CurrentUser.Id) - // { - // if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - // player.TogglePause(); - // else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - // player.TogglePause(); + try + { + //if bot moved + if ((player.VoiceChannel == oldState.VoiceChannel) && + usr.Id == _client.CurrentUser.Id) + { + if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + player.TogglePause(); + else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + player.TogglePause(); - // return Task.CompletedTask; - // } + player.SetVoiceChannel(newState.VoiceChannel); + return; + } - // //if some other user moved - // if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - // player.Paused && - // newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) - // (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - // !player.Paused && - // oldState.VoiceChannel.Users.Count == 1)) - // { - // player.TogglePause(); - // return Task.CompletedTask; - // } - - // } - // catch - // { - // // ignored - // } - // return Task.CompletedTask; - //} + //if some other user moved + if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + player.Paused && + newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) + (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + !player.Paused && + oldState.VoiceChannel.Users.Count == 1)) + { + player.TogglePause(); + return; + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { @@ -96,25 +102,24 @@ namespace NadekoBot.Modules.Music return; } - (bool Success, int Index) qData; + int index; try { - qData = mp.Enqueue(songInfo); + index = mp.Enqueue(songInfo); } catch (QueueFullException) { await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); throw; } - if (qData.Success) + if (index != -1) { if (!silent) { try { - //var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (qData.Index)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") .WithThumbnailUrl(songInfo.Thumbnail) .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) @@ -135,12 +140,26 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public Task Play([Remainder]string query = null) + public async Task Play([Remainder] string query = null) { - if (!string.IsNullOrWhiteSpace(query)) - try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } + var mp = await _music.GetOrCreatePlayer(Context); + if (string.IsNullOrWhiteSpace(query)) + { + await Next(); + } + else if (int.TryParse(query, out var index)) + if (index >= 1) + mp.SetIndex(index - 1); + else + return; else - return Next(); + { + try + { + await Queue(query); + } + catch { } + } } [NadekoCommand, Usage, Description, Aliases] @@ -218,13 +237,12 @@ namespace NadekoBot.Modules.Music page = current / itemsPerPage; //if page is 0 (-1 after this decrement) that means default to the page current song is playing from - - //var total = musicPlayer.TotalPlaytime; - //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", - // (int)total.TotalHours, - // total.Minutes, - // total.Seconds); - //var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; + var total = mp.TotalPlaytime; + var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", + (int)total.TotalHours, + total.Minutes, + total.Seconds); + var maxPlaytime = mp.MaxPlaytimeSeconds; var lastPage = songs.Length / itemsPerPage; Func printAction = curPage => { @@ -255,10 +273,12 @@ namespace NadekoBot.Modules.Music add += "πŸ”€ " + GetText("shuffling_playlist") + "\n"; else { - if (mp.RepeatPlaylist) - add += "πŸ” " + GetText("repeating_playlist") + "\n"; if (mp.Autoplay) add += "β†ͺ " + GetText("autoplaying") + "\n"; + if (mp.FairPlay && !mp.Autoplay) + add += " " + GetText("fairplay") + "\n"; + else if (mp.RepeatPlaylist) + add += "πŸ” " + GetText("repeating_playlist") + "\n"; } if (!string.IsNullOrWhiteSpace(add)) @@ -268,12 +288,8 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) .WithDescription(desc) - //.WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + - // $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + - // (musicPlayer.FairPlay - // ? "βœ”οΈ" + GetText("fairplay") - // : "βœ–οΈ" + GetText("fairplay")) + " | " + - // (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) + .WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " + + $"{("tracks".SnPl(songs.Length))} | {totalStr}")) .WithOkColor(); return embed; @@ -517,27 +533,22 @@ namespace NadekoBot.Modules.Music } } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Fairplay() - //{ - // var channel = (ITextChannel)Context.Channel; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - // var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Fairplay() + { + var mp = await _music.GetOrCreatePlayer(Context); + var val = mp.FairPlay = !mp.FairPlay; - // if (val) - // { - // await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); - // } - // else - // { - // await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); - // } - //} + if (val) + { + await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + } + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -613,58 +624,46 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Playlist([Remainder] string playlist) - //{ + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Playlist([Remainder] string playlist) + { + if (string.IsNullOrWhiteSpace(playlist)) + return; - // var arg = playlist; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - // { - // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - // return; - // } - // var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); - // if (plId == null) - // { - // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - // return; - // } - // var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); - // if (!ids.Any()) - // { - // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - // return; - // } - // var count = ids.Count(); - // var msg = await Context.Channel.SendMessageAsync("🎡 " + GetText("attempting_to_queue", - // Format.Bold(count.ToString()))).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); - // var cancelSource = new CancellationTokenSource(); + var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault(); + if (plId == null) + { + await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + return; + } + var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + if (!ids.Any()) + { + await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + return; + } + var count = ids.Count(); + var msg = await Context.Channel.SendMessageAsync("🎡 " + GetText("attempting_to_queue", + Format.Bold(count.ToString()))).ConfigureAwait(false); + + foreach (var song in ids) + { + try + { + if (mp.Exited) + return; - // var gusr = (IGuildUser)Context.User; - // while (ids.Any() && !cancelSource.IsCancellationRequested) - // { - // var tasks = Task.WhenAll(ids.Take(5).Select(async id => - // { - // if (cancelSource.Token.IsCancellationRequested) - // return; - // try - // { - // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); - // } - // catch (SongNotFoundException) { } - // catch { try { cancelSource.Cancel(); } catch { } } - // })); + await Task.WhenAll(Task.Delay(100), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); + } + catch (SongNotFoundException) { } + catch { break; } + } - // await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); - // ids = ids.Skip(5); - // } - - // await msg.ModifyAsync(m => m.Content = "βœ… " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); - //} + await msg.ModifyAsync(m => m.Content = "βœ… " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 78297dd9..e3504acb 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1198,7 +1198,7 @@ `{0}drawnew` or `{0}drawnew 5` - shuffle plsh + shuffle sh plsh Shuffles the current playlist. @@ -1471,10 +1471,10 @@ play start - If no arguments are specified, acts as `{0}next 1` command. If you specify a search query, acts as a `{0}q` command + If no arguments are specified, acts as `{0}next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `{0}q` command - `{0}play` or `{0}play Dream Of Venice` + `{0}play` or `{0}play 5` or `{0}play Dream Of Venice` stop s diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index d4e09ad1..aa9321c6 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -1,6 +1,5 @@ ο»Ώusing Discord.WebSocket; using NadekoBot.DataStructures.Replacements; -using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NLog; diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 10d4d2ac..c42d56da 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -4,7 +4,9 @@ using System; using System.Threading; using System.Threading.Tasks; using NLog; -using System.Diagnostics; +using System.Linq; +using System.Collections.Concurrent; +using NadekoBot.Extensions; namespace NadekoBot.Services.Music { @@ -18,7 +20,7 @@ namespace NadekoBot.Services.Music public class MusicPlayer { private readonly Task _player; - private IVoiceChannel VoiceChannel { get; set; } + public IVoiceChannel VoiceChannel { get; private set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); @@ -27,6 +29,7 @@ namespace NadekoBot.Services.Music public bool Stopped { get; private set; } = false; public float Volume { get; private set; } = 1.0f; public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; + public bool Paused => pauseTaskSource != null; private TaskCompletionSource pauseTaskSource { get; set; } = null; private CancellationTokenSource SongCancelSource { get; set; } @@ -48,7 +51,27 @@ namespace NadekoBot.Services.Music public uint MaxQueueSize { get => Queue.MaxQueueSize; - set => Queue.MaxQueueSize = value; + set { lock (locker) Queue.MaxQueueSize = value; } + } + private bool _fairPlay; + public bool FairPlay + { + get => _fairPlay; + set + { + if (value) + { + var cur = Queue.Current; + if (cur.Song != null) + RecentlyPlayedUsers.Add(cur.Song.QueuerName); + } + else + { + RecentlyPlayedUsers.Clear(); + } + + _fairPlay = value; + } } public uint MaxPlaytimeSeconds { get; set; } @@ -56,6 +79,7 @@ namespace NadekoBot.Services.Music const int _frameBytes = 3840; const float _miliseconds = 20.0f; public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); + private int _bytesSent = 0; private IAudioClient _audioClient; @@ -63,15 +87,19 @@ namespace NadekoBot.Services.Music private MusicService _musicService; #region events - public event Action OnStarted; + public event Action OnStarted; public event Action OnCompleted; public event Action OnPauseChanged; #endregion private bool manualSkip = false; + private bool manualIndex = false; private bool newVoiceChannel = false; + private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + public TimeSpan TotalPlaytime => TimeSpan.MaxValue; + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); @@ -93,6 +121,7 @@ namespace NadekoBot.Services.Music data = Queue.Current; cancelToken = SongCancelSource.Token; manualSkip = false; + manualIndex = false; } try { @@ -123,19 +152,17 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 200); - OnStarted?.Invoke(this, data.Song); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; int bytesRead = 0; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); + AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -165,60 +192,118 @@ namespace NadekoBot.Services.Music } finally { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped - if (!RepeatCurrentSong || manualSkip) + try { - if (Shuffle) + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + + int queueCount; + lock (locker) + queueCount = Queue.Count; + + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + if (Shuffle) { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (Queue.Count - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - Stop(); + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number } else { - _log.Info("Next song"); - Queue.Next(); + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + { + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } + } } } } + catch (Exception ex) + { + _log.Error(ex); + } do { await Task.Delay(500); } - while (Stopped && !Exited); + while ((Queue.Count == 0 || Stopped) && !Exited); } } }, SongCancelSource.Token); } + public void SetIndex(int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + lock (locker) + { + Queue.CurrentIndex = index; + manualIndex = true; + CancelCurrentSong(); + } + } + private async Task GetAudioClient(bool reconnect = false) { if (_audioClient == null || _audioClient.ConnectionState != ConnectionState.Connected || - reconnect || + reconnect || newVoiceChannel) try { @@ -240,17 +325,24 @@ namespace NadekoBot.Services.Music return _audioClient; } - public (bool Success, int Index) Enqueue(SongInfo song) + public int Enqueue(SongInfo song) { _log.Info("Adding song"); - Queue.Add(song); - return (true, Queue.Count); + lock (locker) + { + if (Exited) + return -1; + Queue.Add(song); + return Queue.Count; + } } public void Next(int skipCount = 1) { lock (locker) { + if (Exited) + return; manualSkip = true; // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution @@ -277,10 +369,13 @@ namespace NadekoBot.Services.Music private void Unpause() { - if (pauseTaskSource != null) + lock (locker) { - pauseTaskSource.TrySetResult(true); - pauseTaskSource = null; + if (pauseTaskSource != null) + { + pauseTaskSource.TrySetResult(true); + pauseTaskSource = null; + } } } @@ -302,7 +397,10 @@ namespace NadekoBot.Services.Music { if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException(nameof(volume)); - Volume = ((float)volume) / 100; + lock (locker) + { + Volume = ((float)volume) / 100; + } } public SongInfo RemoveAt(int index) @@ -335,7 +433,10 @@ namespace NadekoBot.Services.Music } public (int CurrentIndex, SongInfo[] Songs) QueueArray() - => Queue.ToArray(); + { + lock (locker) + return Queue.ToArray(); + } //aidiakapi ftw public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) @@ -410,366 +511,18 @@ namespace NadekoBot.Services.Music public void SetVoiceChannel(IVoiceChannel vch) { - VoiceChannel = vch; - newVoiceChannel = true; - Next(); + lock (locker) + { + if (Exited) + return; + VoiceChannel = vch; + } } - - //private IAudioClient AudioClient { get; set; } - - ///// - ///// Player will prioritize different queuer name - ///// over the song position in the playlist - ///// - //public bool FairPlay { get; set; } = false; - - ///// - ///// Song will stop playing after this amount of time. - ///// To prevent people queueing radio or looped songs - ///// while other people want to listen to other songs too. - ///// - //public uint MaxPlaytimeSeconds { get; set; } = 0; - - //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? // TimeSpan.MaxValue : - // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); - - ///// - ///// Users who recently got their music wish - ///// - //private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - - //private readonly List _playlist = new List(); - //private readonly Logger _log; - //private readonly IGoogleApiService _google; - - //public IReadOnlyCollection Playlist => _playlist; - - //public Song CurrentSong { get; private set; } - //public CancellationTokenSource SongCancelSource { get; private set; } - //private CancellationToken CancelToken { get; set; } - - //public bool Paused { get; set; } - - //public float Volume { get; private set; } - - //public event Action OnCompleted = delegate { }; - //public event Action OnStarted = delegate { }; - //public event Action OnPauseChanged = delegate { }; - - //public IVoiceChannel PlaybackVoiceChannel { get; private set; } - //public ITextChannel OutputTextChannel { get; set; } - - //private bool Destroyed { get; set; } - //public bool RepeatSong { get; private set; } - //public bool RepeatPlaylist { get; private set; } - //public bool Autoplay { get; set; } - //public uint MaxQueueSize { get; set; } = 0; - - //private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); - - //public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; - - //public event Action SongRemoved = delegate { }; - - //public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) - //{ - // _log = LogManager.GetCurrentClassLogger(); - // _google = google; - - // OutputTextChannel = outputChannel; - // Volume = defaultVolume ?? 1.0f; - - // PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); - // SongCancelSource = new CancellationTokenSource(); - // CancelToken = SongCancelSource.Token; - - // Task.Run(async () => - // { - // try - // { - // while (!Destroyed) - // { - // try - // { - // if (ActionQueue.TryDequeue(out Action action)) - // { - // action(); - // } - // } - // finally - // { - // await Task.Delay(100).ConfigureAwait(false); - // } - // } - // } - // catch (Exception ex) - // { - // _log.Warn("Action queue crashed"); - // _log.Warn(ex); - // } - // }).ConfigureAwait(false); - - // var t = new Thread(async () => - // { - // while (!Destroyed) - // { - // try - // { - // CurrentSong = GetNextSong(); - - // if (CurrentSong == null) - // continue; - - // while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || - // AudioClient?.ConnectionState == ConnectionState.Connecting) - // { - // _log.Info("Waiting for Audio client"); - // await Task.Delay(200).ConfigureAwait(false); - // } - - // if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) - // AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); - - // var index = _playlist.IndexOf(CurrentSong); - // if (index != -1) - // RemoveSongAt(index, true); - - // OnStarted(this, CurrentSong); - // try - // { - // await CurrentSong.Play(AudioClient, CancelToken); - // } - // catch (OperationCanceledException) - // { - // } - // finally - // { - // OnCompleted(this, CurrentSong); - // } - - - // if (RepeatPlaylist & !RepeatSong) - // AddSong(CurrentSong, CurrentSong.QueuerName); - - // if (RepeatSong) - // AddSong(CurrentSong, 0); - - // } - // catch (Exception ex) - // { - // _log.Warn("Music thread almost crashed."); - // _log.Warn(ex); - // await Task.Delay(3000).ConfigureAwait(false); - // } - // finally - // { - // if (!CancelToken.IsCancellationRequested) - // { - // SongCancelSource.Cancel(); - // } - // SongCancelSource = new CancellationTokenSource(); - // CancelToken = SongCancelSource.Token; - // CurrentSong = null; - // await Task.Delay(300).ConfigureAwait(false); - // } - // } - // }); - - // t.Start(); - //} - - //public void Next() - //{ - // ActionQueue.Enqueue(() => - // { - // Paused = false; - // SongCancelSource.Cancel(); - // }); - //} - - //public void Stop() - //{ - // ActionQueue.Enqueue(() => - // { - // RepeatPlaylist = false; - // RepeatSong = false; - // Autoplay = false; - // _playlist.Clear(); - // if (!SongCancelSource.IsCancellationRequested) - // SongCancelSource.Cancel(); - // }); - //} - - //public void TogglePause() => OnPauseChanged(Paused = !Paused); - - //public int SetVolume(int volume) - //{ - // if (volume < 0) - // volume = 0; - // if (volume > 100) - // volume = 100; - - // Volume = volume / 100.0f; - // return volume; - //} - - //private Song GetNextSong() - //{ - // if (!FairPlay) - // { - // return _playlist.FirstOrDefault(); - // } - // var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) - // ?? _playlist.FirstOrDefault(); - - // if (song == null) - // return null; - - // if (RecentlyPlayedUsers.Contains(song.QueuerName)) - // { - // RecentlyPlayedUsers.Clear(); - // } - - // RecentlyPlayedUsers.Add(song.QueuerName); - // return song; - //} - - //public void Shuffle() - //{ - // ActionQueue.Enqueue(() => - // { - // var oldPlaylist = _playlist.ToArray(); - // _playlist.Clear(); - // _playlist.AddRange(oldPlaylist.Shuffle()); - // }); - //} - - //public void AddSong(Song s, string username) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ThrowIfQueueFull(); - // ActionQueue.Enqueue(() => - // { - // s.MusicPlayer = this; - // s.QueuerName = username.TrimTo(10); - // _playlist.Add(s); - // }); - //} - - //public void AddSong(Song s, int index) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ActionQueue.Enqueue(() => - // { - // _playlist.Insert(index, s); - // }); - //} - - //public void RemoveSong(Song s) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ActionQueue.Enqueue(() => - // { - // _playlist.Remove(s); - // }); - //} - - //public void RemoveSongAt(int index, bool silent = false) - //{ - // ActionQueue.Enqueue(() => - // { - // if (index < 0 || index >= _playlist.Count) - // return; - // var song = _playlist.ElementAtOrDefault(index); - // if (_playlist.Remove(song) && !silent) - // { - // SongRemoved(song, index); - // } - - // }); - //} - - //public void ClearQueue() - //{ - // ActionQueue.Enqueue(() => - // { - // _playlist.Clear(); - // }); - //} - - //public async Task UpdateSongDurationsAsync() - //{ - // var curSong = CurrentSong; - // var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - // s.TotalTime == TimeSpan.Zero) - // .ToArray(); - // if (curSong != null) - // { - // Array.Resize(ref toUpdate, toUpdate.Length + 1); - // toUpdate[toUpdate.Length - 1] = curSong; - // } - // var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - // .Distinct(); - - // var durations = await _google.GetVideoDurationsAsync(ids); - - // toUpdate.ForEach(s => - // { - // foreach (var kvp in durations) - // { - // if (s.SongInfo.Query.EndsWith(kvp.Key)) - // { - // s.TotalTime = kvp.Value; - // return; - // } - // } - // }); - //} - - //public void Destroy() - //{ - // ActionQueue.Enqueue(async () => - // { - // RepeatPlaylist = false; - // RepeatSong = false; - // Autoplay = false; - // Destroyed = true; - // _playlist.Clear(); - - // try { await AudioClient.StopAsync(); } catch { } - // if (!SongCancelSource.IsCancellationRequested) - // SongCancelSource.Cancel(); - // }); - //} - - ////public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) - ////{ - //// if (audioClient?.ConnectionState != ConnectionState.Connected) - //// throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); - //// PlaybackVoiceChannel = voiceChannel; - //// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); - ////} - - //public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; - - //public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; - - //public bool ToggleAutoplay() => Autoplay = !Autoplay; - - //public void ThrowIfQueueFull() - //{ - // if (MaxQueueSize == 0) - // return; - // if (_playlist.Count >= MaxQueueSize) - // throw new PlaylistFullException(); - //} + // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 9156d0a7..967a5197 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Services.Music { private LinkedList Songs { get; } = new LinkedList(); private int _currentIndex = 0; - private int CurrentIndex + public int CurrentIndex { get { @@ -124,7 +124,7 @@ namespace NadekoBot.Services.Music } } - public (int, SongInfo[]) ToArray() + public (int CurrentIndex, SongInfo[] Songs) ToArray() { lock (locker) { diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index a7f9ef46..2ed203ac 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -131,9 +131,9 @@ namespace NadekoBot.Services.Music playingMessage?.DeleteAfter(0); playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo))) + .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon()) + .WithDescription(song.Song.PrettyName) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo))) .ConfigureAwait(false); } catch @@ -288,11 +288,15 @@ namespace NadekoBot.Services.Music .FirstOrDefault(); if (video == null) // do something with this error + { _log.Info("Could not load any video elements based on the query."); + return null; + } //var m = Regex.Match(query, @"\?t=(?\d*)"); //int gotoTime = 0; //if (m.Captures.Count > 0) // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new SongInfo { Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 2703d2fd..fa4bea1a 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -407,7 +407,7 @@ "music_autoplaying": "Auto-playing.", "music_defvol_set": "Default volume set to {0}%", "music_dir_queue_complete": "Directory queue complete.", - "music_fairplay": "fairplay", + "music_fairplay": "Fairplay", "music_finished_song": "Finished song", "music_fp_disabled": "Fair play disabled.", "music_fp_enabled": "Fair play enabled.", @@ -425,7 +425,7 @@ "music_no_search_results": "No search results.", "music_paused": "Music playback paused.", "music_player_queue": "Player queue - Page {0}/{1}", - "music_playing_song": "Playing song", + "music_playing_song": "Playing song #{0}", "music_playlists": "`#{0}` - **{1}** by *{2}* ({3} songs)", "music_playlists_page": "Page {0} of saved playlists", "music_playlist_deleted": "Playlist deleted.", From 99049a6ace16343f39936574655ca1b897353ffa Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 12:46:51 +0200 Subject: [PATCH 24/40] Prebuffering time drastically decreased --- src/NadekoBot/DataStructures/PoopyRingBuffer.cs | 5 +++++ src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 28b391e6..a09af647 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -38,6 +38,11 @@ namespace NadekoBot.DataStructures } } + public int LightLength => + _readPos <= _writePos? + _writePos - _readPos : + Capacity - (_readPos - _writePos); + public int RemainingCapacity { get { lock (posLock) return Capacity - Length - 1; } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 9f26671d..08737c49 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -54,7 +54,7 @@ namespace NadekoBot.Services.Music bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f || bytesRead == 0) + if (_outStream.LightLength > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); } From 421431d01dd871fa341927223e0b7248c2b249ff Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 13:18:13 +0200 Subject: [PATCH 25/40] pausing disabled again due to issues --- src/NadekoBot/Modules/Music/Music.cs | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 51713c80..6cb3ebe8 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -63,27 +63,27 @@ namespace NadekoBot.Modules.Music if ((player.VoiceChannel == oldState.VoiceChannel) && usr.Id == _client.CurrentUser.Id) { - if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - player.TogglePause(); - else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - player.TogglePause(); + //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // player.TogglePause(); + //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // player.TogglePause(); player.SetVoiceChannel(newState.VoiceChannel); return; } - //if some other user moved - if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - player.Paused && - newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) - (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - !player.Paused && - oldState.VoiceChannel.Users.Count == 1)) - { - player.TogglePause(); - return; - } + ////if some other user moved + //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // player.Paused && + // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) + // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // !player.Paused && + // oldState.VoiceChannel.Users.Count == 1)) + //{ + // player.TogglePause(); + // return; + //} } catch { From 44859529d53a0e4d469f206289b4014876ce7339 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 20:26:17 +0200 Subject: [PATCH 26/40] Cleanup and fixes --- .../DataStructures/PoopyRingBuffer.cs | 143 +++++++----------- src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/NadekoBot.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 82 +++++----- src/NadekoBot/Services/Music/SongBuffer.cs | 36 ++++- 5 files changed, 134 insertions(+), 130 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index a09af647..6c1e1118 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -10,42 +10,27 @@ namespace NadekoBot.DataStructures // writepos == readpos - 1 means full private readonly byte[] buffer; - private readonly object posLock = new object(); public int Capacity { get; } - private volatile int _readPos = 0; + private int _readPos = 0; private int ReadPos { get => _readPos; - set { lock (posLock) _readPos = value; } + set => _readPos = value; } - private volatile int _writePos = 0; + private int _writePos = 0; private int WritePos { get => _writePos; - set { lock (posLock) _writePos = value; } + set => _writePos = value; } - private int Length - { - get - { - lock (posLock) - { - return ReadPos <= WritePos ? - WritePos - ReadPos : - Capacity - (ReadPos - WritePos); - } - } - } - - public int LightLength => - _readPos <= _writePos? - _writePos - _readPos : - Capacity - (_readPos - _writePos); + public int Length => ReadPos <= WritePos + ? WritePos - ReadPos + : Capacity - (ReadPos - WritePos); public int RemainingCapacity { - get { lock (posLock) return Capacity - Length - 1; } + get => Capacity - Length - 1; } private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); @@ -56,90 +41,76 @@ namespace NadekoBot.DataStructures this.buffer = new byte[this.Capacity]; } - public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) => Task.Run(async () => + public int Read(byte[] b, int offset, int toRead) { - await _locker.WaitAsync(cancelToken); - try + if (WritePos == ReadPos) + return 0; + + if (toRead > Length) + toRead = Length; + + if (WritePos > ReadPos) { - if (WritePos == ReadPos) - return 0; - - if (toRead > Length) - toRead = Length; - - if (WritePos > ReadPos) - { - Buffer.BlockCopy(buffer, ReadPos, b, offset, toRead); - ReadPos += toRead; - } - else - { - var toEnd = Capacity - ReadPos; - var firstRead = toRead > toEnd ? - toEnd : - toRead; - Buffer.BlockCopy(buffer, ReadPos, b, offset, firstRead); - ReadPos += firstRead; - var secondRead = toRead - firstRead; - if (secondRead > 0) - { - Buffer.BlockCopy(buffer, 0, b, offset + firstRead, secondRead); - ReadPos = secondRead; - } - } - return toRead; + Array.Copy(buffer, ReadPos, b, offset, toRead); + ReadPos += toRead; } - finally + else { - _locker.Release(); + var toEnd = Capacity - ReadPos; + var firstRead = toRead > toEnd ? + toEnd : + toRead; + Array.Copy(buffer, ReadPos, b, offset, firstRead); + ReadPos += firstRead; + var secondRead = toRead - firstRead; + if (secondRead > 0) + { + Array.Copy(buffer, 0, b, offset + firstRead, secondRead); + ReadPos = secondRead; + } } - }); + return toRead; + } - public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => + public bool Write(byte[] b, int offset, int toWrite) { while (toWrite > RemainingCapacity) - await Task.Delay(1000, cancelToken); // wait a lot, buffer should be large anyway + return false; if (toWrite == 0) - return; + return true; - await _locker.WaitAsync(cancelToken); - try + if (WritePos < ReadPos) { - if (WritePos < ReadPos) + Array.Copy(b, offset, buffer, WritePos, toWrite); + WritePos += toWrite; + } + else + { + var toEnd = Capacity - WritePos; + var firstWrite = toWrite > toEnd ? + toEnd : + toWrite; + Array.Copy(b, offset, buffer, WritePos, firstWrite); + var secondWrite = toWrite - firstWrite; + if (secondWrite > 0) { - Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); - WritePos += toWrite; + Array.Copy(b, offset + firstWrite, buffer, 0, secondWrite); + WritePos = secondWrite; } else { - var toEnd = Capacity - WritePos; - var firstWrite = toWrite > toEnd ? - toEnd : - toWrite; - Buffer.BlockCopy(b, offset, buffer, WritePos, firstWrite); - var secondWrite = toWrite - firstWrite; - if (secondWrite > 0) - { - Buffer.BlockCopy(b, offset + firstWrite, buffer, 0, secondWrite); - WritePos = secondWrite; - } - else - { - WritePos += firstWrite; - if (WritePos == Capacity) - WritePos = 0; - } + WritePos += firstWrite; + if (WritePos == Capacity) + WritePos = 0; } } - finally - { - _locker.Release(); - } - }); + return true; + } public void Dispose() { + } } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 6cb3ebe8..bc88ff2d 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -45,7 +45,7 @@ namespace NadekoBot.Modules.Music var t = _music.DestroyPlayer(arg.Id); return Task.CompletedTask; } - + //todo changing server region is bugged again private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) { var t = Task.Run(() => diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 855c5652..5560437f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -31,6 +31,7 @@ using NadekoBot.Extensions; namespace NadekoBot { + //todo log when joining or leaving the server public class NadekoBot { private Logger _log; diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index c42d56da..a9a12f25 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -131,38 +131,40 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - OnStarted?.Invoke(this, data); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; + AudioOutStream pcm = null; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); + continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + + var ac = await GetAudioClient(); + if (ac == null) + { + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - AdjustVolume(buffer, Volume); + //AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -179,12 +181,16 @@ namespace NadekoBot.Services.Music } finally { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + } OnCompleted?.Invoke(this, data.Song); } @@ -309,8 +315,12 @@ namespace NadekoBot.Services.Music { try { - await _audioClient?.StopAsync(); - _audioClient?.Dispose(); + var t = _audioClient?.StopAsync(); + if (t != null) + { + await t; + _audioClient?.Dispose(); + } } catch { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 08737c49..527c65f3 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -36,10 +36,17 @@ namespace NadekoBot.Services.Music var t = Task.Run(() => { this.p.BeginErrorReadLine(); + this.p.ErrorDataReceived += P_ErrorDataReceived; this.p.WaitForExit(); }); } + private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + _log.Error(">>> " + e.Data); + } + + private readonly object locker = new object(); public Task StartBuffering(CancellationToken cancelToken) { var toReturn = new TaskCompletionSource(); @@ -49,14 +56,26 @@ namespace NadekoBot.Services.Music { byte[] buffer = new byte[readSize]; int bytesRead = 1; - while (!cancelToken.IsCancellationRequested && !this.p.HasExited && bytesRead > 0) + while (!cancelToken.IsCancellationRequested && !this.p.HasExited) { bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + if (bytesRead == 0) + break; + bool written; + do + { + lock (locker) + written = _outStream.Write(buffer, 0, bytesRead); + if (!written) + await Task.Delay(32, cancelToken); + } + while (!written); + lock (locker) + if (_outStream.Length > 200_000 || bytesRead == 0) + if (toReturn.TrySetResult(true)) + _log.Info("Prebuffering finished"); - if (_outStream.LightLength > 200_000 || bytesRead == 0) - if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished"); + await Task.Delay(5); // @.@ } _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); } @@ -84,9 +103,10 @@ Check the guides for your platform on how to setup ffmpeg correctly: return toReturn.Task; } - public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) + public int Read(byte[] b, int offset, int toRead) { - return _outStream.ReadAsync(b, offset, toRead, cancelToken); + lock (locker) + return _outStream.Read(b, offset, toRead); } public void Dispose() @@ -94,6 +114,8 @@ Check the guides for your platform on how to setup ffmpeg correctly: try { this.p.Kill(); } catch { } _outStream.Dispose(); + this.p.StandardError.Dispose(); + this.p.StandardOutput.Dispose(); this.p.Dispose(); } } From 89eabc7c14657f93636316e3cf7d16c5091d0025 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 20:29:32 +0200 Subject: [PATCH 27/40] Fixed crash --- src/NadekoBot/Modules/Music/Music.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index bc88ff2d..09bcde52 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -57,6 +57,9 @@ namespace NadekoBot.Modules.Music var player = _music.GetPlayerOrDefault(usr.Guild.Id); + if (player == null) + return; + try { //if bot moved From f826fb97f6dff871d8f5270230144f554b23f59f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 21:05:35 +0200 Subject: [PATCH 28/40] Super weird fixes, i must've broke something else. Bot has to reconnect after restart now --- src/NadekoBot/Modules/Music/Music.cs | 4 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 311 ++++++++++---------- src/NadekoBot/Services/Music/SongBuffer.cs | 10 +- 3 files changed, 166 insertions(+), 159 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 09bcde52..8f80e726 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -70,8 +70,8 @@ namespace NadekoBot.Modules.Music // player.TogglePause(); //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel // player.TogglePause(); - - player.SetVoiceChannel(newState.VoiceChannel); + + // player.SetVoiceChannel(newState.VoiceChannel); return; } diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index a9a12f25..5df2955a 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -123,172 +123,166 @@ namespace NadekoBot.Services.Music manualSkip = false; manualIndex = false; } - try - { - if (data.Song == null) - continue; - - _log.Info("Starting"); - using (var b = new SongBuffer(data.Song.Uri, "")) - { - AudioOutStream pcm = null; - try - { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - OnStarted?.Invoke(this, data); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - - while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) - { - //AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - unchecked { _bytesSent += bytesRead; } - - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - if (pcm != null) - { - // flush is known to get stuck from time to time, - // just skip flushing if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - } - - OnCompleted?.Invoke(this, data.Song); - } - } - } - finally + if (data.Song == null) + continue; + + _log.Info("Starting"); + using (var b = new SongBuffer(data.Song.Uri, "")) { + AudioOutStream pcm = null; try { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped - - int queueCount; - lock (locker) - queueCount = Queue.Count; - - if (!manualIndex && (!RepeatCurrentSong || manualSkip)) + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) { - if (Shuffle) - { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) - { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (FairPlay) - { - lock (locker) - { - _log.Info("Next fair song"); - var q = Queue.ToArray().Songs.Shuffle().ToArray(); - - bool found = false; - for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently - { - var item = q[i]; - if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index - { - Queue.CurrentIndex = i; - found = true; - break; - } - } - if (!found) //if it's not - { - RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) - Queue.Random(); //go to a random song (to prevent looping on the first few songs) - var cur = Current; - if (cur.Current != null) // add newely scheduled song's queuer to the recently played list - RecentlyPlayedUsers.Add(cur.Current.QueuerName); - } - } - } - else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - lock (locker) - { - Stop(); - } - } - else - { - _log.Info("Next song"); - lock (locker) - { - Queue.Next(); - } - } - } + _log.Info("Buffering failed due to a timeout."); + continue; } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + + var ac = await GetAudioClient(); + if (ac == null) + { + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + { + //AdjustVolume(buffer, Volume); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); } catch (Exception ex) { - _log.Error(ex); + _log.Warn(ex); } - do + finally { - await Task.Delay(500); + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + } + + OnCompleted?.Invoke(this, data.Song); } - while ((Queue.Count == 0 || Stopped) && !Exited); } + try + { + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + + int queueCount; + lock (locker) + queueCount = Queue.Count; + + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) + { + if (Shuffle) + { + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number + } + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + { + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } + } + } + } + } + catch (Exception ex) + { + _log.Error(ex); + } + do + { + await Task.Delay(500); + } + while ((Queue.Count == 0 || Stopped) && !Exited); } }, SongCancelSource.Token); } @@ -319,13 +313,20 @@ namespace NadekoBot.Services.Music if (t != null) { await t; - _audioClient?.Dispose(); + _audioClient.Dispose(); } } catch { } newVoiceChannel = false; + var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); + _audioClient = await VoiceChannel.ConnectAsync(); + if (curUser.VoiceChannel != null) + { + await _audioClient.StopAsync(); + await Task.Delay(1000); + } _audioClient = await VoiceChannel.ConnectAsync(); } catch diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 527c65f3..8cc23462 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -111,11 +111,17 @@ Check the guides for your platform on how to setup ffmpeg correctly: public void Dispose() { + try + { + this.p.StandardOutput.Dispose(); + } + catch (Exception ex) + { + _log.Error(ex); + } try { this.p.Kill(); } catch { } _outStream.Dispose(); - this.p.StandardError.Dispose(); - this.p.StandardOutput.Dispose(); this.p.Dispose(); } } From eba804b5ce1baff5185afefcfff28187630d462b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 23:27:17 +0200 Subject: [PATCH 29/40] Cleanup, improvements --- .../DataStructures/PoopyRingBuffer.cs | 2 +- .../Administration/GuildTimezoneService.cs | 5 +++- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 +-- src/NadekoBot/Services/Music/SongBuffer.cs | 29 +++++++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 6c1e1118..acb7db8e 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -35,7 +35,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 50_000_000) + public PoopyRingBuffer(int capacity = 81920 * 100) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs index 1d469211..6e4757fe 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs @@ -21,7 +21,10 @@ namespace NadekoBot.Services.Administration TimeZoneInfo tz; try { - tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); + if (x.TimeZoneId == null) + tz = null; + else + tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); } catch { diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 5df2955a..fc11e1ab 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -321,10 +321,10 @@ namespace NadekoBot.Services.Music } newVoiceChannel = false; var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); - _audioClient = await VoiceChannel.ConnectAsync(); if (curUser.VoiceChannel != null) { - await _audioClient.StopAsync(); + var ac = await VoiceChannel.ConnectAsync(); + await ac.StopAsync(); await Task.Delay(1000); } _audioClient = await VoiceChannel.ConnectAsync(); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 8cc23462..d0d550b3 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Services.Music { public class SongBuffer : IDisposable { - const int readSize = 38400; + const int readSize = 81920; private Process p; private PoopyRingBuffer _outStream = new PoopyRingBuffer(); @@ -52,8 +52,13 @@ namespace NadekoBot.Services.Music var toReturn = new TaskCompletionSource(); var _ = Task.Run(async () => { + int maxLoopsPerSec = 25; + var sw = Stopwatch.StartNew(); + var delay = 1000 / maxLoopsPerSec; + int currentLoops = 0; try { + ++currentLoops; byte[] buffer = new byte[readSize]; int bytesRead = 1; while (!cancelToken.IsCancellationRequested && !this.p.HasExited) @@ -67,7 +72,7 @@ namespace NadekoBot.Services.Music lock (locker) written = _outStream.Write(buffer, 0, bytesRead); if (!written) - await Task.Delay(32, cancelToken); + await Task.Delay(2000, cancelToken); } while (!written); lock (locker) @@ -75,9 +80,15 @@ namespace NadekoBot.Services.Music if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); - await Task.Delay(5); // @.@ + _log.Info(_outStream.Length); + await Task.Delay(10); } - _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); + if (cancelToken.IsCancellationRequested) + _log.Info("Song canceled"); + else if (p.HasExited) + _log.Info("Song buffered completely (FFmpeg exited)"); + else if (bytesRead == 0) + _log.Info("Nothing read"); } catch (System.ComponentModel.Win32Exception) { @@ -119,8 +130,14 @@ Check the guides for your platform on how to setup ffmpeg correctly: { _log.Error(ex); } - try { this.p.Kill(); } - catch { } + try + { + if(!this.p.HasExited) + this.p.Kill(); + } + catch + { + } _outStream.Dispose(); this.p.Dispose(); } From fc941770e9494a90ceef9137791b254c099a4430 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 23:59:56 +0200 Subject: [PATCH 30/40] Removed spam --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index d0d550b3..7f79eac5 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -80,7 +80,7 @@ namespace NadekoBot.Services.Music if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); - _log.Info(_outStream.Length); + //_log.Info(_outStream.Length); await Task.Delay(10); } if (cancelToken.IsCancellationRequested) From 684dba0d9c3b6de5a13a7876151595f995daba7c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 00:21:45 +0200 Subject: [PATCH 31/40] added some debugging --- src/NadekoBot/Services/Music/SongBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 7f79eac5..5ff91e88 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -78,7 +78,7 @@ namespace NadekoBot.Services.Music lock (locker) if (_outStream.Length > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished"); + _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); //_log.Info(_outStream.Length); await Task.Delay(10); From 842b45178d9f0eeadced464d0414bc2e1e2588e9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 10:16:18 +0200 Subject: [PATCH 32/40] Added reconnect arguments to ffmpeg --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5ff91e88..f6f72b4e 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, From 556174ec897f72207c7fda8dab88b291acc9dc79 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 11:23:34 +0200 Subject: [PATCH 33/40] Restart ffmpeg if it crashes? Maybe i should reconsider file-based cache. Ffmpeg doesn't like being slowed down it seems --- src/NadekoBot/Services/Music/SongBuffer.cs | 100 +++++++++++++-------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f6f72b4e..f962f919 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -19,20 +19,14 @@ namespace NadekoBot.Services.Music public string SongUri { get; private set; } + private volatile bool restart = false; + public SongBuffer(string songUri, string skipTo) { _log = LogManager.GetCurrentClassLogger(); this.SongUri = songUri; - this.p = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }); + this.p = StartFFmpegProcess(songUri, 0); var t = Task.Run(() => { this.p.BeginErrorReadLine(); @@ -41,9 +35,27 @@ namespace NadekoBot.Services.Music }); } + private Process StartFFmpegProcess(string songUri, float skipTo = 0) + { + return Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }); + } + private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { _log.Error(">>> " + e.Data); + if (e.Data.Contains("Error in the pull function")) + { + _log.Info("Got error in the pull function!"); + restart = true; + } } private readonly object locker = new object(); @@ -56,39 +68,57 @@ namespace NadekoBot.Services.Music var sw = Stopwatch.StartNew(); var delay = 1000 / maxLoopsPerSec; int currentLoops = 0; + int _bytesSent = 0; try { - ++currentLoops; - byte[] buffer = new byte[readSize]; - int bytesRead = 1; - while (!cancelToken.IsCancellationRequested && !this.p.HasExited) + do { - bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - break; - bool written; - do + if (restart) { - lock (locker) - written = _outStream.Write(buffer, 0, bytesRead); - if (!written) - await Task.Delay(2000, cancelToken); + var cur = _bytesSent / 3840 / (1000 / 20.0f); + _log.Info("Restarting"); + try { this.p.StandardOutput.Dispose(); } catch { } + try { this.p.Dispose(); } catch { } + this.p = StartFFmpegProcess(SongUri, cur); } - while (!written); - lock (locker) - if (_outStream.Length > 200_000 || bytesRead == 0) - if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); + restart = false; + ++currentLoops; + byte[] buffer = new byte[readSize]; + int bytesRead = 1; + while (!cancelToken.IsCancellationRequested && !this.p.HasExited) + { + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); + _bytesSent += bytesRead; + if (bytesRead == 0) + break; + bool written; + do + { + lock (locker) + written = _outStream.Write(buffer, 0, bytesRead); + if (!written) + await Task.Delay(2000, cancelToken); + } + while (!written); + lock (locker) + if (_outStream.Length > 200_000 || bytesRead == 0) + if (toReturn.TrySetResult(true)) + _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); - //_log.Info(_outStream.Length); - await Task.Delay(10); + //_log.Info(_outStream.Length); + await Task.Delay(10); + } + if (cancelToken.IsCancellationRequested) + _log.Info("Song canceled"); + else if (p.HasExited) + _log.Info("Song buffered completely (FFmpeg exited)"); + else if (bytesRead == 0) + _log.Info("Nothing read"); + + if (restart) + _log.Info("Lets do some magix"); } - if (cancelToken.IsCancellationRequested) - _log.Info("Song canceled"); - else if (p.HasExited) - _log.Info("Song buffered completely (FFmpeg exited)"); - else if (bytesRead == 0) - _log.Info("Nothing read"); + while (restart && !cancelToken.IsCancellationRequested); } catch (System.ComponentModel.Win32Exception) { From b1a4aa6a2109249a821a4f61afe6e5715ec3a1b3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 11:27:48 +0200 Subject: [PATCH 34/40] Fix --- src/NadekoBot/Services/Music/SongBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f962f919..29979b59 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -51,7 +51,7 @@ namespace NadekoBot.Services.Music private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { _log.Error(">>> " + e.Data); - if (e.Data.Contains("Error in the pull function")) + if (e.Data?.Contains("Error in the pull function") == true) { _log.Info("Got error in the pull function!"); restart = true; From a6d432de7f275d101be89621989744e17ef9a210 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 12:38:01 +0200 Subject: [PATCH 35/40] Try ignoring errors --- src/NadekoBot/Services/Music/SongBuffer.cs | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 29979b59..0bf6f089 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + Arguments = $"-ss {skipTo:F4} -err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -64,24 +64,24 @@ namespace NadekoBot.Services.Music var toReturn = new TaskCompletionSource(); var _ = Task.Run(async () => { - int maxLoopsPerSec = 25; + //int maxLoopsPerSec = 25; var sw = Stopwatch.StartNew(); - var delay = 1000 / maxLoopsPerSec; + //var delay = 1000 / maxLoopsPerSec; int currentLoops = 0; int _bytesSent = 0; try { - do - { - if (restart) - { - var cur = _bytesSent / 3840 / (1000 / 20.0f); - _log.Info("Restarting"); - try { this.p.StandardOutput.Dispose(); } catch { } - try { this.p.Dispose(); } catch { } - this.p = StartFFmpegProcess(SongUri, cur); - } - restart = false; + //do + //{ + // if (restart) + // { + // var cur = _bytesSent / 3840 / (1000 / 20.0f); + // _log.Info("Restarting"); + // try { this.p.StandardOutput.Dispose(); } catch { } + // try { this.p.Dispose(); } catch { } + // this.p = StartFFmpegProcess(SongUri, cur); + // } + // restart = false; ++currentLoops; byte[] buffer = new byte[readSize]; int bytesRead = 1; @@ -117,8 +117,8 @@ namespace NadekoBot.Services.Music if (restart) _log.Info("Lets do some magix"); - } - while (restart && !cancelToken.IsCancellationRequested); + //} + //while (restart && !cancelToken.IsCancellationRequested); } catch (System.ComponentModel.Win32Exception) { From 17158d5e8095ebf73268980e5f31764bc8cb43f5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 13:05:23 +0200 Subject: [PATCH 36/40] Testing something --- src/NadekoBot/Services/CommandHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index a1de8871..4c46d46d 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -187,6 +187,7 @@ namespace NadekoBot.Services private async Task MessageReceivedHandler(SocketMessage msg) { + await Task.Delay(1500); try { if (msg.Author.IsBot || !_bot.Ready) //no bots, wait until bot connected and initialized From 8b72447b0f917ddef656d09ad7320a05919a33ee Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:32:38 +0200 Subject: [PATCH 37/40] Removed testing delay --- src/NadekoBot/Services/CommandHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 4c46d46d..a1de8871 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -187,7 +187,6 @@ namespace NadekoBot.Services private async Task MessageReceivedHandler(SocketMessage msg) { - await Task.Delay(1500); try { if (msg.Author.IsBot || !_bot.Ready) //no bots, wait until bot connected and initialized From d5903a1e25552ddf5dc00130cae7ed439d4620cf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:38:19 +0200 Subject: [PATCH 38/40] Readded current time and song durations --- src/NadekoBot/Modules/Music/Music.cs | 10 ++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 45 +++++++++++++++++++- src/NadekoBot/Services/Music/MusicService.cs | 2 +- src/NadekoBot/Services/Music/SongBuffer.cs | 17 +++----- src/NadekoBot/Services/Music/SongInfo.cs | 20 +++++++-- 5 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 8f80e726..4d3584c5 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -75,7 +75,6 @@ namespace NadekoBot.Modules.Music return; } - ////if some other user moved //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause // player.Paused && @@ -232,7 +231,8 @@ namespace NadekoBot.Modules.Music if (--page < -1) return; - //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; @@ -604,13 +604,13 @@ namespace NadekoBot.Modules.Music var (_, currentSong) = mp.Current; if (currentSong == null) return; - //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } var embed = new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) .WithDescription(currentSong.PrettyName) .WithThumbnailUrl(currentSong.Thumbnail) - .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + /*currentSong.PrettyFullTime +*/ $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -847,7 +847,7 @@ namespace NadekoBot.Modules.Music else await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); } - + //todo readd goto //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] //public async Task Goto(int time) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index fc11e1ab..9dd806e1 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -7,6 +7,7 @@ using NLog; using System.Linq; using System.Collections.Concurrent; using NadekoBot.Extensions; +using System.Diagnostics; namespace NadekoBot.Services.Music { @@ -28,10 +29,24 @@ namespace NadekoBot.Services.Music public bool Exited { get; set; } = false; public bool Stopped { get; private set; } = false; public float Volume { get; private set; } = 1.0f; - public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; public bool Paused => pauseTaskSource != null; private TaskCompletionSource pauseTaskSource { get; set; } = null; + public string PrettyVolume => $"πŸ”‰ {(int)(Volume * 100)}%"; + public string PrettyCurrentTime + { + get + { + var time = CurrentTime.ToString(@"mm\:ss"); + var hrs = (int)CurrentTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + else + return time; + } + } + public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?"); private CancellationTokenSource SongCancelSource { get; set; } public ITextChannel OutputTextChannel { get; set; } public (int Index, SongInfo Current) Current @@ -96,11 +111,12 @@ namespace NadekoBot.Services.Music private bool manualSkip = false; private bool manualIndex = false; private bool newVoiceChannel = false; + private readonly IGoogleApiService _google; private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); public TimeSpan TotalPlaytime => TimeSpan.MaxValue; - public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) + public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); this.Volume = volume; @@ -108,6 +124,7 @@ namespace NadekoBot.Services.Music this.SongCancelSource = new CancellationTokenSource(); this.OutputTextChannel = output; this._musicService = musicService; + this._google = google; _player = Task.Run(async () => { @@ -530,6 +547,30 @@ namespace NadekoBot.Services.Music } } + public async Task UpdateSongDurationsAsync() + { + var sw = Stopwatch.StartNew(); + var (_, songs) = Queue.ToArray(); + var toUpdate = songs + .Where(x => x.ProviderType == Database.Models.MusicType.YouTube + && x.TotalTime == TimeSpan.Zero); + + var vIds = toUpdate.Select(x => x.VideoId); + + sw.Stop(); + _log.Info(sw.Elapsed.TotalSeconds); + if (!vIds.Any()) + return; + + var durations = await _google.GetVideoDurationsAsync(vIds); + + foreach (var x in toUpdate) + { + if (durations.TryGetValue(x.VideoId, out var dur)) + x.TotalTime = dur; + } + } + //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 2ed203ac..bcd00b29 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -88,7 +88,7 @@ namespace NadekoBot.Services.Music return MusicPlayers.GetOrAdd(guildId, _ => { var vol = GetDefaultVolume(guildId); - var mp = new MusicPlayer(this, voiceCh, textCh, vol); + var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol); IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 0bf6f089..af6bac8b 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -53,7 +53,7 @@ namespace NadekoBot.Services.Music _log.Error(">>> " + e.Data); if (e.Data?.Contains("Error in the pull function") == true) { - _log.Info("Got error in the pull function!"); + _log.Error("Ignore this."); restart = true; } } @@ -108,15 +108,12 @@ namespace NadekoBot.Services.Music //_log.Info(_outStream.Length); await Task.Delay(10); } - if (cancelToken.IsCancellationRequested) - _log.Info("Song canceled"); - else if (p.HasExited) - _log.Info("Song buffered completely (FFmpeg exited)"); - else if (bytesRead == 0) - _log.Info("Nothing read"); - - if (restart) - _log.Info("Lets do some magix"); + //if (cancelToken.IsCancellationRequested) + // _log.Info("Song canceled"); + //else if (p.HasExited) + // _log.Info("Song buffered completely (FFmpeg exited)"); + //else if (bytesRead == 0) + // _log.Info("Nothing read"); //} //while (restart && !cancelToken.IsCancellationRequested); } diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 22accf2f..04d48d55 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Music public string Uri { get; set; } public string AlbumArt { get; set; } public string QueuerName { get; set; } - public TimeSpan TotalTime = TimeSpan.Zero; + public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; public string PrettyProvider => (Provider ?? "???"); //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; @@ -60,7 +60,20 @@ namespace NadekoBot.Services.Music } } } - private string videoId = null; + private string _videoId = null; + public string VideoId + { + get + { + if (ProviderType == MusicType.YouTube) + return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString(); + + return _videoId ?? ""; + } + + set => _videoId = value; + } + private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); public string Thumbnail { @@ -71,8 +84,7 @@ namespace NadekoBot.Services.Music case MusicType.Radio: return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links case MusicType.YouTube: - videoId = videoId ?? videoIdRegex.Match(Query)?.ToString(); - return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + return $"https://img.youtube.com/vi/{ VideoId }/0.jpg"; case MusicType.Local: return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links case MusicType.Soundcloud: From c33c2bce60e5b1f7c480ca3eda1bbc7be40aa6fa Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:42:07 +0200 Subject: [PATCH 39/40] Readded total playtime --- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 9dd806e1..b810c413 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -114,7 +114,17 @@ namespace NadekoBot.Services.Music private readonly IGoogleApiService _google; private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - public TimeSpan TotalPlaytime => TimeSpan.MaxValue; + public TimeSpan TotalPlaytime + { + get + { + var songs = Queue.ToArray().Songs; + return songs.Any(s => s.TotalTime == TimeSpan.MaxValue) + ? TimeSpan.MaxValue + : new TimeSpan(songs.Sum(s => s.TotalTime.Ticks)); + } + } + public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume) { From 9bb8f3d666124fd3bc93cc32435be6c8f7a4d247 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 23:38:11 +0200 Subject: [PATCH 40/40] .ms readded --- src/NadekoBot/Modules/Music/Music.cs | 72 +++++++++------------ src/NadekoBot/Services/Music/MusicPlayer.cs | 7 +- src/NadekoBot/Services/Music/MusicQueue.cs | 18 +++++- src/NadekoBot/Services/Music/SongBuffer.cs | 8 ++- 4 files changed, 58 insertions(+), 47 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 4d3584c5..d8bc759d 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -45,6 +45,7 @@ namespace NadekoBot.Modules.Music var t = _music.DestroyPlayer(arg.Id); return Task.CompletedTask; } + //todo changing server region is bugged again private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) { @@ -725,7 +726,7 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public void Move() + public async Task Move() { var vch = ((IGuildUser)Context.User).VoiceChannel; @@ -737,53 +738,42 @@ namespace NadekoBot.Modules.Music if (mp == null) return; - mp.SetVoiceChannel(vch); + await mp.SetVoiceChannel(vch); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task MoveSong([Remainder] string fromto) - //{ - // if (string.IsNullOrWhiteSpace(fromto)) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task MoveSong([Remainder] string fromto) + { + if (string.IsNullOrWhiteSpace(fromto)) + return; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + MusicPlayer mp = _music.GetPlayerOrDefault(Context.Guild.Id); + if (mp == null) + return; - // fromto = fromto?.Trim(); - // var fromtoArr = fromto.Split('>'); + fromto = fromto?.Trim(); + var fromtoArr = fromto.Split('>'); - // int n1; - // int n2; + SongInfo s; + if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) || + !int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2 + || (s = mp.MoveSong(n1, n2)) == null) + { + await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); + return; + } - // var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); + var embed = new EmbedBuilder() + .WithTitle(s.Title.TrimTo(65)) + .WithUrl(s.SongUrl) + .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) + .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + .WithColor(NadekoBot.OkColor); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - // if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || - // !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || - // n1 > playlist.Count || n2 > playlist.Count) - // { - // await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); - // return; - // } - - // var s = playlist[n1 - 1]; - // playlist.Insert(n2 - 1, s); - // var nn1 = n2 < n1 ? n1 : n1 - 1; - // playlist.RemoveAt(nn1); - - // var embed = new EmbedBuilder() - // .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") - // .WithUrl(s.SongUrl) - // .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - // .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - // .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) - // .WithColor(NadekoBot.OkColor); - // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - // //await channel.SendConfirmAsync($"🎡Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b810c413..2064fb45 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -365,7 +365,6 @@ namespace NadekoBot.Services.Music public int Enqueue(SongInfo song) { - _log.Info("Adding song"); lock (locker) { if (Exited) @@ -547,7 +546,7 @@ namespace NadekoBot.Services.Music } } - public void SetVoiceChannel(IVoiceChannel vch) + public async Task SetVoiceChannel(IVoiceChannel vch) { lock (locker) { @@ -555,6 +554,7 @@ namespace NadekoBot.Services.Music return; VoiceChannel = vch; } + _audioClient = await vch.ConnectAsync(); } public async Task UpdateSongDurationsAsync() @@ -581,6 +581,9 @@ namespace NadekoBot.Services.Music } } + public SongInfo MoveSong(int n1, int n2) + => Queue.MoveSong(n1, n2); + //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 967a5197..c8890484 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -9,7 +9,7 @@ namespace NadekoBot.Services.Music { public class MusicQueue : IDisposable { - private LinkedList Songs { get; } = new LinkedList(); + private LinkedList Songs { get; set; } = new LinkedList(); private int _currentIndex = 0; public int CurrentIndex { @@ -147,5 +147,21 @@ namespace NadekoBot.Services.Music CurrentIndex = new NadekoRandom().Next(Songs.Count); } } + + public SongInfo MoveSong(int n1, int n2) + { + lock (locker) + { + var playlist = Songs.ToList(); + if (n1 > playlist.Count || n2 > playlist.Count) + return null; + var s = playlist[n1 - 1]; + playlist.Insert(n2 - 1, s); + var nn1 = n2 < n1 ? n1 : n1 - 1; + playlist.RemoveAt(nn1); + Songs = new LinkedList(playlist); + return s; + } + } } } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index af6bac8b..a3ec23a4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -19,7 +19,7 @@ namespace NadekoBot.Services.Music public string SongUri { get; private set; } - private volatile bool restart = false; + //private volatile bool restart = false; public SongBuffer(string songUri, string skipTo) { @@ -50,11 +50,13 @@ namespace NadekoBot.Services.Music private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { + if (string.IsNullOrWhiteSpace(e.Data)) + return; _log.Error(">>> " + e.Data); if (e.Data?.Contains("Error in the pull function") == true) { _log.Error("Ignore this."); - restart = true; + //restart = true; } } @@ -99,7 +101,7 @@ namespace NadekoBot.Services.Music if (!written) await Task.Delay(2000, cancelToken); } - while (!written); + while (!written && !cancelToken.IsCancellationRequested); lock (locker) if (_outStream.Length > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true))