using Discord; using Discord.Commands; using Discord.Modules; using NadekoBot.Classes; using NadekoBot.DataModels; using NadekoBot.Extensions; using NadekoBot.Modules.Music.Classes; using NadekoBot.Modules.Permissions.Classes; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace NadekoBot.Modules.Music { internal class MusicModule : DiscordModule { public static ConcurrentDictionary MusicPlayers = new ConcurrentDictionary(); public static ConcurrentDictionary DefaultMusicVolumes = new ConcurrentDictionary(); public MusicModule() { } public override string Prefix { get; } = NadekoBot.Config.CommandPrefixes.Music; public override void Install(ModuleManager manager) { var client = NadekoBot.Client; manager.CreateCommands(Prefix, cgb => { cgb.AddCheck(PermissionChecker.Instance); commands.ForEach(cmd => cmd.Init(cgb)); cgb.CreateCommand("next") .Alias("n") .Alias("skip") .Description("Goes to the next song in the queue. You have to be in the same voice channel as the bot.\n**Usage**: `!m n`") .Do(e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (musicPlayer.PlaybackVoiceChannel == e.User.VoiceChannel) musicPlayer.Next(); }); cgb.CreateCommand("stop") .Alias("s") .Description("Stops the music and clears the playlist. Stays in the channel.\n**Usage**: `!m s`") .Do(async e => { await Task.Run(() => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel == musicPlayer.PlaybackVoiceChannel) musicPlayer.Stop(); }).ConfigureAwait(false); }); cgb.CreateCommand("destroy") .Alias("d") .Description("Completely stops the music and unbinds the bot from the channel. " + "(may cause weird behaviour)\n**Usage**: `!m d`") .Do(async e => { await Task.Run(() => { MusicPlayer musicPlayer; if (!MusicPlayers.TryRemove(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel == musicPlayer.PlaybackVoiceChannel) musicPlayer.Destroy(); }).ConfigureAwait(false); }); cgb.CreateCommand("pause") .Alias("p") .Description("Pauses or Unpauses the song.\n**Usage**: `!m p`") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; musicPlayer.TogglePause(); if (musicPlayer.Paused) await e.Channel.SendMessage("🎵`Music Player paused.`").ConfigureAwait(false); else await e.Channel.SendMessage("🎵`Music Player unpaused.`").ConfigureAwait(false); }); cgb.CreateCommand("queue") .Alias("q") .Alias("yq") .Description("Queue a song using keywords or a link. Bot will join your voice channel." + "**You must be in a voice channel**.\n**Usage**: `!m q Dream Of Venice`") .Parameter("query", ParameterType.Unparsed) .Do(async e => { await QueueSong(e.Channel, e.User.VoiceChannel, e.GetArg("query")).ConfigureAwait(false); if (e.Server.CurrentUser.GetPermissions(e.Channel).ManageMessages) { await Task.Delay(10000).ConfigureAwait(false); await e.Message.Delete().ConfigureAwait(false); } }); cgb.CreateCommand("listqueue") .Alias("lq") .Description("Lists 15 currently queued songs per page. Default page is 1.\n**Usage**: `!m lq` or `!m lq 2`") .Parameter("page", ParameterType.Optional) .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) { await e.Channel.SendMessage("🎵 No active music player.").ConfigureAwait(false); return; } int page; if (!int.TryParse(e.GetArg("page"), out page) || page <= 0) { page = 1; } var currentSong = musicPlayer.CurrentSong; if (currentSong == null) return; var toSend = $"🎵`Now Playing` {currentSong.PrettyName} " + $"{currentSong.PrettyCurrentTime()}\n"; if (musicPlayer.RepeatSong) toSend += "🔂"; else if (musicPlayer.RepeatPlaylist) toSend += "🔁"; toSend += $" **{musicPlayer.Playlist.Count}** `tracks currently queued. Showing page {page}` "; if (musicPlayer.Playlist.Count >= MusicPlayer.MaximumPlaylistSize) toSend += "**Song queue is full!**\n"; else toSend += "\n"; const int itemsPerPage = 15; int startAt = itemsPerPage * (page - 1); var number = 1 + startAt; await e.Channel.SendMessage(toSend + string.Join("\n", musicPlayer.Playlist.Skip(startAt).Take(15).Select(v => $"`{number++}.` {v.PrettyName}"))).ConfigureAwait(false); }); cgb.CreateCommand("nowplaying") .Alias("np") .Description("Shows the song currently playing.\n**Usage**: `!m np`") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) return; await e.Channel.SendMessage($"🎵`Now Playing` {currentSong.PrettyName} " + $"{currentSong.PrettyCurrentTime()}").ConfigureAwait(false); }); cgb.CreateCommand("volume") .Alias("vol") .Description("Sets the music volume 0-100%\n**Usage**: `!m vol 50`") .Parameter("val", ParameterType.Required) .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; var arg = e.GetArg("val"); int volume; if (!int.TryParse(arg, out volume)) { await e.Channel.SendMessage("Volume number invalid.").ConfigureAwait(false); return; } volume = musicPlayer.SetVolume(volume); await e.Channel.SendMessage($"🎵 `Volume set to {volume}%`").ConfigureAwait(false); }); cgb.CreateCommand("defvol") .Alias("dv") .Description("Sets the default music volume when music playback is started (0-100)." + " Does not persist through restarts.\n**Usage**: `!m dv 80`") .Parameter("val", ParameterType.Required) .Do(async e => { var arg = e.GetArg("val"); float volume; if (!float.TryParse(arg, out volume) || volume < 0 || volume > 100) { await e.Channel.SendMessage("Volume number invalid.").ConfigureAwait(false); return; } DefaultMusicVolumes.AddOrUpdate(e.Server.Id, volume / 100, (key, newval) => volume / 100); await e.Channel.SendMessage($"🎵 `Default volume set to {volume}%`").ConfigureAwait(false); }); cgb.CreateCommand("mute").Alias("min") .Description("Sets the music volume to 0%\n**Usage**: `!m min`") .Do(e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; musicPlayer.SetVolume(0); }); cgb.CreateCommand("max") .Description("Sets the music volume to 100% (real max is actually 150%).\n**Usage**: `!m max`") .Do(e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; musicPlayer.SetVolume(100); }); cgb.CreateCommand("half") .Description("Sets the music volume to 50%.\n**Usage**: `!m half`") .Do(e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; musicPlayer.SetVolume(50); }); cgb.CreateCommand("shuffle") .Alias("sh") .Description("Shuffles the current playlist.\n**Usage**: `!m sh`") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; if (musicPlayer.Playlist.Count < 2) { await e.Channel.SendMessage("💢 Not enough songs in order to perform the shuffle.").ConfigureAwait(false); return; } musicPlayer.Shuffle(); await e.Channel.SendMessage("🎵 `Songs shuffled.`").ConfigureAwait(false); }); cgb.CreateCommand("playlist") .Alias("pl") .Description("Queues up to 50 songs from a youtube playlist specified by a link, or keywords.\n**Usage**: `!m pl playlist link or name`") .Parameter("playlist", ParameterType.Unparsed) .Do(async e => { var arg = e.GetArg("playlist"); if (string.IsNullOrWhiteSpace(arg)) return; if (e.User.VoiceChannel?.Server != e.Server) { await e.Channel.SendMessage("💢 You need to be in a voice channel on this server.\n If you are already in a voice channel, try rejoining it.").ConfigureAwait(false); return; } var plId = await SearchHelper.GetPlaylistIdByKeyword(arg).ConfigureAwait(false); if (plId == null) { await e.Channel.SendMessage("No search results for that query."); return; } var ids = await SearchHelper.GetVideoIDs(plId, 500).ConfigureAwait(false); if (ids == null || ids.Count == 0) { await e.Channel.SendMessage($"🎵`Failed to find any songs.`"); return; } //todo TEMPORARY SOLUTION, USE RESOLVE QUEUE IN THE FUTURE var idArray = ids as string[] ?? ids.ToArray(); var count = idArray.Length; var msg = await e.Channel.SendMessage($"🎵 `Attempting to queue {count} songs".SnPl(count) + "...`").ConfigureAwait(false); foreach (var id in idArray) { try { await QueueSong(e.Channel, e.User.VoiceChannel, id, true).ConfigureAwait(false); } catch { } } await msg.Edit("🎵 `Playlist queue complete.`").ConfigureAwait(false); }); cgb.CreateCommand("localplaylst") .Alias("lopl") .Description("Queues all songs from a directory. **Bot Owner Only!**\n**Usage**: `!m lopl C:/music/classical`") .Parameter("directory", ParameterType.Unparsed) .AddCheck(SimpleCheckers.OwnerOnly()) .Do(async e => { var arg = e.GetArg("directory"); if (string.IsNullOrWhiteSpace(arg)) return; try { var fileEnum = new DirectoryInfo(arg).GetFiles() .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); foreach (var file in fileEnum) { await QueueSong(e.Channel, e.User.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); } await e.Channel.SendMessage("🎵 `Directory queue complete.`").ConfigureAwait(false); } catch { } }); cgb.CreateCommand("radio").Alias("ra") .Description("Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf\n**Usage**: `!m ra radio link here`") .Parameter("radio_link", ParameterType.Required) .Do(async e => { if (e.User.VoiceChannel?.Server != e.Server) { await e.Channel.SendMessage("💢 You need to be in a voice channel on this server.\n If you are already in a voice channel, try rejoining it.").ConfigureAwait(false); return; } await QueueSong(e.Channel, e.User.VoiceChannel, e.GetArg("radio_link"), musicType: MusicType.Radio).ConfigureAwait(false); if (e.Server.CurrentUser.GetPermissions(e.Channel).ManageMessages) { await Task.Delay(10000).ConfigureAwait(false); await e.Message.Delete().ConfigureAwait(false); } }); cgb.CreateCommand("local") .Alias("lo") .Description("Queues a local file by specifying a full path. **Bot Owner Only!**\n**Usage**: `!m lo C:/music/mysong.mp3`") .Parameter("path", ParameterType.Unparsed) .AddCheck(SimpleCheckers.OwnerOnly()) .Do(async e => { var arg = e.GetArg("path"); if (string.IsNullOrWhiteSpace(arg)) return; await QueueSong(e.Channel, e.User.VoiceChannel, e.GetArg("path"), musicType: MusicType.Local).ConfigureAwait(false); }); cgb.CreateCommand("move") .Alias("mv") .Description("Moves the bot to your voice channel. (works only if music is already playing)\n**Usage**: `!m mv`") .Do(e => { MusicPlayer musicPlayer; var voiceChannel = e.User.VoiceChannel; if (voiceChannel == null || voiceChannel.Server != e.Server || !MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; musicPlayer.MoveToVoiceChannel(voiceChannel); }); cgb.CreateCommand("remove") .Alias("rm") .Description("Remove a song by its # in the queue, or 'all' to remove whole queue.\n**Usage**: `!m rm 5`") .Parameter("num", ParameterType.Required) .Do(async e => { var arg = e.GetArg("num"); MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) { return; } if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; if (arg?.ToLower() == "all") { musicPlayer.ClearQueue(); await e.Channel.SendMessage($"🎵`Queue cleared!`").ConfigureAwait(false); return; } int num; if (!int.TryParse(arg, out num)) { return; } if (num <= 0 || num > musicPlayer.Playlist.Count) return; var song = (musicPlayer.Playlist as List)?[num - 1]; musicPlayer.RemoveSongAt(num - 1); await e.Channel.SendMessage($"🎵**Track {song.PrettyName} at position `#{num}` has been removed.**").ConfigureAwait(false); }); //var msRegex = new Regex(@"(?\d+)>(?\d+)", RegexOptions.Compiled); cgb.CreateCommand("movesong") .Alias("ms") .Description($"Moves a song from one position to another.\n**Usage**: `{Prefix} ms` 5>3") .Parameter("fromto") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) { return; } var fromto = e.GetArg("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 e.Channel.SendMessage("`Invalid input.`"); return; } var s = playlist[n1 - 1]; playlist.Insert(n2 - 1, s); playlist.RemoveAt(n1 - 1); await e.Channel.SendMessage($"🎵`Moved` {s.PrettyName} `from #{n1} to #{n2}`"); }); cgb.CreateCommand("cleanup") .Description("Cleans up hanging voice connections. **Bot Owner Only!**\n**Usage**: `!m cleanup`") .AddCheck(SimpleCheckers.OwnerOnly()) .Do(e => { foreach (var kvp in MusicPlayers) { var songs = kvp.Value.Playlist; var currentSong = kvp.Value.CurrentSong; if (songs.Count == 0 && currentSong == null) { MusicPlayer throwaway; MusicPlayers.TryRemove(kvp.Key, out throwaway); throwaway.Destroy(); } } }); cgb.CreateCommand("reptcursong") .Alias("rcs") .Description("Toggles repeat of current song.\n**Usage**: `!m rcs`") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) return; var currentValue = musicPlayer.ToggleRepeatSong(); await e.Channel.SendMessage(currentValue ? $"🎵🔂`Repeating track:`{currentSong.PrettyName}" : $"🎵🔂`Current track repeat stopped.`") .ConfigureAwait(false); }); cgb.CreateCommand("rpeatplaylst") .Alias("rpl") .Description("Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue).\n**Usage**: `!m rpl`") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; var currentValue = musicPlayer.ToggleRepeatPlaylist(); await e.Channel.SendMessage($"🎵🔁`Repeat playlist {(currentValue ? "enabled" : "disabled")}`").ConfigureAwait(false); }); cgb.CreateCommand("save") .Description("Saves a playlist under a certain name. Name must be no longer than 20 characters and mustn't contain dashes.\n**Usage**: `!m save classical1`") .Parameter("name", ParameterType.Unparsed) .Do(async e => { var name = e.GetArg("name")?.Trim(); if (string.IsNullOrWhiteSpace(name) || name.Length > 20 || name.Contains("-")) return; MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; //to avoid concurrency issues var currentPlaylist = new List(musicPlayer.Playlist); var curSong = musicPlayer.CurrentSong; if (curSong != null) currentPlaylist.Insert(0, curSong); if (!currentPlaylist.Any()) return; var songInfos = currentPlaylist.Select(s => new DataModels.SongInfo { Provider = s.SongInfo.Provider, ProviderType = (int)s.SongInfo.ProviderType, Title = s.SongInfo.Title, Uri = s.SongInfo.Uri, Query = s.SongInfo.Query, }).ToList(); var playlist = new MusicPlaylist { CreatorId = (long)e.User.Id, CreatorName = e.User.Name, Name = name.ToLowerInvariant(), }; DbHandler.Instance.SaveAll(songInfos); DbHandler.Instance.Save(playlist); DbHandler.Instance.InsertMany(songInfos.Select(s => new PlaylistSongInfo { PlaylistId = playlist.Id.Value, SongInfoId = s.Id.Value })); await e.Channel.SendMessage($"🎵 `Saved playlist as {name}-{playlist.Id}`").ConfigureAwait(false); }); cgb.CreateCommand("load") .Description("Loads a playlist under a certain name. \n**Usage**: `!m load classical-1`") .Parameter("name", ParameterType.Unparsed) .Do(async e => { var voiceCh = e.User.VoiceChannel; var textCh = e.Channel; if (voiceCh == null || voiceCh.Server != textCh.Server) { await textCh.SendMessage("💢 You need to be in a voice channel on this server.\n If you are already in a voice channel, try rejoining.").ConfigureAwait(false); return; } var name = e.GetArg("name")?.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(name)) return; var parts = name.Split('-'); if (parts.Length != 2) return; var playlistName = parts[0]; int playlistNumber; if (!int.TryParse(parts[1], out playlistNumber)) return; var playlist = DbHandler.Instance.FindOne( p => p.Id == playlistNumber); if (playlist == null) { await e.Channel.SendMessage("Can't find playlist under that name.").ConfigureAwait(false); return; } var psis = DbHandler.Instance.FindAll(psi => psi.PlaylistId == playlist.Id); var songInfos = psis.Select(psi => DbHandler.Instance .FindOne(si => si.Id == psi.SongInfoId)); await e.Channel.SendMessage($"`Attempting to load {songInfos.Count()} songs`").ConfigureAwait(false); foreach (var si in songInfos) { try { await QueueSong(textCh, voiceCh, si.Query, true, (MusicType)si.ProviderType).ConfigureAwait(false); } catch (Exception ex) { Console.WriteLine($"Failed QueueSong in load playlist. {ex}"); } } }); cgb.CreateCommand("playlists") .Alias("pls") .Description("Lists all playlists. Paginated. 20 per page. Default page is 0.\n**Usage**:`!m pls 1`") .Parameter("num", ParameterType.Optional) .Do(e => { int num = 0; int.TryParse(e.GetArg("num"), out num); if (num < 0) return; var result = DbHandler.Instance.GetPlaylistData(num); if (result.Count == 0) e.Channel.SendMessage($"`No saved playlists found on page {num}`"); else e.Channel.SendMessage($"```js\n--- List of saved playlists ---\n\n" + string.Join("\n", result.Select(r => $"'{r.Name}-{r.Id}' by {r.Creator} ({r.SongCnt} songs)")) + $"\n\n --- Page {num} ---```"); }); cgb.CreateCommand("goto") .Description("Goes to a specific time in seconds in a song.") .Parameter("time") .Do(async e => { var skipToStr = e.GetArg("time")?.Trim(); MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; if (e.User.VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; int skipTo; if (!int.TryParse(skipToStr, out skipTo) || skipTo < 0) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) return; //currentSong.PrintStatusMessage = false; var gotoSong = currentSong.Clone(); gotoSong.SkipTo = skipTo; musicPlayer.AddSong(gotoSong, 0); musicPlayer.Next(); var minutes = (skipTo / 60).ToString(); var seconds = (skipTo % 60).ToString(); if (minutes.Length == 1) minutes = "0" + minutes; if (seconds.Length == 1) seconds = "0" + seconds; await e.Channel.SendMessage($"`Skipped to {minutes}:{seconds}`").ConfigureAwait(false); }); cgb.CreateCommand("getlink") .Alias("gl") .Description("Shows a link to the currently playing song.") .Do(async e => { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(e.Server, out musicPlayer)) return; var curSong = musicPlayer.CurrentSong; if (curSong == null) return; await e.Channel.SendMessage($"🎶`Current song:` <{curSong.SongInfo.Query}>"); }); }); } private async Task QueueSong(Channel textCh, Channel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) { if (voiceCh == null || voiceCh.Server != textCh.Server) { if (!silent) await textCh.SendMessage("💢 You need to be in a voice channel on this server.\n If you are already in a voice channel, try rejoining.").ConfigureAwait(false); throw new ArgumentNullException(nameof(voiceCh)); } if (string.IsNullOrWhiteSpace(query) || query.Length < 3) throw new ArgumentException("💢 Invalid query for queue song.", nameof(query)); var musicPlayer = MusicPlayers.GetOrAdd(textCh.Server, server => { float? vol = null; float throwAway; if (DefaultMusicVolumes.TryGetValue(server.Id, out throwAway)) vol = throwAway; var mp = new MusicPlayer(voiceCh, vol); Message playingMessage = null; Message lastFinishedMessage = null; mp.OnCompleted += async (s, song) => { if (song.PrintStatusMessage) { try { if (lastFinishedMessage != null) await lastFinishedMessage.Delete().ConfigureAwait(false); if (playingMessage != null) await playingMessage.Delete().ConfigureAwait(false); lastFinishedMessage = await textCh.SendMessage($"🎵`Finished`{song.PrettyName}").ConfigureAwait(false); } catch { } } }; mp.OnStarted += async (s, song) => { if (song.PrintStatusMessage) { var sender = s as MusicPlayer; if (sender == null) return; try { var msgTxt = $"🎵`Playing`{song.PrettyName} `Vol: {(int)(sender.Volume * 100)}%`"; playingMessage = await textCh.SendMessage(msgTxt).ConfigureAwait(false); } catch { } } }; return mp; }); var resolvedSong = await Song.ResolveSong(query, musicType).ConfigureAwait(false); resolvedSong.MusicPlayer = musicPlayer; musicPlayer.AddSong(resolvedSong); if (!silent) { var queuedMessage = await textCh.SendMessage($"🎵`Queued`{resolvedSong.PrettyName} **at** `#{musicPlayer.Playlist.Count}`").ConfigureAwait(false); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Task.Run(async () => { await Task.Delay(10000).ConfigureAwait(false); try { await queuedMessage.Delete().ConfigureAwait(false); } catch { } }).ConfigureAwait(false); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } } }