using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Discord; using Discord.Commands; using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules; using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Music.Services; namespace NadekoBot.Modules.Music { [NoPublicBot] public sealed partial class Music : NadekoModule { private readonly ILogCommandService _logService; public Music(ILogCommandService _logService) { this._logService = _logService; } private async Task ValidateAsync() { var user = (IGuildUser) ctx.User; var userVoiceChannelId = user.VoiceChannel?.Id; if (userVoiceChannelId is null) { await ReplyErrorLocalizedAsync("must_be_in_voice"); return false; } var currentUser = await ctx.Guild.GetCurrentUserAsync(); if (currentUser.VoiceChannel?.Id != userVoiceChannelId) { await ReplyErrorLocalizedAsync("not_with_bot_in_voice"); return false; } return true; } private static readonly SemaphoreSlim voiceChannelLock = new SemaphoreSlim(1, 1); private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) { botUser ??= await ctx.Guild.GetCurrentUserAsync(); await voiceChannelLock.WaitAsync(); try { if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(Context.Guild.Id, out _)) await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); } finally { voiceChannelLock.Release(); } } private async Task QueuePreconditionInternalAsync() { var user = (IGuildUser) Context.User; var voiceChannelId = user.VoiceChannel?.Id; if (voiceChannelId is null) { await ReplyErrorLocalizedAsync("must_be_in_voice"); return false; } _ = ctx.Channel.TriggerTypingAsync(); var botUser = await ctx.Guild.GetCurrentUserAsync(); await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); if (botUser.VoiceChannel?.Id != voiceChannelId) { await ReplyErrorLocalizedAsync("not_with_bot_in_voice"); return false; } return true; } private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null) { var succ = await QueuePreconditionInternalAsync(); if (!succ) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, Context.User.ToString(), asNext, forcePlatform); if (trackInfo is null) { await ReplyErrorLocalizedAsync("song_not_found"); return; } try { var embed = new EmbedBuilder() .WithOkColor() .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon()) .WithDescription($"{trackInfo.PrettyName()}\n{GetText("queue")} ") .WithFooter(ef => ef.WithText(trackInfo.Platform.ToString())); if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail)) embed.WithThumbnailUrl(trackInfo.Thumbnail); var queuedMessage = await _service.SendToOutputAsync(Context.Guild.Id, embed).ConfigureAwait(false); queuedMessage?.DeleteAfter(10, _logService); if (mp.IsStopped) { var msg = await ReplyErrorLocalizedAsync("queue_stopped", Format.Code(Prefix + "play")); msg.DeleteAfter(10, _logService); } } catch { // ignored } } private async Task MoveToIndex(int index) { if (--index < 0) return; var succ = await QueuePreconditionInternalAsync(); if (!succ) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } mp.MoveTo(index); } // join vc [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Join() { var user = (IGuildUser) Context.User; var voiceChannelId = user.VoiceChannel?.Id; if (voiceChannelId is null) { await ReplyErrorLocalizedAsync("must_be_in_voice"); return; } await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value); } // leave vc (destroy) [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Destroy() { var valid = await ValidateAsync(); if (!valid) return; await _service.LeaveVoiceChannelAsync(Context.Guild.Id); } // play - no args = next [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [Priority(2)] public Task Play() => Next(); // play - index = skip to that index [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [Priority(1)] public Task Play(int index) => MoveToIndex(index); // play - query = q(query) [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] public Task Play([Leftover] string query) => QueueByQuery(query); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public Task Queue([Leftover] string query) => QueueByQuery(query); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public Task QueueNext([Leftover] string query) => QueueByQuery(query, asNext: true); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Volume(int vol) { if (vol < 0 || vol > 100) { await ReplyErrorLocalizedAsync("volume_input_invalid"); return; } var valid = await ValidateAsync(); if (!valid) return; await _service.SetVolumeAsync(ctx.Guild.Id, vol); await ReplyConfirmLocalizedAsync("volume_set", vol); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Next() { var valid = await ValidateAsync(); if (!valid) return; var success = await _service.PlayAsync(Context.Guild.Id, ((IGuildUser)Context.User).VoiceChannel.Id); if (!success) { await ReplyErrorLocalizedAsync("no_player"); return; } } private const int LQ_ITEMS_PER_PAGE = 9; // list queue, relevant page [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue() { // show page with the current song if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) { await ReplyErrorLocalizedAsync("no_player"); return; } await ListQueue(mp.CurrentIndex / LQ_ITEMS_PER_PAGE + 1); } // list queue, specify page [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page) { if (--page < 0) return; IReadOnlyCollection tracks; if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0) { await ReplyErrorLocalizedAsync("no_player"); return; } EmbedBuilder printAction(int curPage) { string desc = string.Empty; var current = mp.GetCurrentTrack(out var currentIndex); if (current is not null) { desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc; } var repeatType = mp.Repeat; var add = ""; if (mp.IsStopped) add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; // var mps = mp.MaxPlaytimeSeconds; // if (mps > 0) // add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n"; if (repeatType == PlayerRepeatType.Track) { add += "🔂 " + GetText("repeating_track") + "\n"; } else { // if (mp.Autoplay) // add += "↪ " + GetText("autoplaying") + "\n"; // if (mp.FairPlay && !mp.Autoplay) // add += " " + GetText("fairplay") + "\n"; if (repeatType == PlayerRepeatType.Queue) add += "🔁 " + GetText("repeating_queue") + "\n"; } desc += tracks .Skip(LQ_ITEMS_PER_PAGE * curPage) .Take(LQ_ITEMS_PER_PAGE) .Select((v, index) => { index += LQ_ITEMS_PER_PAGE * curPage; if (index == currentIndex) return $"**⇒**`{index + 1}.` {v.PrettyFullName()}"; return $"`{index + 1}.` {v.PrettyFullName()}"; }) .JoinWith('\n'); if (!string.IsNullOrWhiteSpace(add)) desc = add + "\n" + desc; var embed = new EmbedBuilder() .WithAuthor(eab => eab .WithName(GetText("player_queue", curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)) .WithMusicIcon()) .WithDescription(desc) .WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") .WithOkColor(); return embed; } await ctx.SendPaginatedConfirmAsync( page, printAction, tracks.Count, LQ_ITEMS_PER_PAGE, false); } // search [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task QueueSearch([Leftover] string query) { _ = ctx.Channel.TriggerTypingAsync(); var videos = await _service.SearchVideosAsync(query); if (videos is null || videos.Count == 0) { await ReplyErrorLocalizedAsync("song_not_found").ConfigureAwait(false); return; } var resultsString = videos .Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}") .JoinWith('\n'); var msg = await ctx.Channel.SendConfirmAsync(resultsString); try { var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false); if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count) { _logService.AddDeleteIgnore(msg.Id); try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } return; } query = videos[index].Url; await Play(query); } finally { _logService.AddDeleteIgnore(msg.Id); try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [Priority(1)] public async Task TrackRemove(int index) { if (index < 1) { await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false); return; } var valid = await ValidateAsync(); if (!valid) return; if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) { await ReplyErrorLocalizedAsync("no_player"); return; } if (!mp.TryRemoveTrackAt(index - 1, out var song)) { await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false); return; } var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon()) .WithDescription(song.PrettyName()) .WithFooter(ef => ef.WithText(song.PrettyInfo())) .WithErrorColor(); await _service.SendToOutputAsync(Context.Guild.Id, embed); } public enum All { All = -1 } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] public async Task TrackRemove(All _ = All.All) { var valid = await ValidateAsync(); if (!valid) return; if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) { await ReplyErrorLocalizedAsync("no_player"); return; } mp.Clear(); await ReplyConfirmLocalizedAsync("queue_cleared").ConfigureAwait(false); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Stop() { var valid = await ValidateAsync(); if (!valid) return; if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) { await ReplyErrorLocalizedAsync("no_player"); return; } mp.Stop(); } public enum InputRepeatType { N = 0, No = 0, None = 0, T = 1, Track = 1, S = 1, Song = 1, Q = 2, Queue = 2, Playlist = 2, Pl = 2, } private PlayerRepeatType InputToDbType(InputRepeatType type) => type switch { InputRepeatType.None => PlayerRepeatType.None, InputRepeatType.Queue => PlayerRepeatType.Queue, InputRepeatType.Track => PlayerRepeatType.Track, _ => PlayerRepeatType.Queue }; [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue) { var valid = await ValidateAsync(); if (!valid) return; await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type)); if (type == InputRepeatType.None) await ReplyConfirmLocalizedAsync("repeating_none"); else if (type == InputRepeatType.Queue) await ReplyConfirmLocalizedAsync("repeating_queue"); else await ReplyConfirmLocalizedAsync("repeating_track"); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() { await ReplyPendingLocalizedAsync("obsolete_use", $"`{Prefix}qrp song`"); await QueueRepeat(InputRepeatType.Song); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Pause() { var valid = await ValidateAsync(); if (!valid) return; if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null) { await ReplyErrorLocalizedAsync("no_player"); return; } mp.TogglePause(); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public Task Radio(string radioLink) => QueueByQuery(radioLink, false, MusicPlatform.Radio); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] public Task Local([Leftover] string path) => QueueByQuery(path, false, MusicPlatform.Local); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] public async Task LocalPlaylist([Leftover] string dirPath) { if (string.IsNullOrWhiteSpace(dirPath)) return; var user = (IGuildUser) Context.User; var voiceChannelId = user.VoiceChannel?.Id; if (voiceChannelId is null) { await ReplyErrorLocalizedAsync("must_be_in_voice"); return; } _ = ctx.Channel.TriggerTypingAsync(); var botUser = await ctx.Guild.GetCurrentUserAsync(); await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); if (botUser.VoiceChannel?.Id != voiceChannelId) { await ReplyErrorLocalizedAsync("not_with_bot_in_voice"); return; } var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString()); await ReplyConfirmLocalizedAsync("dir_queue_complete").ConfigureAwait(false); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task MoveSong(int from, int to) { if (--from < 0 || --to < 0 || from == to) { await ReplyErrorLocalizedAsync("invalid_input").ConfigureAwait(false); return; } var valid = await ValidateAsync(); if (!valid) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } var track = mp.MoveTrack(from, to); if (track is null) { await ReplyErrorLocalizedAsync("invalid_input").ConfigureAwait(false); return; } var embed = new EmbedBuilder() .WithTitle(track.Title.TrimTo(65)) .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($"#{from + 1}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{to + 1}").WithIsInline(true)) .WithColor(Bot.OkColor); if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) embed.WithUrl(track.Url); await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public Task SoundCloudQueue([Leftover] string query) => QueueByQuery(query, false, MusicPlatform.SoundCloud); [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task SoundCloudPl([Leftover] string playlist) { if (string.IsNullOrWhiteSpace(playlist)) return; var succ = await QueuePreconditionInternalAsync(); if (!succ) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } _ = ctx.Channel.TriggerTypingAsync(); await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString()); await ctx.OkAsync(); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task Playlist([Leftover] string playlistQuery) { if (string.IsNullOrWhiteSpace(playlistQuery)) return; var succ = await QueuePreconditionInternalAsync(); if (!succ) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } _ = Context.Channel.TriggerTypingAsync(); var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString()); if (queuedCount == 0) { await ReplyErrorLocalizedAsync("no_search_results").ConfigureAwait(false); return; } await ctx.OkAsync(); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task NowPlaying() { var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } var currentTrack = mp.GetCurrentTrack(out _); if (currentTrack is null) return; var embed = new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) .WithDescription(currentTrack.PrettyName()) .WithThumbnailUrl(currentTrack.Thumbnail) .WithFooter($"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task PlaylistShuffle() { var valid = await ValidateAsync(); if (!valid) return; var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel); if (mp is null) { await ReplyErrorLocalizedAsync("no_player"); return; } mp.ShuffleQueue(); await ReplyConfirmLocalizedAsync("queue_shuffled"); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] public async Task SetMusicChannel() { await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id); await ReplyConfirmLocalizedAsync("set_music_channel"); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] public async Task UnsetMusicChannel() { await _service.SetMusicChannelAsync(ctx.Guild.Id, null); await ReplyConfirmLocalizedAsync("unset_music_channel"); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] public async Task AutoDisconnect() { var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id); if(newState) await ReplyConfirmLocalizedAsync("autodc_enable"); else await ReplyConfirmLocalizedAsync("autodc_disable"); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.Administrator)] public async Task MusicQuality() { var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id); await ReplyConfirmLocalizedAsync("current_music_quality", Format.Bold(quality.ToString())); } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.Administrator)] public async Task MusicQuality(QualityPreset preset) { await _service.SetMusicQualityAsync(ctx.Guild.Id, preset); await ReplyConfirmLocalizedAsync("music_quality_set", Format.Bold(preset.ToString())); } } }