diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs index b7fabd25..2a7ac6bf 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -74,25 +74,25 @@ namespace NadekoBot.DataStructures.Replacements return this; } - public ReplacementBuilder WithMusic(MusicService ms) - { - _reps.TryAdd("%playing%", () => - { - var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try - { - var mp = ms.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch - { - return "No songs"; - } - }); - _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); - return this; - } + //public ReplacementBuilder WithMusic(MusicService ms) + //{ + // _reps.TryAdd("%playing%", () => + // { + // var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + // if (cnt != 1) return cnt.ToString(); + // try + // { + // var mp = ms.MusicPlayers.FirstOrDefault(); + // return mp.Value.CurrentSong.SongInfo.Title; + // } + // catch + // { + // return "No songs"; + // } + // }); + // _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); + // return this; + //} public ReplacementBuilder WithRngRegex() { diff --git a/src/NadekoBot/DataStructures/SyncPrecondition.cs b/src/NadekoBot/DataStructures/SyncPrecondition.cs new file mode 100644 index 00000000..6dd675e5 --- /dev/null +++ b/src/NadekoBot/DataStructures/SyncPrecondition.cs @@ -0,0 +1,23 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + //public class SyncPrecondition : PreconditionAttribute + //{ + // public override Task CheckPermissions(ICommandContext context, + // CommandInfo command, + // IServiceProvider services) + // { + + // } + //} + //public enum SyncType + //{ + // Guild + //} +} diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ebec33c5..472acd17 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -15,6 +15,7 @@ using NadekoBot.Services.Database.Models; using System.Threading; using NadekoBot.Services.Music; using NadekoBot.DataStructures; +using System.Collections.Concurrent; namespace NadekoBot.Modules.Music { @@ -35,139 +36,79 @@ namespace NadekoBot.Modules.Music _google = google; _db = db; _music = music; - //it can fail if its currenctly opened or doesn't exist. Either way i don't care - _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } - private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //{ + // var usr = iusr as SocketGuildUser; + // if (usr == null || + // oldState.VoiceChannel == newState.VoiceChannel) + // return Task.CompletedTask; + + // MusicPlayer player; + // if ((player = _music.GetPlayer(usr.Guild.Id)) == null) + // return Task.CompletedTask; + + // try + // { + // //if bot moved + // if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && + // usr.Id == _client.CurrentUser.Id) + // { + // if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // player.TogglePause(); + // else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // player.TogglePause(); + + // return Task.CompletedTask; + // } + + + // //if some other user moved + // if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // player.Paused && + // newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) + // (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // !player.Paused && + // oldState.VoiceChannel.Users.Count == 1)) + // { + // player.TogglePause(); + // return Task.CompletedTask; + // } + + // } + // catch + // { + // // ignored + // } + // return Task.CompletedTask; + //} + + private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { - var usr = iusr as SocketGuildUser; - if (usr == null || - oldState.VoiceChannel == newState.VoiceChannel) - return Task.CompletedTask; - - MusicPlayer player; - if ((player = _music.GetPlayer(usr.Guild.Id)) == null) - return Task.CompletedTask; - - try + var qData = mp.Enqueue(songInfo); + if (qData.Success) { - //if bot moved - if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - usr.Id == _client.CurrentUser.Id) + if (!silent) { - if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - player.TogglePause(); - else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - player.TogglePause(); - - return Task.CompletedTask; + try + { + //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (qData.Index)).WithMusicIcon()) + .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") + .WithThumbnailUrl(songInfo.Thumbnail) + .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) + .ConfigureAwait(false); + queuedMessage?.DeleteAfter(10); + } + catch + { + // ignored + } // if queued message sending fails, don't attempt to delete it } - - - //if some other user moved - if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - player.Paused && - newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) - (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - !player.Paused && - oldState.VoiceChannel.Users.Count == 1)) - { - player.TogglePause(); - return Task.CompletedTask; - } - - } - catch - { - // ignored - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Next(int skipCount = 1) - { - if (skipCount < 1) - return Task.CompletedTask; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (musicPlayer.PlaybackVoiceChannel == ((IGuildUser)Context.User).VoiceChannel) - { - while (--skipCount > 0) - { - musicPlayer.RemoveSongAt(0); - } - musicPlayer.Next(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Stop() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - { - musicPlayer.Autoplay = false; - musicPlayer.Stop(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Destroy() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - _music.DestroyPlayer(Context.Guild.Id); - - return Task.CompletedTask; - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Pause() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; - musicPlayer.TogglePause(); - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Fairplay() - { - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; - - if (val) - { - await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); } } @@ -175,7 +116,10 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); + var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + await InternalQueue(mp, songInfo, false); + if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); @@ -217,29 +161,16 @@ namespace NadekoBot.Modules.Music { try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudQueue([Remainder] string query) - { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 1) { - Song currentSong; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if ((currentSong = musicPlayer?.CurrentSong) == null) + var mp = await _music.GetOrCreatePlayer(Context); + var (current, songs) = mp.QueueArray(); + + if (!songs.Any()) { await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; @@ -247,33 +178,39 @@ namespace NadekoBot.Modules.Music if (--page < 0) return; - - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + //todo say whether music player is stopped + //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; - var total = musicPlayer.TotalPlaytime; - var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", - (int) total.TotalHours, - total.Minutes, - total.Seconds); - var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; - var lastPage = musicPlayer.Playlist.Count / itemsPerPage; + //var total = musicPlayer.TotalPlaytime; + //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", + // (int)total.TotalHours, + // total.Minutes, + // total.Seconds); + //var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; + var lastPage = songs.Length / itemsPerPage; Func printAction = curPage => { var startAt = itemsPerPage * curPage; var number = 0 + startAt; - var desc = string.Join("\n", musicPlayer.Playlist + var desc = string.Join("\n", songs .Skip(startAt) .Take(itemsPerPage) - .Select(v => $"`{++number}.` {v.PrettyFullName}")); - - desc = $"`🔊` {currentSong.PrettyFullName}\n\n" + desc; + .Select(v => + { + if(number++ == current) + return $"**⇒**`{number}.` {v.PrettyFullName}"; + else + return $"`{number}.` {v.PrettyFullName}"; + })); //todo v.prettyfullname instead of title - if (musicPlayer.RepeatSong) - desc = "🔂 " + GetText("repeating_cur_song") +"\n\n" + desc; - else if (musicPlayer.RepeatPlaylist) - desc = "🔁 " + GetText("repeating_playlist")+"\n\n" + desc; + desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; + + if (mp.RepeatCurrentSong) + desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; + //else if (musicPlayer.RepeatPlaylist) + // desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; @@ -281,12 +218,12 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) .WithDescription(desc) - .WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + - $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + - (musicPlayer.FairPlay - ? "✔️" + GetText("fairplay") - : "✖️" + GetText("fairplay")) + " | " + - (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) + //.WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + + // $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + + // (musicPlayer.FairPlay + // ? "✔️" + GetText("fairplay") + // : "✖️" + GetText("fairplay")) + " | " + + // (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) .WithOkColor(); return embed; @@ -296,41 +233,52 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task NowPlaying() + public async Task Next(int skipCount = 1) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (skipCount < 1) return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var mp = await _music.GetOrCreatePlayer(Context); - var embed = new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) - .WithDescription(currentSong.PrettyName) - .WithThumbnailUrl(currentSong.Thumbnail) - .WithFooter(ef => ef.WithText(musicPlayer.PrettyVolume + " | " + currentSong.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + mp.Next(); + } - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Stop() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task Destroy() + { + _music.DestroyPlayer(Context.Guild.Id); + return Task.CompletedTask; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Pause() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.TogglePause(); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Volume(int val) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); if (val < 0 || val > 100) { await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); return; } - var volume = musicPlayer.SetVolume(val); - await ReplyConfirmLocalized("volume_set", volume).ConfigureAwait(false); + mp.SetVolume(val); + await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -350,401 +298,40 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ShufflePlaylist() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - if (musicPlayer.Playlist.Count < 2) - return; - - musicPlayer.Shuffle(); - await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Playlist([Remainder] string playlist) - { - - var arg = playlist; - if (string.IsNullOrWhiteSpace(arg)) - return; - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); - if (plId == null) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); - if (!ids.Any()) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var count = ids.Count(); - var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", - Format.Bold(count.ToString()))).ConfigureAwait(false); - - var cancelSource = new CancellationTokenSource(); - - var gusr = (IGuildUser)Context.User; - while (ids.Any() && !cancelSource.IsCancellationRequested) - { - var tasks = Task.WhenAll(ids.Take(5).Select(async id => - { - if (cancelSource.Token.IsCancellationRequested) - return; - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { try { cancelSource.Cancel(); } catch { } } - })); - - await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); - ids = ids.Skip(5); - } - - await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudPl([Remainder] string pl) - { - - pl = pl?.Trim(); - - if (string.IsNullOrWhiteSpace(pl)) - return; - - using (var http = new HttpClient()) - { - var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - foreach (var svideo in scvids.Skip(1)) - { - try - { - musicPlayer.AddSong(new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Normal, - Query = svideo.TrackLink, - }), ((IGuildUser)Context.User).Username); - } - catch (PlaylistFullException) { break; } - } - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task LocalPl([Remainder] string directory) - { - - var arg = directory; - if (string.IsNullOrWhiteSpace(arg)) - return; - var dir = new DirectoryInfo(arg); - var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) - .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); - var gusr = (IGuildUser)Context.User; - foreach (var file in fileEnum) - { - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); - } - catch (PlaylistFullException) - { - break; - } - catch - { - // ignored - } - } - await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Radio(string radioLink) - { - - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Local([Remainder] string path) - { - - var arg = path; - if (string.IsNullOrWhiteSpace(arg)) - return; - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); - - } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ - - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] - public Task SongRemove(int num) + public async Task SongRemove(int index) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; + var mp = await _music.GetOrCreatePlayer(Context); + try + { + var song = mp.RemoveAt(index - 1); + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .WithErrorColor(); - musicPlayer.RemoveSongAt(num - 1); - return Task.CompletedTask; + await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (ArgumentOutOfRangeException) + { + //todo error message + } } + public enum All { All } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(1)] - public async Task SongRemove(string all) + public async Task SongRemove(All all) { - if (all.Trim().ToUpperInvariant() != "ALL") - return; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.ClearQueue(); + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(true); await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task MoveSong([Remainder] string fromto) - { - if (string.IsNullOrWhiteSpace(fromto)) - return; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - fromto = fromto?.Trim(); - var fromtoArr = fromto.Split('>'); - - int n1; - int n2; - - var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); - - if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || - !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || - n1 > playlist.Count || n2 > playlist.Count) - { - await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); - return; - } - - var s = playlist[n1 - 1]; - playlist.Insert(n2 - 1, s); - var nn1 = n2 < n1 ? n1 : n1 - 1; - playlist.RemoveAt(nn1); - - var embed = new EmbedBuilder() - .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") - .WithUrl(s.SongUrl) - .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxQueue(uint size = 0) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - musicPlayer.MaxQueueSize = size; - - if(size == 0) - await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxPlaytime(uint seconds) - { - if (seconds < 15 && seconds != 0) - return; - - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.MaxPlaytimeSeconds = seconds; - if (seconds == 0) - await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ReptCurSong() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - var currentValue = musicPlayer.ToggleRepeatSong(); - - if (currentValue) - await Context.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track"))) - .WithDescription(currentSong.PrettyName) - .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); - else - await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) - .ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RepeatPl() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentValue = musicPlayer.ToggleRepeatPlaylist(); - if(currentValue) - await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Save([Remainder] string name) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - var curSong = musicPlayer.CurrentSong; - var songs = musicPlayer.Playlist.Append(curSong) - .Select(s => new PlaylistSong() - { - Provider = s.SongInfo.Provider, - ProviderType = s.SongInfo.ProviderType, - Title = s.SongInfo.Title, - Uri = s.SongInfo.Uri, - Query = s.SongInfo.Query, - }).ToList(); - - MusicPlaylist playlist; - using (var uow = _db.UnitOfWork) - { - playlist = new MusicPlaylist - { - Name = name, - Author = Context.User.Username, - AuthorId = Context.User.Id, - Songs = songs, - }; - uow.MusicPlaylists.Add(playlist); - await uow.CompleteAsync().ConfigureAwait(false); - } - - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithTitle(GetText("playlist_saved")) - .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) - .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Load([Remainder] int id) - { - MusicPlaylist mpl; - using (var uow = _db.UnitOfWork) - { - mpl = uow.MusicPlaylists.GetWithSongs(id); - } - - if (mpl == null) - { - await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); - return; - } - IUserMessage msg = null; - try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - foreach (var item in mpl.Songs) - { - var usr = (IGuildUser)Context.User; - try - { - await _music.QueueSong(usr, (ITextChannel)Context.Channel, usr.VoiceChannel, item.Query, true, item.ProviderType).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { break; } - } - if (msg != null) - await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Playlists([Remainder] int num = 1) @@ -767,7 +354,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task DeletePlaylist([Remainder] int id) @@ -803,69 +390,489 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Goto(int time) + public async Task Save([Remainder] string name) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); - if (time < 0) - return; + var songs = mp.QueueArray().Songs + .Select(s => new PlaylistSong() + { + Provider = s.Provider, + ProviderType = s.ProviderType, + Title = s.Title, + Uri = s.Uri, + Query = s.Query, + }).ToList(); - var currentSong = musicPlayer.CurrentSong; + MusicPlaylist playlist; + using (var uow = _db.UnitOfWork) + { + playlist = new MusicPlaylist + { + Name = name, + Author = Context.User.Username, + AuthorId = Context.User.Id, + Songs = songs, + }; + uow.MusicPlaylists.Add(playlist); + await uow.CompleteAsync().ConfigureAwait(false); + } - if (currentSong == null) - return; - - //currentSong.PrintStatusMessage = false; - var gotoSong = currentSong.Clone(); - gotoSong.SkipTo = time; - musicPlayer.AddSong(gotoSong, 0); - musicPlayer.Next(); - - var minutes = (time / 60).ToString(); - var seconds = (time % 60).ToString(); - - if (minutes.Length == 1) - minutes = "0" + minutes; - if (seconds.Length == 1) - seconds = "0" + seconds; - - await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithTitle(GetText("playlist_saved")) + .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) + .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); } + private readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Autoplay() + public async Task Load([Remainder] int id) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (!PlaylistLoadBlacklist.Add(Context.Guild.Id)) return; + try + { + var mp = await _music.GetOrCreatePlayer(Context); + MusicPlaylist mpl; + using (var uow = _db.UnitOfWork) + { + mpl = uow.MusicPlaylists.GetWithSongs(id); + } - if (!musicPlayer.ToggleAutoplay()) - await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + if (mpl == null) + { + await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); + return; + } + IUserMessage msg = null; + try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + foreach (var item in mpl.Songs) + { + try + { + //todo fix for all + if (item.ProviderType == MusicType.Normal) + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + } + catch (SongNotFoundException) { } + catch { break; } + } + if (msg != null) + await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); + } + finally + { + PlaylistLoadBlacklist.TryRemove(Context.Guild.Id); + } } + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Fairplay() + //{ + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; + + // if (val) + // { + // await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); + // } + // else + // { + // await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudQueue([Remainder] string query) + //{ + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task NowPlaying() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var embed = new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) + .WithDescription(currentSong.PrettyName) + .WithThumbnailUrl(currentSong.Thumbnail) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + /*currentSong.PrettyFullTime +*/ $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task ShufflePlaylist() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // if (musicPlayer.Playlist.Count < 2) + // return; + + // musicPlayer.Shuffle(); + // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Playlist([Remainder] string playlist) + //{ + + // var arg = playlist; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); + // if (plId == null) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + // if (!ids.Any()) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var count = ids.Count(); + // var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", + // Format.Bold(count.ToString()))).ConfigureAwait(false); + + // var cancelSource = new CancellationTokenSource(); + + // var gusr = (IGuildUser)Context.User; + // while (ids.Any() && !cancelSource.IsCancellationRequested) + // { + // var tasks = Task.WhenAll(ids.Take(5).Select(async id => + // { + // if (cancelSource.Token.IsCancellationRequested) + // return; + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); + // } + // catch (SongNotFoundException) { } + // catch { try { cancelSource.Cancel(); } catch { } } + // })); + + // await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); + // ids = ids.Skip(5); + // } + + // await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudPl([Remainder] string pl) + //{ + + // pl = pl?.Trim(); + + // if (string.IsNullOrWhiteSpace(pl)) + // return; + + // using (var http = new HttpClient()) + // { + // var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // foreach (var svideo in scvids.Skip(1)) + // { + // try + // { + // musicPlayer.AddSong(new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Normal, + // Query = svideo.TrackLink, + // }), ((IGuildUser)Context.User).Username); + // } + // catch (PlaylistFullException) { break; } + // } + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task LocalPl([Remainder] string directory) + //{ + + // var arg = directory; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // var dir = new DirectoryInfo(arg); + // var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) + // .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); + // var gusr = (IGuildUser)Context.User; + // foreach (var file in fileEnum) + // { + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); + // } + // catch (PlaylistFullException) + // { + // break; + // } + // catch + // { + // // ignored + // } + // } + // await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Radio(string radioLink) + //{ + + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task Local([Remainder] string path) + //{ + + // var arg = path; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Move() + //{ + + // MusicPlayer musicPlayer; + // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; + // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) + // return; + // await musicPlayer.MoveToVoiceChannel(voiceChannel); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task MoveSong([Remainder] string fromto) + //{ + // if (string.IsNullOrWhiteSpace(fromto)) + // return; + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // fromto = fromto?.Trim(); + // var fromtoArr = fromto.Split('>'); + + // int n1; + // int n2; + + // var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); + + // if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || + // !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || + // n1 > playlist.Count || n2 > playlist.Count) + // { + // await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); + // return; + // } + + // var s = playlist[n1 - 1]; + // playlist.Insert(n2 - 1, s); + // var nn1 = n2 < n1 ? n1 : n1 - 1; + // playlist.RemoveAt(nn1); + + // var embed = new EmbedBuilder() + // .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") + // .WithUrl(s.SongUrl) + // .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) + // .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) + // .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + // .WithColor(NadekoBot.OkColor); + // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + + // //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); + + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxQueue(uint size = 0) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // musicPlayer.MaxQueueSize = size; + + // if(size == 0) + // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxPlaytime(uint seconds) + //{ + // if (seconds < 15 && seconds != 0) + // return; + + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // musicPlayer.MaxPlaytimeSeconds = seconds; + // if (seconds == 0) + // await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ReptCurSong() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + var currentValue = mp.ToggleRepeatSong(); + + if (currentValue) + await Context.Channel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track"))) + .WithDescription(currentSong.PrettyName) + .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); + else + await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) + .ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task RepeatPl() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // var currentValue = musicPlayer.ToggleRepeatPlaylist(); + // if(currentValue) + // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Goto(int time) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + + // if (time < 0) + // return; + + // var currentSong = musicPlayer.CurrentSong; + + // if (currentSong == null) + // return; + + // //currentSong.PrintStatusMessage = false; + // var gotoSong = currentSong.Clone(); + // gotoSong.SkipTo = time; + // musicPlayer.AddSong(gotoSong, 0); + // musicPlayer.Next(); + + // var minutes = (time / 60).ToString(); + // var seconds = (time % 60).ToString(); + + // if (minutes.Length == 1) + // minutes = "0" + minutes; + // if (seconds.Length == 1) + // seconds = "0" + seconds; + + // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Autoplay() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // if (!musicPlayer.ToggleAutoplay()) + // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + //} + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] public async Task SetMusicChannel() { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - { - await ReplyErrorLocalized("no_player").ConfigureAwait(false); - return; - } + var mp = await _music.GetOrCreatePlayer(Context); - musicPlayer.OutputTextChannel = (ITextChannel)Context.Channel; + mp.OutputTextChannel = (ITextChannel)Context.Channel; await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false); } - } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 9e33aef7..d4e09ad1 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Services.Administration _rep = new ReplacementBuilder() .WithClient(client) .WithStats(client) - .WithMusic(music) + //.WithMusic(music) .Build(); _t = new Timer(async (objState) => diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 4b9b6bac..ed7ec815 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.52"; + public const string BotVersion = "1.53"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/Impl/SyncPreconditionService.cs b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs new file mode 100644 index 00000000..30d5b48a --- /dev/null +++ b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Impl +{ + public class SyncPreconditionService + { + + } +} diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs index 1dbe8ad7..af195c15 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,7 +2,7 @@ namespace NadekoBot.Services.Music { - class PlaylistFullException : Exception + public class PlaylistFullException : Exception { public PlaylistFullException(string message) : base(message) { @@ -10,11 +10,19 @@ namespace NadekoBot.Services.Music public PlaylistFullException() : base("Queue is full.") { } } - class SongNotFoundException : Exception + public class SongNotFoundException : Exception { public SongNotFoundException(string message) : base(message) { } public SongNotFoundException() : base("Song is not found.") { } } + public class NotInVoiceChannelException : Exception + { + public NotInVoiceChannelException(string message) : base(message) + { + } + + public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } + } } diff --git a/src/NadekoBot/Services/Music/MusicControls.cs b/src/NadekoBot/Services/Music/MusicControls.cs deleted file mode 100644 index 4e688351..00000000 --- a/src/NadekoBot/Services/Music/MusicControls.cs +++ /dev/null @@ -1,381 +0,0 @@ -using Discord; -using Discord.Audio; -using NadekoBot.Extensions; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NLog; -using NadekoBot.Services.Database.Models; - -namespace NadekoBot.Services.Music -{ - public enum StreamState - { - Resolving, - Queued, - Playing, - Completed - } - - public class MusicPlayer - { - private IAudioClient AudioClient { get; set; } - - /// - /// Player will prioritize different queuer name - /// over the song position in the playlist - /// - public bool FairPlay { get; set; } = false; - - /// - /// Song will stop playing after this amount of time. - /// To prevent people queueing radio or looped songs - /// while other people want to listen to other songs too. - /// - public uint MaxPlaytimeSeconds { get; set; } = 0; - - - // this should be written better - public TimeSpan TotalPlaytime => - _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? - TimeSpan.MaxValue : - new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); - - /// - /// Users who recently got their music wish - /// - private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - - private readonly List _playlist = new List(); - private readonly Logger _log; - private readonly IGoogleApiService _google; - - public IReadOnlyCollection Playlist => _playlist; - - public Song CurrentSong { get; private set; } - public CancellationTokenSource SongCancelSource { get; private set; } - private CancellationToken CancelToken { get; set; } - - public bool Paused { get; set; } - - public float Volume { get; private set; } - - public event Action OnCompleted = delegate { }; - public event Action OnStarted = delegate { }; - public event Action OnPauseChanged = delegate { }; - - public IVoiceChannel PlaybackVoiceChannel { get; private set; } - public ITextChannel OutputTextChannel { get; set; } - - private bool Destroyed { get; set; } - public bool RepeatSong { get; private set; } - public bool RepeatPlaylist { get; private set; } - public bool Autoplay { get; set; } - public uint MaxQueueSize { get; set; } = 0; - - private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); - - public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; - - public event Action SongRemoved = delegate { }; - - public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) - { - _log = LogManager.GetCurrentClassLogger(); - _google = google; - - OutputTextChannel = outputChannel; - Volume = defaultVolume ?? 1.0f; - - PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - - Task.Run(async () => - { - try - { - while (!Destroyed) - { - try - { - if (ActionQueue.TryDequeue(out Action action)) - { - action(); - } - } - finally - { - await Task.Delay(100).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - _log.Warn("Action queue crashed"); - _log.Warn(ex); - } - }).ConfigureAwait(false); - - var t = new Thread(async () => - { - while (!Destroyed) - { - try - { - CurrentSong = GetNextSong(); - - if (CurrentSong == null) - continue; - - while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || - AudioClient?.ConnectionState == ConnectionState.Connecting) - { - _log.Info("Waiting for Audio client"); - await Task.Delay(200).ConfigureAwait(false); - } - - if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) - AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); - - var index = _playlist.IndexOf(CurrentSong); - if (index != -1) - RemoveSongAt(index, true); - - OnStarted(this, CurrentSong); - try - { - await CurrentSong.Play(AudioClient, CancelToken); - } - catch (OperationCanceledException) - { - } - finally - { - OnCompleted(this, CurrentSong); - } - - - if (RepeatPlaylist & !RepeatSong) - AddSong(CurrentSong, CurrentSong.QueuerName); - - if (RepeatSong) - AddSong(CurrentSong, 0); - - } - catch (Exception ex) - { - _log.Warn("Music thread almost crashed."); - _log.Warn(ex); - await Task.Delay(3000).ConfigureAwait(false); - } - finally - { - if (!CancelToken.IsCancellationRequested) - { - SongCancelSource.Cancel(); - } - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - CurrentSong = null; - await Task.Delay(300).ConfigureAwait(false); - } - } - }); - - t.Start(); - } - - public void Next() - { - ActionQueue.Enqueue(() => - { - Paused = false; - SongCancelSource.Cancel(); - }); - } - - public void Stop() - { - ActionQueue.Enqueue(() => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - _playlist.Clear(); - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - public void TogglePause() => OnPauseChanged(Paused = !Paused); - - public int SetVolume(int volume) - { - if (volume < 0) - volume = 0; - if (volume > 100) - volume = 100; - - Volume = volume / 100.0f; - return volume; - } - - private Song GetNextSong() - { - if (!FairPlay) - { - return _playlist.FirstOrDefault(); - } - var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) - ?? _playlist.FirstOrDefault(); - - if (song == null) - return null; - - if (RecentlyPlayedUsers.Contains(song.QueuerName)) - { - RecentlyPlayedUsers.Clear(); - } - - RecentlyPlayedUsers.Add(song.QueuerName); - return song; - } - - public void Shuffle() - { - ActionQueue.Enqueue(() => - { - var oldPlaylist = _playlist.ToArray(); - _playlist.Clear(); - _playlist.AddRange(oldPlaylist.Shuffle()); - }); - } - - public void AddSong(Song s, string username) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ThrowIfQueueFull(); - ActionQueue.Enqueue(() => - { - s.MusicPlayer = this; - s.QueuerName = username.TrimTo(10); - _playlist.Add(s); - }); - } - - public void AddSong(Song s, int index) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Insert(index, s); - }); - } - - public void RemoveSong(Song s) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Remove(s); - }); - } - - public void RemoveSongAt(int index, bool silent = false) - { - ActionQueue.Enqueue(() => - { - if (index < 0 || index >= _playlist.Count) - return; - var song = _playlist.ElementAtOrDefault(index); - if (_playlist.Remove(song) && !silent) - { - SongRemoved(song, index); - } - - }); - } - - public void ClearQueue() - { - ActionQueue.Enqueue(() => - { - _playlist.Clear(); - }); - } - - public async Task UpdateSongDurationsAsync() - { - var curSong = CurrentSong; - var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - s.TotalTime == TimeSpan.Zero) - .ToArray(); - if (curSong != null) - { - Array.Resize(ref toUpdate, toUpdate.Length + 1); - toUpdate[toUpdate.Length - 1] = curSong; - } - var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - .Distinct(); - - var durations = await _google.GetVideoDurationsAsync(ids); - - toUpdate.ForEach(s => - { - foreach (var kvp in durations) - { - if (s.SongInfo.Query.EndsWith(kvp.Key)) - { - s.TotalTime = kvp.Value; - return; - } - } - }); - } - - public void Destroy() - { - ActionQueue.Enqueue(async () => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - Destroyed = true; - _playlist.Clear(); - - try { await AudioClient.StopAsync(); } catch { } - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - //public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) - //{ - // if (audioClient?.ConnectionState != ConnectionState.Connected) - // throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); - // PlaybackVoiceChannel = voiceChannel; - // audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); - //} - - public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; - - public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; - - public bool ToggleAutoplay() => Autoplay = !Autoplay; - - public void ThrowIfQueueFull() - { - if (MaxQueueSize == 0) - return; - if (_playlist.Count >= MaxQueueSize) - throw new PlaylistFullException(); - } - } -} diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs new file mode 100644 index 00000000..64c3aa22 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -0,0 +1,657 @@ +using Discord; +using Discord.Audio; +using System; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using System.Diagnostics; + +namespace NadekoBot.Services.Music +{ + public enum StreamState + { + Resolving, + Queued, + Playing, + Completed + } + public class MusicPlayer : IDisposable + { + private readonly Task _player; + private readonly IVoiceChannel VoiceChannel; + private readonly Logger _log; + + private MusicQueue Queue { get; } = new MusicQueue(); + + public bool Exited { get; set; } = false; + public bool Stopped { get; private set; } = false; + public float Volume { get; private set; } = 1.0f; + public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + private TaskCompletionSource pauseTaskSource { get; set; } = null; + + private CancellationTokenSource SongCancelSource { get; set; } + public ITextChannel OutputTextChannel { get; set; } + public (int Index, SongInfo Current) Current + { + get + { + if (Stopped) + return (0, null); + return Queue.Current; + } + } + + public bool RepeatCurrentSong { get; private set; } + + private IAudioClient _audioClient; + private readonly object locker = new object(); + + #region events + public event Action OnStarted; + public event Action OnCompleted; + public event Action OnPauseChanged; + #endregion + + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) + { + _log = LogManager.GetCurrentClassLogger(); + this.Volume = volume; + this.VoiceChannel = vch; + this.SongCancelSource = new CancellationTokenSource(); + this.OutputTextChannel = output; + + _player = Task.Run(async () => + { + while (!Exited) + { + CancellationToken cancelToken; + (int Index, SongInfo Song) data; + lock (locker) + { + data = Queue.Current; + cancelToken = SongCancelSource.Token; + } + try + { + _log.Info("Checking for songs"); + if (data.Song == null) + continue; + + _log.Info("Connecting"); + + + _log.Info("Starting"); + var p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + var ac = await GetAudioClient(); + if (ac == null) + continue; + var pcm = ac.CreatePCMStream(AudioApplication.Music); + + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try + { + while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken); + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + + OnCompleted?.Invoke(this, data.Song); + } + } + finally + { + _log.Info("Next song"); + do + { + await Task.Delay(100); + } + while (Stopped && !Exited); + if(!RepeatCurrentSong) + Queue.Next(); + } + } + }, SongCancelSource.Token); + } + + private async Task GetAudioClient(bool reconnect = false) + { + if (_audioClient == null || + _audioClient.ConnectionState != ConnectionState.Connected || + reconnect) + try + { + _audioClient = await VoiceChannel.ConnectAsync(); + } + catch + { + return null; + } + return _audioClient; + } + + public (bool Success, int Index) Enqueue(SongInfo song) + { + _log.Info("Adding song"); + Queue.Add(song); + return (true, Queue.Count); + } + + public void Next() + { + lock (locker) + { + Stopped = false; + Unpause(); + } + CancelCurrentSong(); + } + + public void Stop(bool clearQueue = false) + { + lock (locker) + { + Stopped = true; + Queue.ResetCurrent(); + if (clearQueue) + Queue.Clear(); + Unpause(); + } + CancelCurrentSong(); + } + + private void Unpause() + { + if (pauseTaskSource != null) + { + pauseTaskSource.TrySetResult(true); + pauseTaskSource = null; + } + } + + public void TogglePause() + { + lock (locker) + { + if (pauseTaskSource == null) + pauseTaskSource = new TaskCompletionSource(); + else + { + Unpause(); + } + } + OnPauseChanged?.Invoke(this, pauseTaskSource != null); + } + + public void SetVolume(int volume) + { + if (volume < 0 || volume > 100) + throw new ArgumentOutOfRangeException(nameof(volume)); + Volume = ((float)volume) / 100; + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + var cur = Queue.Current; + if (cur.Index == index) + Next(); + return Queue.RemoveAt(index); + } + } + + private void CancelCurrentSong() + { + lock (locker) + { + var cs = SongCancelSource; + SongCancelSource = new CancellationTokenSource(); + cs.Cancel(); + } + } + + public void ClearQueue() + { + lock (locker) + { + Queue.Clear(); + } + } + + public (int CurrentIndex, SongInfo[] Songs) QueueArray() + => Queue.ToArray(); + + //aidiakapi ftw + public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) + { + if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; + + // 16-bit precision for the multiplication + var volumeFixed = (int)Math.Round(volume * 65536d); + + var count = audioSamples.Length / 2; + + fixed (byte* srcBytes = audioSamples) + { + var src = (short*)srcBytes; + + for (var i = count; i != 0; i--, src++) + *src = (short)(((*src) * volumeFixed) >> 16); + } + + return audioSamples; + } + + public bool ToggleRepeatSong() + { + lock (locker) + { + return RepeatCurrentSong = !RepeatCurrentSong; + } + } + + public void Dispose() + { + _log.Info("Disposing"); + lock (locker) + { + Exited = true; + Unpause(); + } + CancelCurrentSong(); + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; + } + + + //private IAudioClient AudioClient { get; set; } + + ///// + ///// Player will prioritize different queuer name + ///// over the song position in the playlist + ///// + //public bool FairPlay { get; set; } = false; + + ///// + ///// Song will stop playing after this amount of time. + ///// To prevent people queueing radio or looped songs + ///// while other people want to listen to other songs too. + ///// + //public uint MaxPlaytimeSeconds { get; set; } = 0; + + + //// this should be written better + //public TimeSpan TotalPlaytime => + // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? + // TimeSpan.MaxValue : + // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); + + ///// + ///// Users who recently got their music wish + ///// + //private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + + //private readonly List _playlist = new List(); + //private readonly Logger _log; + //private readonly IGoogleApiService _google; + + //public IReadOnlyCollection Playlist => _playlist; + + //public Song CurrentSong { get; private set; } + //public CancellationTokenSource SongCancelSource { get; private set; } + //private CancellationToken CancelToken { get; set; } + + //public bool Paused { get; set; } + + //public float Volume { get; private set; } + + //public event Action OnCompleted = delegate { }; + //public event Action OnStarted = delegate { }; + //public event Action OnPauseChanged = delegate { }; + + //public IVoiceChannel PlaybackVoiceChannel { get; private set; } + //public ITextChannel OutputTextChannel { get; set; } + + //private bool Destroyed { get; set; } + //public bool RepeatSong { get; private set; } + //public bool RepeatPlaylist { get; private set; } + //public bool Autoplay { get; set; } + //public uint MaxQueueSize { get; set; } = 0; + + //private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); + + //public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + + //public event Action SongRemoved = delegate { }; + + //public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) + //{ + // _log = LogManager.GetCurrentClassLogger(); + // _google = google; + + // OutputTextChannel = outputChannel; + // Volume = defaultVolume ?? 1.0f; + + // PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + + // Task.Run(async () => + // { + // try + // { + // while (!Destroyed) + // { + // try + // { + // if (ActionQueue.TryDequeue(out Action action)) + // { + // action(); + // } + // } + // finally + // { + // await Task.Delay(100).ConfigureAwait(false); + // } + // } + // } + // catch (Exception ex) + // { + // _log.Warn("Action queue crashed"); + // _log.Warn(ex); + // } + // }).ConfigureAwait(false); + + // var t = new Thread(async () => + // { + // while (!Destroyed) + // { + // try + // { + // CurrentSong = GetNextSong(); + + // if (CurrentSong == null) + // continue; + + // while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || + // AudioClient?.ConnectionState == ConnectionState.Connecting) + // { + // _log.Info("Waiting for Audio client"); + // await Task.Delay(200).ConfigureAwait(false); + // } + + // if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) + // AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); + + // var index = _playlist.IndexOf(CurrentSong); + // if (index != -1) + // RemoveSongAt(index, true); + + // OnStarted(this, CurrentSong); + // try + // { + // await CurrentSong.Play(AudioClient, CancelToken); + // } + // catch (OperationCanceledException) + // { + // } + // finally + // { + // OnCompleted(this, CurrentSong); + // } + + + // if (RepeatPlaylist & !RepeatSong) + // AddSong(CurrentSong, CurrentSong.QueuerName); + + // if (RepeatSong) + // AddSong(CurrentSong, 0); + + // } + // catch (Exception ex) + // { + // _log.Warn("Music thread almost crashed."); + // _log.Warn(ex); + // await Task.Delay(3000).ConfigureAwait(false); + // } + // finally + // { + // if (!CancelToken.IsCancellationRequested) + // { + // SongCancelSource.Cancel(); + // } + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + // CurrentSong = null; + // await Task.Delay(300).ConfigureAwait(false); + // } + // } + // }); + + // t.Start(); + //} + + //public void Next() + //{ + // ActionQueue.Enqueue(() => + // { + // Paused = false; + // SongCancelSource.Cancel(); + // }); + //} + + //public void Stop() + //{ + // ActionQueue.Enqueue(() => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // _playlist.Clear(); + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + //public void TogglePause() => OnPauseChanged(Paused = !Paused); + + //public int SetVolume(int volume) + //{ + // if (volume < 0) + // volume = 0; + // if (volume > 100) + // volume = 100; + + // Volume = volume / 100.0f; + // return volume; + //} + + //private Song GetNextSong() + //{ + // if (!FairPlay) + // { + // return _playlist.FirstOrDefault(); + // } + // var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) + // ?? _playlist.FirstOrDefault(); + + // if (song == null) + // return null; + + // if (RecentlyPlayedUsers.Contains(song.QueuerName)) + // { + // RecentlyPlayedUsers.Clear(); + // } + + // RecentlyPlayedUsers.Add(song.QueuerName); + // return song; + //} + + //public void Shuffle() + //{ + // ActionQueue.Enqueue(() => + // { + // var oldPlaylist = _playlist.ToArray(); + // _playlist.Clear(); + // _playlist.AddRange(oldPlaylist.Shuffle()); + // }); + //} + + //public void AddSong(Song s, string username) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ThrowIfQueueFull(); + // ActionQueue.Enqueue(() => + // { + // s.MusicPlayer = this; + // s.QueuerName = username.TrimTo(10); + // _playlist.Add(s); + // }); + //} + + //public void AddSong(Song s, int index) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Insert(index, s); + // }); + //} + + //public void RemoveSong(Song s) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Remove(s); + // }); + //} + + //public void RemoveSongAt(int index, bool silent = false) + //{ + // ActionQueue.Enqueue(() => + // { + // if (index < 0 || index >= _playlist.Count) + // return; + // var song = _playlist.ElementAtOrDefault(index); + // if (_playlist.Remove(song) && !silent) + // { + // SongRemoved(song, index); + // } + + // }); + //} + + //public void ClearQueue() + //{ + // ActionQueue.Enqueue(() => + // { + // _playlist.Clear(); + // }); + //} + + //public async Task UpdateSongDurationsAsync() + //{ + // var curSong = CurrentSong; + // var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && + // s.TotalTime == TimeSpan.Zero) + // .ToArray(); + // if (curSong != null) + // { + // Array.Resize(ref toUpdate, toUpdate.Length + 1); + // toUpdate[toUpdate.Length - 1] = curSong; + // } + // var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) + // .Distinct(); + + // var durations = await _google.GetVideoDurationsAsync(ids); + + // toUpdate.ForEach(s => + // { + // foreach (var kvp in durations) + // { + // if (s.SongInfo.Query.EndsWith(kvp.Key)) + // { + // s.TotalTime = kvp.Value; + // return; + // } + // } + // }); + //} + + //public void Destroy() + //{ + // ActionQueue.Enqueue(async () => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // Destroyed = true; + // _playlist.Clear(); + + // try { await AudioClient.StopAsync(); } catch { } + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + ////public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) + ////{ + //// if (audioClient?.ConnectionState != ConnectionState.Connected) + //// throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); + //// PlaybackVoiceChannel = voiceChannel; + //// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); + ////} + + //public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; + + //public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; + + //public bool ToggleAutoplay() => Autoplay = !Autoplay; + + //public void ThrowIfQueueFull() + //{ + // if (MaxQueueSize == 0) + // return; + // if (_playlist.Count >= MaxQueueSize) + // throw new PlaylistFullException(); + //} + } +} diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs new file mode 100644 index 00000000..50219dc8 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class MusicQueue : IDisposable + { + private LinkedList Songs { get; } = new LinkedList(); + private int _currentIndex = 0; + private int CurrentIndex + { + get + { + return _currentIndex; + } + set + { + lock (locker) + { + if (Songs.Count == 0) + _currentIndex = 0; + else + _currentIndex = value %= Songs.Count; + } + } + } + public (int Index, SongInfo Song) Current + { + get + { + var cur = CurrentIndex; + return (cur, Songs.ElementAtOrDefault(cur)); + } + } + + private readonly object locker = new object(); + private TaskCompletionSource nextSource { get; } = new TaskCompletionSource(); + public int Count + { + get + { + lock (locker) + { + return Songs.Count; + } + } + } + + public void Add(SongInfo song) + { + lock (locker) + { + Songs.AddLast(song); + } + } + + public void Next() + { + CurrentIndex++; + } + + public void Dispose() + { + Clear(); + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + if (index < 0 || index >= Songs.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var current = Songs.First; + for (int i = 0; i < Songs.Count; i++) + { + if (i == index) + { + Songs.Remove(current); + if (CurrentIndex != 0) + { + if (CurrentIndex >= index) + { + --CurrentIndex; + } + } + break; + } + } + return current.Value; + } + } + + public void Clear() + { + lock (locker) + { + Songs.Clear(); + CurrentIndex = 0; + } + } + + public (int, SongInfo[]) ToArray() + { + lock (locker) + { + return (CurrentIndex, Songs.ToArray()); + } + } + + public void ResetCurrent() + { + CurrentIndex = 0; + } + } +} diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 9ea7a958..eb2a5016 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -11,6 +11,7 @@ using System.IO; using VideoLibrary; using System.Net.Http; using System.Collections.Generic; +using Discord.Commands; namespace NadekoBot.Services.Music { @@ -29,8 +30,8 @@ namespace NadekoBot.Services.Music public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, - NadekoStrings strings, ILocalization localization, DbService db, + public MusicService(IGoogleApiService google, + NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { _google = google; @@ -48,28 +49,49 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - public MusicPlayer GetPlayer(ulong guildId) + // public MusicPlayer GetPlayer(ulong guildId) + // { + // MusicPlayers.TryGetValue(guildId, out var player); + // return player; + // } + public float GetDefaultVolume(ulong guildId) { - MusicPlayers.TryGetValue(guildId, out var player); - return player; + return _defaultVolumes.GetOrAdd(guildId, (id) => + { + using (var uow = _db.UnitOfWork) + { + return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; + } + }); } - public MusicPlayer GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) + public Task GetOrCreatePlayer(ICommandContext context) + { + var gUsr = (IGuildUser)context.User; + var txtCh = (ITextChannel)context.Channel; + var vCh = gUsr.VoiceChannel; + return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh); + } + + public async Task GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) { string GetText(string text, params object[] replacements) => _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - return MusicPlayers.GetOrAdd(guildId, server => + if (voiceCh == null || voiceCh.Guild != textCh.Guild) { - var vol = _defaultVolumes.GetOrAdd(guildId, (id) => + if (textCh != null) { - using (var uow = _db.UnitOfWork) - { - return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; - } - }); - - var mp = new MusicPlayer(voiceCh, textCh, vol, _google); + await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); + } + throw new ArgumentException(nameof(voiceCh)); + } + + return MusicPlayers.GetOrAdd(guildId, _ => + { + var vol = GetDefaultVolume(guildId); + var mp = new MusicPlayer(this, voiceCh, textCh, vol); + IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; mp.OnCompleted += async (s, song) => @@ -91,30 +113,30 @@ namespace NadekoBot.Services.Music // ignored } - if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) - { - var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); - if (relatedVideos.Count > 0) - await QueueSong(await textCh.Guild.GetCurrentUserAsync(), - textCh, - voiceCh, - relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], - true).ConfigureAwait(false); - } + //todo autoplay should be independent from event handlers + //if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) + //{ + // var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); + // if (relatedVideos.Count > 0) + // await QueueSong(await textCh.Guild.GetCurrentUserAsync(), + // textCh, + // voiceCh, + // relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], + // true).ConfigureAwait(false); + //} } catch { // ignored } }; - mp.OnStarted += async (player, song) => { - try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } - catch - { - // ignored - } + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } + //catch + //{ + // // ignored + //} var sender = player; if (sender == null) return; @@ -125,7 +147,7 @@ namespace NadekoBot.Services.Music playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo))) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo))) .ConfigureAwait(false); } catch @@ -133,7 +155,7 @@ namespace NadekoBot.Services.Music // ignored } }; - mp.OnPauseChanged += async (paused) => + mp.OnPauseChanged += async (player, paused) => { try { @@ -150,291 +172,228 @@ namespace NadekoBot.Services.Music // ignored } }; + //mp.SongRemoved += async (song, index) => + //{ + // try + // { + // var embed = new EmbedBuilder() + // .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + // .WithDescription(song.PrettyName) + // .WithFooter(ef => ef.WithText(song.PrettyInfo)) + // .WithErrorColor(); - mp.SongRemoved += async (song, index) => - { - try - { - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo)) - .WithErrorColor(); + // await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - - } - catch - { - // ignored - } - }; + // } + // catch + // { + // // ignored + // } + //}; return mp; }); } - - public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { - string GetText(string text, params object[] replacements) => - _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + query.ThrowIfNull(nameof(query)); - if (voiceCh == null || voiceCh.Guild != textCh.Guild) - { - if (!silent) - await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); - throw new ArgumentNullException(nameof(voiceCh)); - } - if (string.IsNullOrWhiteSpace(query) || query.Length < 3) - throw new ArgumentException("Invalid song query.", nameof(query)); + SongInfo sinfo; - var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - Song resolvedSong; - try - { - musicPlayer.ThrowIfQueueFull(); - resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); + sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false); - if (resolvedSong == null) - throw new SongNotFoundException(); + return sinfo; + } - musicPlayer.AddSong(resolvedSong, queuer.Username); - } - catch (PlaylistFullException) + public async Task ResolveYoutubeSong(string query, string queuerName) + { + var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(link)) + throw new OperationCanceledException("Not a valid youtube query."); + var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 256) + .OrderByDescending(v => v.AudioBitrate) + .FirstOrDefault(); + + if (video == null) // do something with this error + throw new Exception("Could not load any video elements based on the query."); + //var m = Regex.Match(query, @"\?t=(?\d*)"); + //int gotoTime = 0; + //if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new SongInfo { - try - { - await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); - } - catch - { - // ignored - } - throw; - } - if (!silent) - { - try - { - //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) - .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") - .WithThumbnailUrl(resolvedSong.Thumbnail) - .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) - .ConfigureAwait(false); - queuedMessage?.DeleteAfter(10); - } - catch - { - // ignored - } // if queued message sending fails, don't attempt to delete it - } + Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + Provider = "YouTube", + Uri = await video.GetUriAsync().ConfigureAwait(false), + Query = link, + ProviderType = MusicType.Normal, + QueuerName = queuerName + }; + return song; } public void DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) - mp.Destroy(); + mp.Dispose(); } + // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + // { + // string GetText(string text, params object[] replacements) => + // _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); + //if (string.IsNullOrWhiteSpace(query) || query.Length< 3) + // throw new ArgumentException("Invalid song query.", nameof(query)); - if (musicType != MusicType.Local && IsRadioLink(query)) - { - musicType = MusicType.Radio; - query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; - } + // var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); + // Song resolvedSong; + // try + // { + // musicPlayer.ThrowIfQueueFull(); + // resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); - try - { - switch (musicType) - { - case MusicType.Local: - return new Song(new SongInfo - { - Uri = "\"" + Path.GetFullPath(query) + "\"", - Title = Path.GetFileNameWithoutExtension(query), - Provider = "Local File", - ProviderType = musicType, - Query = query, - }); - case MusicType.Radio: - return new Song(new SongInfo - { - Uri = query, - Title = $"{query}", - Provider = "Radio Stream", - ProviderType = musicType, - Query = query - }) - { TotalTime = TimeSpan.MaxValue }; - } - if (_sc.IsSoundCloudLink(query)) - { - var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = musicType, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // if (resolvedSong == null) + // throw new SongNotFoundException(); - if (musicType == MusicType.Soundcloud) - { - var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Soundcloud, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // musicPlayer.AddSong(resolvedSong, queuer.Username); + // } + // catch (PlaylistFullException) + // { + // try + // { + // await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); + // } + // catch + // { + // // ignored + // } + // throw; + // } + // if (!silent) + // { + // try + // { + // //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + // var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() + // .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) + // .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") + // .WithThumbnailUrl(resolvedSong.Thumbnail) + // .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) + // .ConfigureAwait(false); + // queuedMessage?.DeleteAfter(10); + // } + // catch + // { + // // ignored + // } // if queued message sending fails, don't attempt to delete it + // } + // } - var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(link)) - throw new OperationCanceledException("Not a valid youtube query."); - var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 256) - .OrderByDescending(v => v.AudioBitrate) - .FirstOrDefault(); - if (video == null) // do something with this error - throw new Exception("Could not load any video elements based on the query."); - var m = Regex.Match(query, @"\?t=(?\d*)"); - int gotoTime = 0; - if (m.Captures.Count > 0) - int.TryParse(m.Groups["t"].ToString(), out gotoTime); - var song = new Song(new SongInfo - { - Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - Provider = "YouTube", - Uri = await video.GetUriAsync().ConfigureAwait(false), - Query = link, - ProviderType = musicType, - }); - song.SkipTo = gotoTime; - return song; - } - catch (Exception ex) - { - _log.Warn($"Failed resolving the link.{ex.Message}"); - _log.Warn(ex); - return null; - } - } - private async Task HandleStreamContainers(string query) - { - string file = null; - try - { - using (var http = new HttpClient()) - { - file = await http.GetStringAsync(query).ConfigureAwait(false); - } - } - catch - { - return query; - } - if (query.Contains(".pls")) - { - //File1=http://armitunes.com:8000/ - //Regex.Match(query) - try - { - var m = Regex.Match(file, "File1=(?.*?)\\n"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .pls:\n{file}"); - return null; - } - } - if (query.Contains(".m3u")) - { - /* -# This is a comment - C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - */ - try - { - var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .m3u:\n{file}"); - return null; - } - } - if (query.Contains(".asx")) - { - // - try - { - var m = Regex.Match(file, ".*?)\""); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .asx:\n{file}"); - return null; - } - } - if (query.Contains(".xspf")) - { - /* - - - - file:///mp3s/song_1.mp3 - */ - try - { - var m = Regex.Match(file, "(?.*?)"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .xspf:\n{file}"); - return null; - } - } - return query; - } - private bool IsRadioLink(string query) => - (query.StartsWith("http") || - query.StartsWith("ww")) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); + // private async Task HandleStreamContainers(string query) + // { + // string file = null; + // try + // { + // using (var http = new HttpClient()) + // { + // file = await http.GetStringAsync(query).ConfigureAwait(false); + // } + // } + // catch + // { + // return query; + // } + // if (query.Contains(".pls")) + // { + // //File1=http://armitunes.com:8000/ + // //Regex.Match(query) + // try + // { + // var m = Regex.Match(file, "File1=(?.*?)\\n"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .pls:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".m3u")) + // { + // /* + //# This is a comment + // C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + // C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .m3u:\n{file}"); + // return null; + // } + + // } + // if (query.Contains(".asx")) + // { + // // + // try + // { + // var m = Regex.Match(file, ".*?)\""); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .asx:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".xspf")) + // { + // /* + // + // + // + // file:///mp3s/song_1.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?.*?)"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .xspf:\n{file}"); + // return null; + // } + // } + + // return query; + // } + + // private bool IsRadioLink(string query) => + // (query.StartsWith("http") || + // query.StartsWith("ww")) + // && + // (query.Contains(".pls") || + // query.Contains(".m3u") || + // query.Contains(".asx") || + // query.Contains(".xspf")); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 187b3b6e..074238d6 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -1,296 +1,246 @@ -using Discord.Audio; -using NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +using NadekoBot.Extensions; using System.Net; using Discord; using NadekoBot.Services.Database.Models; +using System; namespace NadekoBot.Services.Music { - public class SongInfo - { - public string Provider { get; set; } - public MusicType ProviderType { get; set; } - public string Query { get; set; } - public string Title { get; set; } - public string Uri { get; set; } - public string AlbumArt { get; set; } - } + //public class Song + //{ + // public SongInfo SongInfo { get; } + // public MusicPlayer MusicPlayer { get; set; } - public class Song - { - public SongInfo SongInfo { get; } - public MusicPlayer MusicPlayer { get; set; } + // private string _queuerName; + // public string QueuerName { get{ + // return Format.Sanitize(_queuerName); + // } set { _queuerName = value; } } - private string _queuerName; - public string QueuerName { get{ - return Format.Sanitize(_queuerName); - } set { _queuerName = value; } } + // public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; + // public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); - public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; - public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); + // private const int _milliseconds = 20; + // private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; + // private const int _frameBytes = 3840; //16-bit, 2 channels - private const int _milliseconds = 20; - private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; - private const int _frameBytes = 3840; //16-bit, 2 channels + // private ulong BytesSent { get; set; } - private ulong BytesSent { get; set; } + // //pwetty - //pwetty + // public string PrettyProvider => + // $"{(SongInfo.Provider ?? "???")}"; - public string PrettyProvider => - $"{(SongInfo.Provider ?? "???")}"; + // public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; - public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + // public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; - public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; + // public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; - public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + // public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; - public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; + // public string PrettyCurrentTime { + // get { + // var time = CurrentTime.ToString(@"mm\:ss"); + // var hrs = (int)CurrentTime.TotalHours; - public string PrettyCurrentTime { - get { - var time = CurrentTime.ToString(@"mm\:ss"); - var hrs = (int)CurrentTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // else + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - else - return time; - } - } + // public string PrettyTotalTime { + // get + // { + // if (TotalTime == TimeSpan.Zero) + // return "(?)"; + // if (TotalTime == TimeSpan.MaxValue) + // return "∞"; + // var time = TotalTime.ToString(@"mm\:ss"); + // var hrs = (int)TotalTime.TotalHours; - public string PrettyTotalTime { - get - { - if (TotalTime == TimeSpan.Zero) - return "(?)"; - if (TotalTime == TimeSpan.MaxValue) - return "∞"; - var time = TotalTime.ToString(@"mm\:ss"); - var hrs = (int)TotalTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - return time; - } - } + // public string Thumbnail { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Radio: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + // case MusicType.Normal: + // //todo 50 have videoid in songinfo from the start + // var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); + // return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + // case MusicType.Local: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + // case MusicType.Soundcloud: + // return SongInfo.AlbumArt; + // default: + // return ""; + // } + // } + // } - public string Thumbnail { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Radio: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.Normal: - //todo 50 have videoid in songinfo from the start - var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); - return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; - case MusicType.Local: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links - case MusicType.Soundcloud: - return SongInfo.AlbumArt; - default: - return ""; - } - } - } + // public string SongUrl { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Normal: + // return SongInfo.Query; + // case MusicType.Soundcloud: + // return SongInfo.Query; + // case MusicType.Local: + // return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; + // case MusicType.Radio: + // return $"https://google.com/search?q={SongInfo.Title}"; + // default: + // return ""; + // } + // } + // } - public string SongUrl { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Normal: - return SongInfo.Query; - case MusicType.Soundcloud: - return SongInfo.Query; - case MusicType.Local: - return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; - case MusicType.Radio: - return $"https://google.com/search?q={SongInfo.Title}"; - default: - return ""; - } - } - } + // private readonly Logger _log; - public int SkipTo { get; set; } + // public Song(SongInfo songInfo) + // { + // SongInfo = songInfo; + // _log = LogManager.GetCurrentClassLogger(); + // } - private readonly Logger _log; + // public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) + // { + // BytesSent = (ulong) SkipTo * 3840 * 50; + // var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); - public Song(SongInfo songInfo) - { - SongInfo = songInfo; - _log = LogManager.GetCurrentClassLogger(); - } + // var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); + // var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); - public Song Clone() - { - var s = new Song(SongInfo) - { - MusicPlayer = MusicPlayer, - QueuerName = QueuerName - }; - return s; - } + // try + // { + // var attempt = 0; - public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) - { - BytesSent = (ulong) SkipTo * 3840 * 50; - var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); + // var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy + // var finished = false; + // var count = 0; + // var sw = new Stopwatch(); + // var slowconnection = false; + // sw.Start(); + // while (!finished) + // { + // var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); + // if (t != prebufferingTask) + // { + // count++; + // if (count == 10) + // { + // slowconnection = true; + // prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); + // _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); + // continue; + // } - var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); - var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); + // if (inStream.BufferingCompleted && count == 1) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // else + // { + // continue; + // } + // } + // else if (prebufferingTask.IsCanceled) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // finished = true; + // } + // sw.Stop(); + // _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); - try - { - var attempt = 0; + // var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); - var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy - var finished = false; - var count = 0; - var sw = new Stopwatch(); - var slowconnection = false; - sw.Start(); - while (!finished) - { - var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); - if (t != prebufferingTask) - { - count++; - if (count == 10) - { - slowconnection = true; - prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); - _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); - continue; - } + // int nextTime = Environment.TickCount + _milliseconds; - if (inStream.BufferingCompleted && count == 1) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - else - { - continue; - } - } - else if (prebufferingTask.IsCanceled) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - finished = true; - } - sw.Stop(); - _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); + // byte[] buffer = new byte[_frameBytes]; + // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason + // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime + // { + // //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); + // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + // //await inStream.CopyToAsync(voiceClient.OutputStream); + // if (read < _frameBytes) + // _log.Debug("read {0}", read); + // unchecked + // { + // BytesSent += (ulong)read; + // } + // if (read < _frameBytes) + // { + // if (read == 0) + // { + // if (inStream.BufferingCompleted) + // break; + // if (attempt++ == 20) + // { + // MusicPlayer.SongCancelSource.Cancel(); + // break; + // } + // if (slowconnection) + // { + // _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); + // await Task.Delay(1000, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // else + // { + // await Task.Delay(100, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // } + // else + // attempt = 0; + // } + // else + // attempt = 0; - int nextTime = Environment.TickCount + _milliseconds; - - byte[] buffer = new byte[_frameBytes]; - while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason - !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime - { - //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); - var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - //await inStream.CopyToAsync(voiceClient.OutputStream); - if (read < _frameBytes) - _log.Debug("read {0}", read); - unchecked - { - BytesSent += (ulong)read; - } - if (read < _frameBytes) - { - if (read == 0) - { - if (inStream.BufferingCompleted) - break; - if (attempt++ == 20) - { - MusicPlayer.SongCancelSource.Cancel(); - break; - } - if (slowconnection) - { - _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - - await Task.Delay(1000, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - else - { - await Task.Delay(100, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - } - else - attempt = 0; - } - else - attempt = 0; - - while (MusicPlayer.Paused) - { - await Task.Delay(200, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } + // while (MusicPlayer.Paused) + // { + // await Task.Delay(200, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } - buffer = AdjustVolume(buffer, MusicPlayer.Volume); - if (read != _frameBytes) continue; - nextTime = unchecked(nextTime + _milliseconds); - int delayMillis = unchecked(nextTime - Environment.TickCount); - if (delayMillis > 0) - await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); - await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); - } - } - finally - { - await bufferTask; - inStream.Dispose(); - } - } + // buffer = AdjustVolume(buffer, MusicPlayer.Volume); + // if (read != _frameBytes) continue; + // nextTime = unchecked(nextTime + _milliseconds); + // int delayMillis = unchecked(nextTime - Environment.TickCount); + // if (delayMillis > 0) + // await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); + // await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); + // } + // } + // finally + // { + // await bufferTask; + // inStream.Dispose(); + // } + // } - private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) - { - while (!inStream.BufferingCompleted && inStream.Length < size) - { - await Task.Delay(100, cancelToken); - } - _log.Debug("Buffering successfull"); - } - - //aidiakapi ftw - public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; - - // 16-bit precision for the multiplication - var volumeFixed = (int)Math.Round(volume * 65536d); - - var count = audioSamples.Length / 2; - - fixed (byte* srcBytes = audioSamples) - { - var src = (short*)srcBytes; - - for (var i = count; i != 0; i--, src++) - *src = (short)(((*src) * volumeFixed) >> 16); - } - - return audioSamples; - } - } + // private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) + // { + // while (!inStream.BufferingCompleted && inStream.Length < size) + // { + // await Task.Delay(100, cancelToken); + // } + // _log.Debug("Buffering successfull"); + // } + //} } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index ff3a0ed2..f11c2cce 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,219 +1,219 @@ -using NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +//using NadekoBot.Extensions; +//using NLog; +//using System; +//using System.Diagnostics; +//using System.IO; +//using System.Threading; +//using System.Threading.Tasks; -namespace NadekoBot.Services.Music -{ - /// - /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. - /// It also help for large music by deleting files that are already seen. - /// - class SongBuffer : Stream - { - public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) - { - MusicPlayer = musicPlayer; - Basename = basename; - SongInfo = songInfo; - SkipTo = skipTo; - MaxFileSize = maxFileSize; - CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - _log = LogManager.GetCurrentClassLogger(); - } +//namespace NadekoBot.Services.Music +//{ +// /// +// /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. +// /// It also help for large music by deleting files that are already seen. +// /// +// class SongBuffer : Stream +// { +// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) +// { +// MusicPlayer = musicPlayer; +// Basename = basename; +// SongInfo = songInfo; +// SkipTo = skipTo; +// MaxFileSize = maxFileSize; +// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// _log = LogManager.GetCurrentClassLogger(); +// } - MusicPlayer MusicPlayer { get; } +// MusicPlayer MusicPlayer { get; } - private string Basename { get; } +// private string Basename { get; } - private SongInfo SongInfo { get; } +// private SongInfo SongInfo { get; } - private int SkipTo { get; } +// private int SkipTo { get; } - private int MaxFileSize { get; } = 2.MiB(); +// private int MaxFileSize { get; } = 2.MiB(); - private long FileNumber = -1; +// private long FileNumber = -1; - private long NextFileToRead = 0; +// private long NextFileToRead = 0; - public bool BufferingCompleted { get; private set; } = false; +// public bool BufferingCompleted { get; private set; } = false; - private ulong CurrentBufferSize = 0; +// private ulong CurrentBufferSize = 0; - private FileStream CurrentFileStream; - private Logger _log; +// private FileStream CurrentFileStream; +// private Logger _log; - public Task BufferSong(CancellationToken cancelToken) => - Task.Run(async () => - { - Process p = null; - FileStream outStream = null; - try - { - p = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = false, - CreateNoWindow = true, - }); +// public Task BufferSong(CancellationToken cancelToken) => +// Task.Run(async () => +// { +// Process p = null; +// FileStream outStream = null; +// try +// { +// p = Process.Start(new ProcessStartInfo +// { +// FileName = "ffmpeg", +// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", +// UseShellExecute = false, +// RedirectStandardOutput = true, +// RedirectStandardError = false, +// CreateNoWindow = true, +// }); - byte[] buffer = new byte[81920]; - int currentFileSize = 0; - ulong prebufferSize = 100ul.MiB(); +// byte[] buffer = new byte[81920]; +// int currentFileSize = 0; +// ulong prebufferSize = 100ul.MiB(); - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - while (!p.HasExited) //Also fix low bandwidth - { - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); - if (currentFileSize >= MaxFileSize) - { - try - { - outStream.Dispose(); - } - catch { } - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - currentFileSize = bytesRead; - } - else - { - currentFileSize += bytesRead; - } - CurrentBufferSize += Convert.ToUInt64(bytesRead); - await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - while (CurrentBufferSize > prebufferSize) - await Task.Delay(100, cancelToken); - } - BufferingCompleted = true; - } - catch (System.ComponentModel.Win32Exception) - { - var oldclr = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(@"You have not properly installed or configured FFMPEG. -Please install and configure FFMPEG to play music. -Check the guides for your platform on how to setup ffmpeg correctly: - Windows Guide: https://goo.gl/OjKk8F - Linux Guide: https://goo.gl/ShjCUo"); - Console.ForegroundColor = oldclr; - } - catch (Exception ex) - { - Console.WriteLine($"Buffering stopped: {ex.Message}"); - } - finally - { - if (outStream != null) - outStream.Dispose(); - Console.WriteLine($"Buffering done."); - if (p != null) - { - try - { - p.Kill(); - } - catch { } - p.Dispose(); - } - } - }); +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// while (!p.HasExited) //Also fix low bandwidth +// { +// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); +// if (currentFileSize >= MaxFileSize) +// { +// try +// { +// outStream.Dispose(); +// } +// catch { } +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// currentFileSize = bytesRead; +// } +// else +// { +// currentFileSize += bytesRead; +// } +// CurrentBufferSize += Convert.ToUInt64(bytesRead); +// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); +// while (CurrentBufferSize > prebufferSize) +// await Task.Delay(100, cancelToken); +// } +// BufferingCompleted = true; +// } +// catch (System.ComponentModel.Win32Exception) +// { +// var oldclr = Console.ForegroundColor; +// Console.ForegroundColor = ConsoleColor.Red; +// Console.WriteLine(@"You have not properly installed or configured FFMPEG. +//Please install and configure FFMPEG to play music. +//Check the guides for your platform on how to setup ffmpeg correctly: +// Windows Guide: https://goo.gl/OjKk8F +// Linux Guide: https://goo.gl/ShjCUo"); +// Console.ForegroundColor = oldclr; +// } +// catch (Exception ex) +// { +// Console.WriteLine($"Buffering stopped: {ex.Message}"); +// } +// finally +// { +// if (outStream != null) +// outStream.Dispose(); +// Console.WriteLine($"Buffering done."); +// if (p != null) +// { +// try +// { +// p.Kill(); +// } +// catch { } +// p.Dispose(); +// } +// } +// }); - /// - /// Return the next file to read, and delete the old one - /// - /// Name of the file to read - private string GetNextFile() - { - string filename = Basename + "-" + NextFileToRead; +// /// +// /// Return the next file to read, and delete the old one +// /// +// /// Name of the file to read +// private string GetNextFile() +// { +// string filename = Basename + "-" + NextFileToRead; - if (NextFileToRead != 0) - { - try - { - CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); - File.Delete(Basename + "-" + (NextFileToRead - 1)); - } - catch { } - } - NextFileToRead++; - return filename; - } +// if (NextFileToRead != 0) +// { +// try +// { +// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); +// File.Delete(Basename + "-" + (NextFileToRead - 1)); +// } +// catch { } +// } +// NextFileToRead++; +// return filename; +// } - private bool IsNextFileReady() - { - return NextFileToRead <= FileNumber; - } +// private bool IsNextFileReady() +// { +// return NextFileToRead <= FileNumber; +// } - private void CleanFiles() - { - for (long i = NextFileToRead - 1; i <= FileNumber; i++) - { - try - { - File.Delete(Basename + "-" + i); - } - catch { } - } - } +// private void CleanFiles() +// { +// for (long i = NextFileToRead - 1; i <= FileNumber; i++) +// { +// try +// { +// File.Delete(Basename + "-" + i); +// } +// catch { } +// } +// } - //Stream part +// //Stream part - public override bool CanRead => true; +// public override bool CanRead => true; - public override bool CanSeek => false; +// public override bool CanSeek => false; - public override bool CanWrite => false; +// public override bool CanWrite => false; - public override long Length => (long)CurrentBufferSize; +// public override long Length => (long)CurrentBufferSize; - public override long Position { get; set; } = 0; +// public override long Position { get; set; } = 0; - public override void Flush() { } +// public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) - { - int read = CurrentFileStream.Read(buffer, offset, count); - if (read < count) - { - if (!BufferingCompleted || IsNextFileReady()) - { - CurrentFileStream.Dispose(); - CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - read += CurrentFileStream.Read(buffer, read + offset, count - read); - } - if (read < count) - Array.Clear(buffer, read, count - read); - } - return read; - } +// public override int Read(byte[] buffer, int offset, int count) +// { +// int read = CurrentFileStream.Read(buffer, offset, count); +// if (read < count) +// { +// if (!BufferingCompleted || IsNextFileReady()) +// { +// CurrentFileStream.Dispose(); +// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// read += CurrentFileStream.Read(buffer, read + offset, count - read); +// } +// if (read < count) +// Array.Clear(buffer, read, count - read); +// } +// return read; +// } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } +// public override long Seek(long offset, SeekOrigin origin) +// { +// throw new NotImplementedException(); +// } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } +// public override void SetLength(long value) +// { +// throw new NotImplementedException(); +// } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } +// public override void Write(byte[] buffer, int offset, int count) +// { +// throw new NotImplementedException(); +// } - public new void Dispose() - { - CurrentFileStream.Dispose(); - MusicPlayer.SongCancelSource.Cancel(); - CleanFiles(); - base.Dispose(); - } - } -} \ No newline at end of file +// public new void Dispose() +// { +// CurrentFileStream.Dispose(); +// MusicPlayer.SongCancelSource.Cancel(); +// CleanFiles(); +// base.Dispose(); +// } +// } +//} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs new file mode 100644 index 00000000..4ad63b4a --- /dev/null +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -0,0 +1,85 @@ +using Discord; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using System; +using System.Net; +using System.Text.RegularExpressions; + +namespace NadekoBot.Services.Music +{ + public class SongInfo + { + public string Provider { get; set; } + public MusicType ProviderType { get; set; } + public string Query { get; set; } + public string Title { get; set; } + public string Uri { get; set; } + public string AlbumArt { get; set; } + public string QueuerName { get; set; } + public TimeSpan TotalTime = TimeSpan.Zero; + + public string PrettyProvider => (Provider ?? "???"); + //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**"; + public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`"; + public string PrettyTotalTime + { + get + { + if (TotalTime == TimeSpan.Zero) + return "(?)"; + if (TotalTime == TimeSpan.MaxValue) + return "∞"; + var time = TotalTime.ToString(@"mm\:ss"); + var hrs = (int)TotalTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + return time; + } + } + + public string SongUrl + { + get + { + switch (ProviderType) + { + case MusicType.Normal: + return Query; + case MusicType.Soundcloud: + return Query; + case MusicType.Local: + return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }"; + case MusicType.Radio: + return $"https://google.com/search?q={Title}"; + default: + return ""; + } + } + } + private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); + public string Thumbnail + { + get + { + switch (ProviderType) + { + case MusicType.Radio: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + case MusicType.Normal: + //todo have videoid in songinfo from the start + var videoId = videoIdRegex.Match(Query); + return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + case MusicType.Local: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + case MusicType.Soundcloud: + return AlbumArt; + default: + return ""; + } + } + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver.cs b/src/NadekoBot/Services/Music/SongResolver.cs new file mode 100644 index 00000000..06fe5194 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class SongResolver + { + // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) + // { + // if (string.IsNullOrWhiteSpace(query)) + // throw new ArgumentNullException(nameof(query)); + + // if (musicType != MusicType.Local && IsRadioLink(query)) + // { + // musicType = MusicType.Radio; + // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; + // } + + // try + // { + // switch (musicType) + // { + // case MusicType.Local: + // return new Song(new SongInfo + // { + // Uri = "\"" + Path.GetFullPath(query) + "\"", + // Title = Path.GetFileNameWithoutExtension(query), + // Provider = "Local File", + // ProviderType = musicType, + // Query = query, + // }); + // case MusicType.Radio: + // return new Song(new SongInfo + // { + // Uri = query, + // Title = $"{query}", + // Provider = "Radio Stream", + // ProviderType = musicType, + // Query = query + // }) + // { TotalTime = TimeSpan.MaxValue }; + // } + // if (_sc.IsSoundCloudLink(query)) + // { + // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = musicType, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // if (musicType == MusicType.Soundcloud) + // { + // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Soundcloud, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + // if (string.IsNullOrWhiteSpace(link)) + // throw new OperationCanceledException("Not a valid youtube query."); + // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + // var video = videos + // .Where(v => v.AudioBitrate < 256) + // .OrderByDescending(v => v.AudioBitrate) + // .FirstOrDefault(); + + // if (video == null) // do something with this error + // throw new Exception("Could not load any video elements based on the query."); + // var m = Regex.Match(query, @"\?t=(?\d*)"); + // int gotoTime = 0; + // if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + // var song = new Song(new SongInfo + // { + // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + // Provider = "YouTube", + // Uri = await video.GetUriAsync().ConfigureAwait(false), + // Query = link, + // ProviderType = musicType, + // }); + // song.SkipTo = gotoTime; + // return song; + // } + // catch (Exception ex) + // { + // _log.Warn($"Failed resolving the link.{ex.Message}"); + // _log.Warn(ex); + // return null; + // } + // } + } +}