diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs new file mode 100644 index 00000000..acb7db8e --- /dev/null +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -0,0 +1,116 @@ +using System; +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; + public int Capacity { get; } + + private int _readPos = 0; + private int ReadPos + { + get => _readPos; + set => _readPos = value; + } + private int _writePos = 0; + private int WritePos + { + get => _writePos; + set => _writePos = value; + } + public int Length => ReadPos <= WritePos + ? WritePos - ReadPos + : Capacity - (ReadPos - WritePos); + + public int RemainingCapacity + { + get => Capacity - Length - 1; + } + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + public PoopyRingBuffer(int capacity = 81920 * 100) + { + this.Capacity = capacity + 1; + this.buffer = new byte[this.Capacity]; + } + + public int Read(byte[] b, int offset, int toRead) + { + if (WritePos == ReadPos) + return 0; + + if (toRead > Length) + toRead = Length; + + if (WritePos > ReadPos) + { + Array.Copy(buffer, ReadPos, b, offset, toRead); + ReadPos += toRead; + } + else + { + 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 bool Write(byte[] b, int offset, int toWrite) + { + while (toWrite > RemainingCapacity) + return false; + + if (toWrite == 0) + return true; + + 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) + { + Array.Copy(b, offset + firstWrite, buffer, 0, secondWrite); + WritePos = secondWrite; + } + else + { + WritePos += firstWrite; + if (WritePos == Capacity) + WritePos = 0; + } + } + return true; + } + + public void Dispose() + { + + } + } +} 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/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/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/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 56a0affc..fafd9f48 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -134,9 +134,8 @@ namespace NadekoBot.Modules.Administration await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { - Console.WriteLine(ex); await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ebec33c5..d8bc759d 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -1,20 +1,20 @@ 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; +using System.IO; +using System.Net.Http; +using Newtonsoft.Json.Linq; namespace NadekoBot.Modules.Music { @@ -35,139 +35,133 @@ 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.LeftGuild += _client_LeftGuild; } + private Task _client_LeftGuild(SocketGuild arg) + { + 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 usr = iusr as SocketGuildUser; - if (usr == null || - oldState.VoiceChannel == newState.VoiceChannel) - return Task.CompletedTask; + 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); + if (player == null) + return; + + 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(); + + // 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; + //} + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) + { + if (songInfo == null) + { + if(!silent) + await ReplyErrorLocalized("song_not_found").ConfigureAwait(false); + return; + } + + int index; try { - //if bot moved - if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - usr.Id == _client.CurrentUser.Id) + index = mp.Enqueue(songInfo); + } + catch (QueueFullException) + { + await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); + throw; + } + if (index != -1) + { + 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 mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) + .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") + .WithThumbnailUrl(songInfo.Thumbnail) + .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) + .ConfigureAwait(false); + if (mp.Stopped) + { + (await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); + } + queuedMessage?.DeleteAfter(10); + } + catch + { + // ignored + } } - - - //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 + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Play([Remainder] string query = null) + { + var mp = await _music.GetOrCreatePlayer(Context); + if (string.IsNullOrWhiteSpace(query)) { - // 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); + await Next(); } + else if (int.TryParse(query, out var index)) + if (index >= 1) + mp.SetIndex(index - 1); + else + return; else { - await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + try + { + await Queue(query); + } + catch { } } } @@ -175,7 +169,11 @@ 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()); + + try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } + if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); @@ -217,76 +215,85 @@ 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) + public async Task ListQueue(int page = 0) { - 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); - } - } + var mp = await _music.GetOrCreatePlayer(Context); + var (current, songs) = mp.QueueArray(); - [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) + if (!songs.Any()) { await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; } - - if (--page < 0) + + if (--page < -1) return; - - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + try { await mp.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, + 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 = mp.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 maxPlaytime = mp.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}")); + .Select(v => + { + if(number++ == current) + return $"**⇒**`{number}.` {v.PrettyFullName}"; + else + return $"`{number}.` {v.PrettyFullName}"; + })); + + desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; + + 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) + add += "🔀 " + GetText("shuffling_playlist") + "\n"; + else + { + 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)) + desc = add + "\n" + desc; - desc = $"`🔊` {currentSong.PrettyFullName}\n\n" + desc; - - if (musicPlayer.RepeatSong) - desc = "🔂 " + GetText("repeating_cur_song") +"\n\n" + desc; - else if (musicPlayer.RepeatPlaylist) - desc = "🔁 " + GetText("repeating_playlist")+"\n\n" + desc; - - - var embed = new EmbedBuilder() .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; @@ -296,41 +303,51 @@ 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(skipCount); + } - 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 async Task Destroy() + { + await _music.DestroyPlayer(Context.Guild.Id); + } + + [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 +367,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)).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) + { + await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); + } } + 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 +423,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,48 +459,427 @@ 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) + var mp = await _music.GetOrCreatePlayer(Context); + + 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(); + + 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()))); + } + + private static readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Load([Remainder] int id) + { + if (!PlaylistLoadBlacklist.Add(Context.Guild.Id)) return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + try + { + var mp = await _music.GetOrCreatePlayer(Context); + 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) + { + try + { + await Task.Yield(); + + 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 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); + } + } + + [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); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudPl([Remainder] string pl) + { + pl = pl?.Trim(); + + if (string.IsNullOrWhiteSpace(pl)) return; - if (time < 0) - return; - - var currentSong = musicPlayer.CurrentSong; + 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(); + 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 (Exception ex) + { + _log.Warn(ex); + 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 NowPlaying() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; if (currentSong == null) return; + try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } - //currentSong.PrintStatusMessage = false; - var gotoSong = currentSong.Clone(); - gotoSong.SkipTo = time; - musicPlayer.AddSong(gotoSong, 0); - musicPlayer.Next(); + 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 + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); - 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(embed).ConfigureAwait(false); } + [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)] + public async Task Playlist([Remainder] string playlist) + { + if (string.IsNullOrWhiteSpace(playlist)) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + + 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; + + await Task.WhenAll(Task.Delay(100), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); + } + catch (SongNotFoundException) { } + catch { break; } + } + + await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + } + + + [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); + } + + [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); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task LocalPl([Remainder] string dirPath) + { + if (string.IsNullOrWhiteSpace(dirPath)) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + + 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 + { + await Task.Yield(); + 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)] + public async Task Move() + { + var vch = ((IGuildUser)Context.User).VoiceChannel; + + if (vch == null) + return; + + var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + + if (mp == null) + return; + + await mp.SetVoiceChannel(vch); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task MoveSong([Remainder] string fromto) + { + if (string.IsNullOrWhiteSpace(fromto)) + return; + + MusicPlayer mp = _music.GetPlayerOrDefault(Context.Guild.Id); + if (mp == null) + return; + + fromto = fromto?.Trim(); + var fromtoArr = fromto.Split('>'); + + 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 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); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxQueue(uint size = 0) + { + if (size < 0) + return; + var mp = await _music.GetOrCreatePlayer(Context); + + mp.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 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); + } + + [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() + { + 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); + } + //todo readd goto + //[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; + var mp = await _music.GetOrCreatePlayer(Context); - if (!musicPlayer.ToggleAutoplay()) + if (!mp.ToggleAutoplay()) await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); else await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); @@ -855,17 +890,11 @@ namespace NadekoBot.Modules.Music [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/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 200487b2..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; @@ -183,7 +184,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 @@ -212,8 +213,6 @@ namespace NadekoBot var pokemonService = new PokemonService(); #endregion - - //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() .Add(Localization) @@ -269,7 +268,6 @@ namespace NadekoBot .Add(this) .Build(); - CommandHandler.AddServices(Services); //setup typereaders @@ -352,7 +350,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/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 8ddcf04d..e3504acb 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 sh plsh Shuffles the current playlist. @@ -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 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 5` or `{0}play Dream Of Venice` + stop s 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/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 9e33aef7..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; @@ -35,7 +34,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/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/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/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..8d4dab72 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,19 +2,27 @@ namespace NadekoBot.Services.Music { - 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.") { } } - 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..2064fb45 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -0,0 +1,593 @@ +using Discord; +using Discord.Audio; +using System; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using System.Linq; +using System.Collections.Concurrent; +using NadekoBot.Extensions; +using System.Diagnostics; + +namespace NadekoBot.Services.Music +{ + public enum StreamState + { + Resolving, + Queued, + Playing, + Completed + } + public class MusicPlayer + { + private readonly Task _player; + public IVoiceChannel VoiceChannel { get; private set; } + 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 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 + { + get + { + if (Stopped) + return (0, null); + return Queue.Current; + } + } + + 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; + public uint MaxQueueSize + { + get => Queue.MaxQueueSize; + 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; } + + + 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(); + private MusicService _musicService; + + #region events + 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 readonly IGoogleApiService _google; + + private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + 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) + { + _log = LogManager.GetCurrentClassLogger(); + this.Volume = volume; + this.VoiceChannel = vch; + this.SongCancelSource = new CancellationTokenSource(); + this.OutputTextChannel = output; + this._musicService = musicService; + this._google = google; + + _player = Task.Run(async () => + { + while (!Exited) + { + _bytesSent = 0; + CancellationToken cancelToken; + (int Index, SongInfo Song) data; + lock (locker) + { + data = Queue.Current; + cancelToken = SongCancelSource.Token; + manualSkip = false; + manualIndex = false; + } + 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); + } + } + 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); + } + + 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 || + newVoiceChannel) + try + { + try + { + var t = _audioClient?.StopAsync(); + if (t != null) + { + await t; + _audioClient.Dispose(); + } + } + catch + { + } + newVoiceChannel = false; + var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); + if (curUser.VoiceChannel != null) + { + var ac = await VoiceChannel.ConnectAsync(); + await ac.StopAsync(); + await Task.Delay(1000); + } + _audioClient = await VoiceChannel.ConnectAsync(); + } + catch + { + return null; + } + return _audioClient; + } + + public int Enqueue(SongInfo song) + { + 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 + if (!Stopped) + Queue.Next(skipCount - 1); + 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() + { + lock (locker) + { + 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)); + lock (locker) + { + 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() + { + lock (locker) + return 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 async Task Destroy() + { + _log.Info("Destroying"); + lock (locker) + { + Stop(); + Exited = true; + Unpause(); + + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; + } + var ac = _audioClient; + if (ac != null) + 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 async Task SetVoiceChannel(IVoiceChannel vch) + { + lock (locker) + { + if (Exited) + return; + VoiceChannel = vch; + } + _audioClient = await vch.ConnectAsync(); + } + + 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; + } + } + + 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) ? + // TimeSpan.MaxValue : + // 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 new file mode 100644 index 00000000..c8890484 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -0,0 +1,167 @@ +using NadekoBot.Extensions; +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; set; } = new LinkedList(); + private int _currentIndex = 0; + public 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; + } + } + } + + 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 && Songs.Count >= MaxQueueSize) + throw new QueueFullException(); + Songs.AddLast(song); + } + } + + public void Next(int skipCount = 1) + { + lock(locker) + CurrentIndex += skipCount; + } + + 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 CurrentIndex, SongInfo[] Songs) ToArray() + { + lock (locker) + { + return (CurrentIndex, Songs.ToArray()); + } + } + + public void ResetCurrent() + { + lock (locker) + { + CurrentIndex = 0; + } + } + + public void Random() + { + lock (locker) + { + 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/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 9ea7a958..bcd00b29 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -5,12 +5,14 @@ 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; +using Discord.WebSocket; +using System.Text.RegularExpressions; +using System.Net.Http; namespace NadekoBot.Services.Music { @@ -26,13 +28,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, - NadekoStrings strings, ILocalization localization, DbService db, + 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; @@ -48,28 +52,44 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - public MusicPlayer GetPlayer(ulong guildId) + 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, _google, voiceCh, textCh, vol); + IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; mp.OnCompleted += async (s, song) => @@ -90,31 +110,19 @@ 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); - } } 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; @@ -123,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(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 @@ -133,7 +141,7 @@ namespace NadekoBot.Services.Music // ignored } }; - mp.OnPauseChanged += async (paused) => + mp.OnPauseChanged += async (player, paused) => { try { @@ -150,195 +158,177 @@ 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; }); } - - public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + public MusicPlayer GetPlayerOrDefault(ulong guildId) { - string GetText(string text, params object[] replacements) => - _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - - 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)); - - var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - Song resolvedSong; - try - { - musicPlayer.ThrowIfQueueFull(); - resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); - - 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 - } + if (MusicPlayers.TryGetValue(guildId, out var mp)) + return mp; + else + return null; } - public void DestroyPlayer(ulong id) + public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch) { - if (MusicPlayers.TryRemove(id, out var mp)) - mp.Destroy(); + 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.YouTube); + if (si == null) + throw new SongNotFoundException(); + var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); + mp.Enqueue(si); } - - public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType? musicType = null) { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); + query.ThrowIfNull(nameof(query)); - if (musicType != MusicType.Local && IsRadioLink(query)) + SongInfo sinfo = null; + switch (musicType) { - musicType = MusicType.Radio; - query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; + 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; } - try + 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 { - 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) }; - } + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = await svideo.StreamLink().ConfigureAwait(false), + ProviderType = MusicType.Soundcloud, + Query = svideo.TrackLink, + AlbumArt = svideo.artwork_url, + QueuerName = queuerName + }; - 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) + public SongInfo ResolveLocalSong(string query, string queuerName) + { + return new SongInfo { - _log.Warn($"Failed resolving the link.{ex.Message}"); - _log.Warn(ex); + 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)) + { + _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 + .Where(v => v.AudioBitrate < 256) + .OrderByDescending(v => v.AudioBitrate) + .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" + Provider = "YouTube", + Uri = await video.GetUriAsync().ConfigureAwait(false), + Query = link, + 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(); + } + + 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); + private async Task HandleStreamContainers(string query) { string file = null; @@ -359,7 +349,7 @@ namespace NadekoBot.Services.Music //Regex.Match(query) try { - var m = Regex.Match(file, "File1=(?.*?)\\n"); + var m = plsRegex.Match(file); var res = m.Groups["url"]?.ToString(); return res?.Trim(); } @@ -378,7 +368,7 @@ namespace NadekoBot.Services.Music */ try { - var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); + var m = m3uRegex.Match(file); var res = m.Groups["url"]?.ToString(); return res?.Trim(); } @@ -394,7 +384,7 @@ namespace NadekoBot.Services.Music // try { - var m = Regex.Match(file, ".*?)\""); + var m = asxRegex.Match(file); var res = m.Groups["url"]?.ToString(); return res?.Trim(); } @@ -414,7 +404,7 @@ namespace NadekoBot.Services.Music */ try { - var m = Regex.Match(file, "(?.*?)"); + var m = xspfRegex.Match(file); var res = m.Groups["url"]?.ToString(); return res?.Trim(); } @@ -427,14 +417,5 @@ namespace NadekoBot.Services.Music 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..a9567f9c 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -1,296 +1,245 @@ -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 + // { + // 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..a3ec23a4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,4 +1,4 @@ -using NadekoBot.Extensions; +using NadekoBot.DataStructures; using NLog; using System; using System.Diagnostics; @@ -8,212 +8,378 @@ 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 class SongBuffer : IDisposable { - public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) + const int readSize = 81920; + private Process p; + private PoopyRingBuffer _outStream = new PoopyRingBuffer(); + + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly Logger _log; + + public string SongUri { get; private set; } + + //private volatile bool restart = false; + + public SongBuffer(string songUri, string skipTo) { - MusicPlayer = musicPlayer; - Basename = basename; - SongInfo = songInfo; - SkipTo = skipTo; - MaxFileSize = maxFileSize; - CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); _log = LogManager.GetCurrentClassLogger(); + this.SongUri = songUri; + + this.p = StartFFmpegProcess(songUri, 0); + var t = Task.Run(() => + { + this.p.BeginErrorReadLine(); + this.p.ErrorDataReceived += P_ErrorDataReceived; + this.p.WaitForExit(); + }); } - MusicPlayer MusicPlayer { get; } + private Process StartFFmpegProcess(string songUri, float skipTo = 0) + { + return Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + 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, + CreateNoWindow = true, + }); + } - private string Basename { get; } + 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; + } + } - private SongInfo SongInfo { get; } + private readonly object locker = new object(); + public Task StartBuffering(CancellationToken cancelToken) + { + var toReturn = new TaskCompletionSource(); + var _ = Task.Run(async () => + { + //int maxLoopsPerSec = 25; + var sw = Stopwatch.StartNew(); + //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; + ++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 && !cancelToken.IsCancellationRequested); + lock (locker) + if (_outStream.Length > 200_000 || bytesRead == 0) + if (toReturn.TrySetResult(true)) + _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); - private int SkipTo { get; } - - private int MaxFileSize { get; } = 2.MiB(); - - private long FileNumber = -1; - - private long NextFileToRead = 0; - - public bool BufferingCompleted { get; private set; } = false; - - private ulong CurrentBufferSize = 0; - - 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, - }); - - 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. + //_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"); + //} + //while (restart && !cancelToken.IsCancellationRequested); + } + 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"); - 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; - - 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 void CleanFiles() - { - for (long i = NextFileToRead - 1; i <= FileNumber; i++) - { - try + catch (OperationCanceledException) { } + catch (InvalidOperationException) { } // when ffmpeg is disposed + catch (Exception ex) { - File.Delete(Basename + "-" + i); + _log.Info(ex); } - catch { } - } - } - - //Stream part - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => (long)CurrentBufferSize; - - public override long Position { get; set; } = 0; - - 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()) + finally { - CurrentFileStream.Dispose(); - CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - read += CurrentFileStream.Read(buffer, read + offset, count - read); + if (toReturn.TrySetResult(false)) + _log.Info("Prebuffering failed"); } - if (read < count) - Array.Clear(buffer, read, count - read); + }, cancelToken); + + return toReturn.Task; + } + + public int Read(byte[] b, int offset, int toRead) + { + lock (locker) + return _outStream.Read(b, offset, toRead); + } + + public void Dispose() + { + try + { + this.p.StandardOutput.Dispose(); } - return read; - } - - public override long Seek(long offset, SeekOrigin origin) - { - 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 new void Dispose() - { - CurrentFileStream.Dispose(); - MusicPlayer.SongCancelSource.Cancel(); - CleanFiles(); - base.Dispose(); + catch (Exception ex) + { + _log.Error(ex); + } + try + { + if(!this.p.HasExited) + this.p.Kill(); + } + catch + { + } + _outStream.Dispose(); + this.p.Dispose(); } } -} \ No newline at end of file +} + +//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; } + +// private string Basename { get; } + +// private SongInfo SongInfo { get; } + +// private int SkipTo { get; } + +// private int MaxFileSize { get; } = 2.MiB(); + +// private long FileNumber = -1; + +// private long NextFileToRead = 0; + +// public bool BufferingCompleted { get; private set; } = false; + +// private ulong CurrentBufferSize = 0; + +// 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, +// }); + +// 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(); +// 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; + +// 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 void CleanFiles() +// { +// for (long i = NextFileToRead - 1; i <= FileNumber; i++) +// { +// try +// { +// File.Delete(Basename + "-" + i); +// } +// catch { } +// } +// } + +// //Stream part + +// public override bool CanRead => true; + +// public override bool CanSeek => false; + +// public override bool CanWrite => false; + +// public override long Length => (long)CurrentBufferSize; + +// public override long Position { get; set; } = 0; + +// 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 long Seek(long offset, SeekOrigin origin) +// { +// 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 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..04d48d55 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -0,0 +1,98 @@ +using Discord; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using System; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +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 { get; set; } = 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.YouTube: + 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 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 + { + get + { + switch (ProviderType) + { + case MusicType.Radio: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + case MusicType.YouTube: + 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; + // } + // } + } +} diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index e7a3b885..fa4bea1a 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -404,9 +404,10 @@ "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", + "music_fairplay": "Fairplay", "music_finished_song": "Finished song", "music_fp_disabled": "Fair play disabled.", "music_fp_enabled": "Fair play enabled.", @@ -424,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.", @@ -437,19 +438,24 @@ "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_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", "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_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",