diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 4f9db9be..4c8764db 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -48,7 +48,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 38400) + public PoopyRingBuffer(int capacity = 3640 * 200) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 3681d631..c82e59cc 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -111,11 +111,15 @@ namespace NadekoBot.Modules.Music } } } + //todo add play command. .play = .n, .play whatever = .q whatever + + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { + //todo add a notice that player is stopped if user queues a song while it is var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); await InternalQueue(mp, songInfo, false); @@ -209,11 +213,9 @@ namespace NadekoBot.Modules.Music if (mp.RepeatCurrentSong) desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; - //else if (musicPlayer.RepeatPlaylist) - // desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; - - - + else if (mp.Shuffle) + desc = "🔀 " + GetText("shuffling_playlist") + "\n\n" + desc; + var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) @@ -307,7 +309,7 @@ namespace NadekoBot.Modules.Music { var song = mp.RemoveAt(index - 1); var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon()) .WithDescription(song.PrettyName) .WithFooter(ef => ef.WithText(song.PrettyInfo)) .WithErrorColor(); @@ -519,22 +521,18 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task ShufflePlaylist() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - // if (musicPlayer.Playlist.Count < 2) - // return; - - // musicPlayer.Shuffle(); - // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - //} + //todo test shuffle + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ShufflePlaylist() + { + var mp = await _music.GetOrCreatePlayer(Context); + var val = mp.ToggleShuffle(); + if(val) + await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false); + else + await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -687,17 +685,22 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Move() + { + var vch = ((IGuildUser)Context.User).VoiceChannel; - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} + if (vch == null) + return; + + var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + + if (mp == null) + return; + //todo test move + mp.SetVoiceChannel(vch); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -745,21 +748,21 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SetMaxQueue(uint size = 0) - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxQueue(uint size = 0) + { + if (size < 0) + return; + var mp = await _music.GetOrCreatePlayer(Context); - // musicPlayer.MaxQueueSize = size; + mp.SetMaxQueueSize(size); - // if(size == 0) - // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - //} + if (size == 0) + await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + else + await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -800,19 +803,17 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task RepeatPl() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // var currentValue = musicPlayer.ToggleRepeatPlaylist(); - // if(currentValue) - // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - //} + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RepeatPl() + { + var mp = await _music.GetOrCreatePlayer(Context); + var currentValue = mp.ToggleRepeatPlaylist(); + if (currentValue) + await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -849,19 +850,17 @@ namespace NadekoBot.Modules.Music // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Autoplay() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Autoplay() + { + var mp = await _music.GetOrCreatePlayer(Context); - // if (!musicPlayer.ToggleAutoplay()) - // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); - //} + if (!mp.ToggleAutoplay()) + await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 200487b2..d75cd963 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -183,7 +183,7 @@ namespace NadekoBot #endregion var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); - var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); + var musicService = new MusicService(Client, GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); #region Games diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 97581b2f..d6cac926 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Services.Music public class MusicPlayer { private readonly Task _player; - private readonly IVoiceChannel VoiceChannel; + private IVoiceChannel VoiceChannel { get; set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); @@ -42,9 +42,13 @@ namespace NadekoBot.Services.Music } public bool RepeatCurrentSong { get; private set; } + public bool Shuffle { get; private set; } + public bool Autoplay { get; private set; } + public bool RepeatPlaylist { get; private set; } = true; private IAudioClient _audioClient; private readonly object locker = new object(); + private MusicService _musicService; #region events public event Action OnStarted; @@ -59,6 +63,7 @@ namespace NadekoBot.Services.Music this.VoiceChannel = vch; this.SongCancelSource = new CancellationTokenSource(); this.OutputTextChannel = output; + this._musicService = musicService; _player = Task.Run(async () => { @@ -96,42 +101,43 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music); - - OnStarted?.Invoke(this, data.Song); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try + using (var pcm = ac.CreatePCMStream(AudioApplication.Music)) { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); - await (pauseTaskSource?.Task ?? Task.CompletedTask); + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); - OnCompleted?.Invoke(this, data.Song); + OnCompleted?.Invoke(this, data.Song); + } } } } @@ -143,8 +149,35 @@ namespace NadekoBot.Services.Music await Task.Delay(500); } while (Stopped && !Exited); - if(!RepeatCurrentSong) - Queue.Next(); + if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) + { + if (Shuffle) + Queue.Random(); //if shuffle is set, set current song index to a random number + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (Queue.Count == data.Index && Autoplay && data.Song?.Provider == "YouTube") + { + try + { + //todo test autoplay + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch { } + } + else if (Queue.Count == data.Index && !RepeatPlaylist) + { + //todo test repeatplaylist + Stop(); + } + else + { + Queue.Next(); + } + } + } } } }, SongCancelSource.Token); @@ -313,6 +346,41 @@ namespace NadekoBot.Services.Music await ac.StopAsync(); } + public bool ToggleShuffle() + { + lock (locker) + { + return Shuffle = !Shuffle; + } + } + + public bool ToggleAutoplay() + { + lock (locker) + { + return Autoplay = !Autoplay; + } + } + + public bool ToggleRepeatPlaylist() + { + lock (locker) + { + return RepeatPlaylist = !RepeatPlaylist; + } + } + + public void SetMaxQueueSize(uint size) + { + Queue.SetMaxQueueSize(size); + } + + public void SetVoiceChannel(IVoiceChannel vch) + { + VoiceChannel = vch; + Next(); + } + //private IAudioClient AudioClient { get; set; } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index cf3a0818..dd5986aa 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -1,4 +1,5 @@ -using System; +using NadekoBot.Extensions; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -49,17 +50,23 @@ namespace NadekoBot.Services.Music } } + public uint maxQueueSize { get; private set; } + public void Add(SongInfo song) { + song.ThrowIfNull(nameof(song)); lock (locker) { + if(CurrentIndex >= maxQueueSize) + throw new PlaylistFullException(); Songs.AddLast(song); } } public void Next() { - CurrentIndex++; + lock(locker) + CurrentIndex++; } public void Dispose() @@ -118,5 +125,24 @@ namespace NadekoBot.Services.Music CurrentIndex = 0; } } + + public void Random() + { + lock (locker) + { + CurrentIndex = new NadekoRandom().Next(Songs.Count); + } + } + + public void SetMaxQueueSize(uint size) + { + if (size < 0) + throw new ArgumentOutOfRangeException(nameof(size)); + + lock (locker) + { + maxQueueSize = size; + } + } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 986fd0fd..ccc70dbc 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -10,6 +10,7 @@ using System.IO; using VideoLibrary; using System.Collections.Generic; using Discord.Commands; +using Discord.WebSocket; namespace NadekoBot.Services.Music { @@ -25,13 +26,15 @@ namespace NadekoBot.Services.Music private readonly SoundCloudApiService _sc; private readonly IBotCredentials _creds; private readonly ConcurrentDictionary _defaultVolumes; + private readonly DiscordSocketClient _client; public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, + public MusicService(DiscordSocketClient client, IGoogleApiService google, NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { + _client = client; _google = google; _strings = strings; _localization = localization; @@ -187,6 +190,25 @@ namespace NadekoBot.Services.Music }); } + public MusicPlayer GetPlayerOrDefault(ulong guildId) + { + if (MusicPlayers.TryGetValue(guildId, out var mp)) + return mp; + else + return null; + } + + public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch) + { + var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray(); + if (!related.Any()) + return; + + var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.Normal); + var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); + mp.Enqueue(si); + } + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { query.ThrowIfNull(nameof(query)); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index c5c08175..5f4c8c91 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -b:a 128k -ac 2 pipe:1 -loglevel quiet -nostdin", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 4ad63b4a..28d407ee 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -4,6 +4,7 @@ using NadekoBot.Services.Database.Models; using System; using System.Net; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace NadekoBot.Services.Music { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index e7a3b885..378ac8a3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -442,12 +442,14 @@ "music_repeating_playlist": "Repeating playlist", "music_repeating_track": "Repeating track", "music_repeating_track_stopped": "Current track repeat stopped.", + "music_shuffling_playlist": "Shuffling songs", "music_resumed": "Music playback resumed.", "music_rpl_disabled": "Repeat playlist disabled.", "music_rpl_enabled": "Repeat playlist enabled.", "music_set_music_channel": "I will now output playing, finished, paused and removed songs in this channel.", "music_skipped_to": "Skipped to `{0}:{1}`", - "music_songs_shuffled": "Songs shuffled", + "music_songs_shuffle_enable": "Songs will shuffle from now on.", + "music_songs_shuffle_disable": "Songs will no longer shuffle.", "music_song_moved": "Song moved", "music_song_not_found": "No song found.", "music_time_format": "{0}h {1}m {2}s",