From 29a0dbdfce42d51a07782d614842921ebf5b3873 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 27 Feb 2016 08:38:22 +0100 Subject: [PATCH] continued work on music --- NadekoBot/Classes/Music/MusicControls.cs | 180 +++------- NadekoBot/Classes/Music/Song.cs | 191 +++++++++++ NadekoBot/Classes/Music/StreamRequest.cs | 56 +-- NadekoBot/Commands/PlayingRotate.cs | 4 +- NadekoBot/Modules/Administration.cs | 2 +- NadekoBot/Modules/Music.cs | 418 ++++++++--------------- NadekoBot/NadekoBot.csproj | 1 + 7 files changed, 389 insertions(+), 463 deletions(-) create mode 100644 NadekoBot/Classes/Music/Song.cs diff --git a/NadekoBot/Classes/Music/MusicControls.cs b/NadekoBot/Classes/Music/MusicControls.cs index 64cb008c..6e997619 100644 --- a/NadekoBot/Classes/Music/MusicControls.cs +++ b/NadekoBot/Classes/Music/MusicControls.cs @@ -1,13 +1,10 @@ using Discord; using Discord.Audio; +using NadekoBot.Extensions; using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Discord.Commands; -using MusicModule = NadekoBot.Modules.Music; -using System.Collections; -using NadekoBot.Extensions; using System.Threading; +using System.Threading.Tasks; namespace NadekoBot.Classes.Music { @@ -16,16 +13,18 @@ namespace NadekoBot.Classes.Music { Normal, Local } - public class Song { - public StreamState State { get; internal set; } - private Song() { } - - internal Task Play(CancellationToken cancelToken) { - throw new NotImplementedException(); - } + public enum StreamState { + Resolving, + Queued, + Buffering, //not using it atm + Playing, + Completed } + public class MusicPlayer { + public static int MaximumPlaylistSize => 50; + private IAudioClient _client { get; set; } private List _playlist = new List(); @@ -40,18 +39,31 @@ namespace NadekoBot.Classes.Music { public float Volume { get; private set; } - public MusicPlayer(IAudioClient client) { - if (client == null) - throw new ArgumentNullException(nameof(client)); - _client = client; + public Action OnCompleted = delegate { }; + public Action OnStarted = delegate { }; + + public Channel PlaybackVoiceChannel { get; private set; } + + public MusicPlayer(Channel startingVoiceChannel, float defaultVolume) { + if (startingVoiceChannel == null) + throw new ArgumentNullException(nameof(startingVoiceChannel)); + if (startingVoiceChannel.Type != ChannelType.Voice) + throw new ArgumentException("Channel must be of type voice"); + + PlaybackVoiceChannel = startingVoiceChannel; SongCancelSource = new CancellationTokenSource(); cancelToken = SongCancelSource.Token; + Task.Run(async () => { - while (_client?.State == ConnectionState.Connected) { + while (_client?.State != ConnectionState.Disconnected && + _client?.State != ConnectionState.Disconnecting) { + CurrentSong = GetNextSong(); if (CurrentSong != null) { try { - await CurrentSong.Play(cancelToken); + _client = await PlaybackVoiceChannel.JoinAudio(); + OnStarted(CurrentSong); + CurrentSong.Play(_client, cancelToken); } catch (OperationCanceledException) { Console.WriteLine("Song canceled"); @@ -59,19 +71,17 @@ namespace NadekoBot.Classes.Music { catch (Exception ex) { Console.WriteLine($"Exception in PlaySong: {ex}"); } + OnCompleted(CurrentSong); SongCancelSource = new CancellationTokenSource(); cancelToken = SongCancelSource.Token; } - else { - await Task.Delay(1000); - } + await Task.Delay(1000); } - await Stop(); }); } public void Next() { - if(!SongCancelSource.IsCancellationRequested) + if (!SongCancelSource.IsCancellationRequested) SongCancelSource.Cancel(); } @@ -87,22 +97,25 @@ namespace NadekoBot.Classes.Music { catch { Console.WriteLine("This shouldn't happen"); } + Console.WriteLine("Disconnecting"); await _client?.Disconnect(); } + public void TogglePause() => Paused = !Paused; + public void Shuffle() { lock (_playlist) { _playlist.Shuffle(); } } - public void SetVolume(float volume) { + public int SetVolume(int volume) { if (volume < 0) volume = 0; if (volume > 150) volume = 150; - Volume = volume / 100.0f; + return (int)(Volume = volume / 100.0f); } private Song GetNextSong() { @@ -139,118 +152,17 @@ namespace NadekoBot.Classes.Music { } } - /* - private CommandEventArgs _e; - public bool NextSong { get; set; } = false; - public IAudioClient Voice { get; set; } - - public bool Pause { get; set; } = false; - public List SongQueue { get; set; } = new List(); - public StreamRequest CurrentSong { get; set; } = null; - public float Volume { get; set; } = .5f; - - public bool IsPaused { get; internal set; } = false; - public bool Stopped { get; private set; } - - public Channel VoiceChannel { get; set; } = null; - - public IAudioClient VoiceClient { get; set; } = null; - - private readonly object _voiceLock = new object(); - - public MusicPlayer() { - Task.Run(async () => { - while (true) { - if (!Stopped) { - if (CurrentSong == null) { - if (SongQueue.Count > 0) - await LoadNextSong(); - - } - else if (CurrentSong.State == StreamState.Completed || NextSong) { - NextSong = false; - await LoadNextSong(); - } - } - else if (VoiceClient == null) - break; - await Task.Delay(500); - } - }); + internal Task MoveToVoiceChannel(Channel voiceChannel) { + if (_client?.State != ConnectionState.Connected) + throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); + PlaybackVoiceChannel = voiceChannel; + return PlaybackVoiceChannel.JoinAudio(); } - internal void AddSong(StreamRequest streamRequest) { - lock (_voiceLock) { - Stopped = false; - this.SongQueue.Add(streamRequest); + internal void ClearQueue() { + lock (playlistLock) { + _playlist.Clear(); } } - - public MusicPlayer(Channel voiceChannel, CommandEventArgs e, float? vol) : this() { - if (voiceChannel == null) - throw new ArgumentNullException(nameof(voiceChannel)); - if (vol != null) - Volume = (float)vol; - VoiceChannel = voiceChannel; - _e = e; - } - - public async Task LoadNextSong() { - CurrentSong?.Stop(); - CurrentSong = null; - if (SongQueue.Count != 0) { - lock (_voiceLock) { - CurrentSong = SongQueue[0]; - SongQueue.RemoveAt(0); - } - } - else { - Stop(); - return; - } - - try { - if (VoiceClient == null) { - Console.WriteLine($"Joining voice channel [{DateTime.Now.Second}]"); - //todo add a new event, to tell people nadeko is trying to join - VoiceClient = await Task.Run(async () => await VoiceChannel.JoinAudio()); - Console.WriteLine($"Joined voicechannel [{DateTime.Now.Second}]"); - } - await Task.Factory.StartNew(async () => await CurrentSong?.Start(), TaskCreationOptions.LongRunning).Unwrap(); - } - catch (Exception ex) { - Console.WriteLine($"Starting failed: {ex}"); - CurrentSong?.Stop(); - } - } - - internal void Stop(bool leave = false) { - Stopped = true; - SongQueue.Clear(); - try { - CurrentSong?.Stop(); - } - catch { } - CurrentSong = null; - if (leave) { - VoiceClient?.Disconnect(); - VoiceClient = null; - - MusicPlayer throwAwayValue; - MusicModule.musicPlayers.TryRemove(_e.Server, out throwAwayValue); - } - } - - public int SetVolume(int value) { - if (value < 0) - value = 0; - if (value > 150) - value = 150; - this.Volume = value / 100f; - return value; - } - - internal bool TogglePause() => IsPaused = !IsPaused; - */ } } diff --git a/NadekoBot/Classes/Music/Song.cs b/NadekoBot/Classes/Music/Song.cs new file mode 100644 index 00000000..72e99d91 --- /dev/null +++ b/NadekoBot/Classes/Music/Song.cs @@ -0,0 +1,191 @@ +using Discord.Audio; +using NadekoBot.Extensions; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using VideoLibrary; + +namespace NadekoBot.Classes.Music { + + + public class SongInfo { + public string Provider { get; internal set; } + public string Title { get; internal set; } + public string Uri { get; internal set; } + } + + public class Song { + public StreamState State { get; internal set; } + public object PrettyName => + $"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`"; + public SongInfo SongInfo { get; } + + private Song(SongInfo songInfo) { + this.SongInfo = songInfo; + } + + internal void Play(IAudioClient voiceClient, CancellationToken cancelToken) { + var p = Process.Start(new ProcessStartInfo { + FileName = "ffmpeg", + Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + }); + Task.Delay(2000); //give it 2 seconds to get some dataz + int blockSize = 3840; // 1920 for mono + byte[] buffer = new byte[blockSize]; + int read; + while (!cancelToken.IsCancellationRequested) { + read = p.StandardOutput.BaseStream.Read(buffer, 0, blockSize); + if (read == 0) + break; //nothing to read + voiceClient.Send(buffer, 0, read); + } + voiceClient.Wait(); + } + + public static 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) ?? query; + } + + try { + if (musicType == MusicType.Local) { + return new Song(new SongInfo { + Uri = "\"" + Path.GetFullPath(query) + "\"", + Title = Path.GetFileNameWithoutExtension(query), + Provider = "Local File", + }); + } + else if (musicType == MusicType.Radio) { + return new Song(new SongInfo { + Uri = query, + Title = $"{query}", + Provider = "Radio Stream", + }); + } + else if (SoundCloud.Default.IsSoundCloudLink(query)) { + var svideo = await SoundCloud.Default.GetVideoAsync(query); + return new Song(new SongInfo { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = svideo.StreamLink, + }); + } + else { + var links = await SearchHelper.FindYoutubeUrlByKeywords(query); + if (links == String.Empty) + throw new OperationCanceledException("Not a valid youtube query."); + var allVideos = await Task.Factory.StartNew(async () => await YouTube.Default.GetAllVideosAsync(links)).Unwrap(); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 192) + .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."); + return new Song(new SongInfo { + Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + Provider = "YouTube", + Uri = video.Uri, + }); + + } + } + catch (Exception ex) { + Console.WriteLine($"Failed resolving the link.{ex.Message}"); + return null; + } + } + + private static async Task HandleStreamContainers(string query) { + string file = null; + try { + file = await SearchHelper.GetResponseAsync(query); + } + 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 { + Console.WriteLine($"Failed reading .pls:\n{file}"); + return null; + } + } + else 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 { + Console.WriteLine($"Failed reading .m3u:\n{file}"); + return null; + } + + } + else if (query.Contains(".asx")) { + // + try { + var m = Regex.Match(file, ".*?)\""); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch { + Console.WriteLine($"Failed reading .asx:\n{file}"); + return null; + } + } + else 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 { + Console.WriteLine($"Failed reading .xspf:\n{file}"); + return null; + } + } + + return query; + } + + private static bool IsRadioLink(string query) { + return (query.StartsWith("http") || + query.StartsWith("ww")) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + } + } +} diff --git a/NadekoBot/Classes/Music/StreamRequest.cs b/NadekoBot/Classes/Music/StreamRequest.cs index e1deb17a..3481772e 100644 --- a/NadekoBot/Classes/Music/StreamRequest.cs +++ b/NadekoBot/Classes/Music/StreamRequest.cs @@ -1,4 +1,5 @@ -using System; +/* +using System; using System.Linq; using System.Threading.Tasks; using Discord; @@ -10,13 +11,6 @@ using NadekoBot.Extensions; using VideoLibrary; namespace NadekoBot.Classes.Music { - public enum StreamState { - Resolving, - Queued, - Buffering, //not using it atm - Playing, - Completed - } public class StreamRequest { public Server Server { get; } @@ -54,50 +48,7 @@ namespace NadekoBot.Classes.Music { public async Task Resolve() { string uri = null; - try { - if (this.LinkType == MusicType.Local) { - uri = "\"" + Path.GetFullPath(Query) + "\""; - Title = Path.GetFileNameWithoutExtension(Query); - Provider = "Local File"; - } - else if (this.LinkType == MusicType.Radio) { - uri = Query; - Title = $"{Query}"; - Provider = "Radio Stream"; - } - else if (SoundCloud.Default.IsSoundCloudLink(Query)) { - var svideo = await SoundCloud.Default.GetVideoAsync(Query); - Title = svideo.FullName; - Provider = "SoundCloud"; - uri = svideo.StreamLink; - Console.WriteLine(uri); - } - else { - var links = await SearchHelper.FindYoutubeUrlByKeywords(Query); - if (links == String.Empty) - throw new OperationCanceledException("Not a valid youtube query."); - var allVideos = await Task.Factory.StartNew(async () => await YouTube.Default.GetAllVideosAsync(links)).Unwrap(); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 192) - .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."); - - Title = video.Title.Substring(0, video.Title.Length - 10); // removing trailing "- You Tube" - Provider = "YouTube"; - uri = video.Uri; - } - } - catch (Exception ex) { - privateState = StreamState.Completed; - if (OnResolvingFailed != null) - OnResolvingFailed(ex.Message); - Console.WriteLine($"Failed resolving the link.{ex.Message}"); - return; - } + musicStreamer = new MusicStreamer(this, uri); musicStreamer.OnCompleted += () => { @@ -389,3 +340,4 @@ namespace NadekoBot.Classes.Music { } } } +*/ \ No newline at end of file diff --git a/NadekoBot/Commands/PlayingRotate.cs b/NadekoBot/Commands/PlayingRotate.cs index 0800b731..defcdd00 100644 --- a/NadekoBot/Commands/PlayingRotate.cs +++ b/NadekoBot/Commands/PlayingRotate.cs @@ -20,13 +20,13 @@ namespace NadekoBot.Commands { if(cnt == 1) { try { var mp = Modules.Music.musicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.Title; + return mp.Value.CurrentSong.SongInfo.Title; } catch { } } return cnt.ToString(); } }, - {"%queued%", () => Modules.Music.musicPlayers.Sum(kvp=>kvp.Value.SongQueue.Count).ToString() }, + {"%queued%", () => Modules.Music.musicPlayers.Sum(kvp=>kvp.Value.Playlist.Count).ToString() }, {"%trivia%", () => Commands.Trivia.runningTrivias.Count.ToString() } }; private object playingPlaceholderLock => new object(); diff --git a/NadekoBot/Modules/Administration.cs b/NadekoBot/Modules/Administration.cs index e07f0723..78f984db 100644 --- a/NadekoBot/Modules/Administration.cs +++ b/NadekoBot/Modules/Administration.cs @@ -369,7 +369,7 @@ namespace NadekoBot.Modules { .Description("Shows some basic stats for Nadeko.") .Do(async e => { var t = Task.Run(() => { - return NadekoStats.Instance.GetStats() + "`" + Music.GetMusicStats() + "`"; + return NadekoStats.Instance.GetStats(); //+ "`" + Music.GetMusicStats() + "`"; }); await e.Channel.SendMessage(await t); diff --git a/NadekoBot/Modules/Music.cs b/NadekoBot/Modules/Music.cs index f13d2ffc..266e7fc7 100644 --- a/NadekoBot/Modules/Music.cs +++ b/NadekoBot/Modules/Music.cs @@ -16,13 +16,7 @@ namespace NadekoBot.Modules { class Music : DiscordModule { public static ConcurrentDictionary musicPlayers = new ConcurrentDictionary(); - public static ConcurrentDictionary musicVolumes = new ConcurrentDictionary(); - - internal static string GetMusicStats() { - var stats = musicPlayers.Where(kvp => kvp.Value?.SongQueue.Count > 0 || kvp.Value?.CurrentSong != null); - int cnt; - return $"Playing {cnt = stats.Count()} songs".SnPl(cnt) + $", {stats.Sum(kvp => kvp.Value?.SongQueue?.Count ?? 0)} queued."; - } + public static ConcurrentDictionary defaultMusicVolumes = new ConcurrentDictionary(); Timer setgameTimer => new Timer(); @@ -34,7 +28,7 @@ namespace NadekoBot.Modules { setgameTimer.Elapsed += (s, e) => { try { int num = musicPlayers.Where(kvp => kvp.Value.CurrentSong != null).Count(); - NadekoBot.client.SetGame($"{num} songs".SnPl(num) + $", {musicPlayers.Sum(kvp => kvp.Value.SongQueue.Count())} queued"); + NadekoBot.client.SetGame($"{num} songs".SnPl(num) + $", {musicPlayers.Sum(kvp => kvp.Value.Playlist.Count())} queued"); } catch { } }; @@ -53,28 +47,32 @@ namespace NadekoBot.Modules { cgb.CreateCommand("n") .Alias("next") .Description("Goes to the next song in the queue.") - .Do(async e => { - if (!musicPlayers.ContainsKey(e.Server)) return; - await musicPlayers[e.Server].LoadNextSong(); + .Do(e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) return; + musicPlayer.Next(); }); cgb.CreateCommand("s") .Alias("stop") .Description("Completely stops the music, unbinds the bot from the channel, and cleans up files.") - .Do(e => { - if (!musicPlayers.ContainsKey(e.Server)) return; - musicPlayers[e.Server].Stop(true); + .Do(async e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryRemove(e.Server, out musicPlayer)) return; + await musicPlayer.Stop(); }); cgb.CreateCommand("p") .Alias("pause") .Description("Pauses or Unpauses the song.") .Do(async e => { - if (!musicPlayers.ContainsKey(e.Server)) return; - if (musicPlayers[e.Server].TogglePause()) - await e.Channel.SendMessage("🎵`Music player paused.`"); + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) return; + musicPlayer.TogglePause(); + if (musicPlayer.Paused) + await e.Channel.SendMessage("🎵`Music musicPlayer paused.`"); else - await e.Channel.SendMessage("🎵`Music player unpaused.`"); + await e.Channel.SendMessage("🎵`Music musicPlayer unpaused.`"); }); cgb.CreateCommand("q") @@ -82,49 +80,51 @@ namespace NadekoBot.Modules { .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, e.GetArg("query")); + await QueueSong(e.Channel, e.User.VoiceChannel, e.GetArg("query")); }); cgb.CreateCommand("lq") .Alias("ls").Alias("lp") .Description("Lists up to 15 currently queued songs.") .Do(async e => { - if (musicPlayers.ContainsKey(e.Server) == false) { - await e.Channel.SendMessage("🎵 No active music player."); + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) { + await e.Channel.SendMessage("🎵 No active music musicPlayer."); return; } - var player = musicPlayers[e.Server]; - string toSend = "🎵 **" + player.SongQueue.Count + "** `videos currently queued.` "; - if (player.SongQueue.Count >= 50) + string toSend = "🎵 **" + musicPlayer.Playlist.Count + "** `videos currently queued.` "; + if (musicPlayer.Playlist.Count >= MusicPlayer.MaximumPlaylistSize) toSend += "**Song queue is full!**\n"; int number = 1; - await e.Channel.SendMessage(toSend + string.Join("\n", player.SongQueue.Take(15).Select(v => $"`{number++}.` {v.FullPrettyName}"))); + await e.Channel.SendMessage(toSend + string.Join("\n", musicPlayer.Playlist.Take(15).Select(v => $"`{number++}.` {v.PrettyName}"))); }); cgb.CreateCommand("np") - .Alias("playing") - .Description("Shows the song currently playing.") - .Do(async e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - await e.Channel.SendMessage($"🎵`Now Playing` {player.CurrentSong.FullPrettyName}"); - }); + .Alias("playing") + .Description("Shows the song currently playing.") + .Do(async e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + await e.Channel.SendMessage($"🎵`Now Playing` {musicPlayer.CurrentSong.PrettyName}"); + }); cgb.CreateCommand("vol") - .Description("Sets the music volume 0-150%") - .Parameter("val", ParameterType.Required) - .Do(async e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - var arg = e.GetArg("val"); - int volume; - if (!int.TryParse(arg, out volume)) { - await e.Channel.SendMessage("Volume number invalid."); - return; - } - volume = player.SetVolume(volume); - await e.Channel.SendMessage($"🎵 `Volume set to {volume}%`"); - }); + .Description("Sets the music volume 0-150%") + .Parameter("val", ParameterType.Required) + .Do(async e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + var arg = e.GetArg("val"); + int volume; + if (!int.TryParse(arg, out volume)) { + await e.Channel.SendMessage("Volume number invalid."); + return; + } + volume = musicPlayer.SetVolume(volume); + await e.Channel.SendMessage($"🎵 `Volume set to {volume}%`"); + }); cgb.CreateCommand("dv") .Alias("defvol") @@ -137,52 +137,56 @@ namespace NadekoBot.Modules { await e.Channel.SendMessage("Volume number invalid."); return; } - musicVolumes.AddOrUpdate(e.Server.Id, volume / 100, (key, newval) => volume / 100); + defaultMusicVolumes.AddOrUpdate(e.Server.Id, volume / 100, (key, newval) => volume / 100); await e.Channel.SendMessage($"🎵 `Default volume set to {volume}%`"); }); cgb.CreateCommand("min").Alias("mute") .Description("Sets the music volume to 0%") .Do(e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - player.SetVolume(0); + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + musicPlayer.SetVolume(0); }); cgb.CreateCommand("max") - .Description("Sets the music volume to 100% (real max is actually 150%).") - .Do(e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - player.SetVolume(100); - }); + .Description("Sets the music volume to 100% (real max is actually 150%).") + .Do(e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + musicPlayer.SetVolume(100); + }); cgb.CreateCommand("half") - .Description("Sets the music volume to 50%.") - .Do(e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - player.SetVolume(50); - }); + .Description("Sets the music volume to 50%.") + .Do(e => { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + musicPlayer.SetVolume(50); + }); cgb.CreateCommand("sh") .Description("Shuffles the current playlist.") .Do(async e => { - if (musicPlayers.ContainsKey(e.Server) == false) return; - var player = musicPlayers[e.Server]; - if (player.SongQueue.Count < 2) { - await e.Channel.SendMessage("Not enough songs in order to perform the shuffle."); + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) + return; + if (musicPlayer.Playlist.Count < 2) { + await e.Channel.SendMessage("💢 Not enough songs in order to perform the shuffle."); return; } - player.SongQueue.Shuffle(); + musicPlayer.Shuffle(); await e.Channel.SendMessage("🎵 `Songs shuffled.`"); }); + cgb.CreateCommand("setgame") .Description("Sets the game of the bot to the number of songs playing.**Owner only**") + .AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly()) .Do(async e => { - if (NadekoBot.OwnerID != e.User.Id) - return; setgameEnabled = !setgameEnabled; if (setgameEnabled) setgameTimer.Start(); @@ -204,29 +208,28 @@ namespace NadekoBot.Modules { //todo TEMPORARY SOLUTION, USE RESOLVE QUEUE IN THE FUTURE var msg = await e.Channel.SendMessage($"🎵 `Attempting to queue {ids.Count} songs".SnPl(ids.Count) + "...`"); foreach (var id in ids) { - Task.Run(async () => await QueueSong(e, id, true)); - await Task.Delay(150); + await QueueSong(e.Channel, e.User.VoiceChannel, id, true); } - msg?.Edit("🎵 `Playlist queue complete.`"); + await msg.Edit("🎵 `Playlist queue complete.`"); }); cgb.CreateCommand("lopl") - .Description("Queues up to 50 songs from a directory.") - .Parameter("directory", ParameterType.Unparsed) - .AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly()) - .Do(async e => { - var arg = e.GetArg("directory"); - if (string.IsNullOrWhiteSpace(e.GetArg("directory"))) - return; - try { - var fileEnum = System.IO.Directory.EnumerateFiles(e.GetArg("directory")).Take(50); - foreach (var file in fileEnum) { - await Task.Run(async () => await QueueSong(e, file, true, MusicType.Local)).ConfigureAwait(false); - } - await e.Channel.SendMessage("🎵 `Directory queue complete.`"); - } - catch { } - }); + .Description("Queues up to 50 songs from a directory.") + .Parameter("directory", ParameterType.Unparsed) + .AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly()) + .Do(async e => { + var arg = e.GetArg("directory"); + if (string.IsNullOrWhiteSpace(e.GetArg("directory"))) + return; + try { + var fileEnum = System.IO.Directory.EnumerateFiles(e.GetArg("directory")).Take(50); + foreach (var file in fileEnum) { + await QueueSong(e.Channel, e.User.VoiceChannel, file, true, MusicType.Local); + } + await e.Channel.SendMessage("🎵 `Directory queue complete.`"); + } + catch { } + }); cgb.CreateCommand("radio").Alias("ra") .Description("Queues a direct radio stream from a link.") @@ -236,41 +239,41 @@ namespace NadekoBot.Modules { 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."); return; } - await QueueSong(e, e.GetArg("radio_link"), musicType: MusicType.Radio); + await QueueSong(e.Channel, e.User.VoiceChannel, e.GetArg("radio_link"), musicType: MusicType.Radio); }); cgb.CreateCommand("lo") - .Description("Queues a local file by specifying a full path. BOT OWNER ONLY.") - .Parameter("path", ParameterType.Unparsed) - .AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly()) - .Do(async e => { - var arg = e.GetArg("path"); - if (string.IsNullOrWhiteSpace(arg)) - return; - await QueueSong(e, e.GetArg("path"), musicType: MusicType.Local); - }); + .Description("Queues a local file by specifying a full path. BOT OWNER ONLY.") + .Parameter("path", ParameterType.Unparsed) + .AddCheck(Classes.Permissions.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); + }); cgb.CreateCommand("mv") - .Description("Moves the bot to your voice channel. (works only if music is already playing)") - .Do(async e => { - MusicPlayer mc; - if (e.User.VoiceChannel == null || e.User.VoiceChannel.Server != e.Server || !musicPlayers.TryGetValue(e.Server, out mc)) - return; - mc.VoiceChannel = e.User.VoiceChannel; - mc.VoiceClient = await mc.VoiceChannel.JoinAudio(); - }); + .Description("Moves the bot to your voice channel. (works only if music is already playing)") + .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("rm") .Description("Remove a song by its # in the queue, or 'all' to remove whole queue.") .Parameter("num", ParameterType.Required) .Do(async e => { var arg = e.GetArg("num"); - MusicPlayer mc; - if (!musicPlayers.TryGetValue(e.Server, out mc)) { + MusicPlayer musicPlayer; + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) { return; } if (arg?.ToLower() == "all") { - mc.SongQueue?.Clear(); + musicPlayer.ClearQueue(); await e.Channel.SendMessage($"🎵`Queue cleared!`"); return; } @@ -278,193 +281,60 @@ namespace NadekoBot.Modules { if (!int.TryParse(arg, out num)) { return; } - if (num <= 0 || num > mc.SongQueue.Count) + if (num <= 0 || num > musicPlayer.Playlist.Count) return; - mc.SongQueue.RemoveAt(num - 1); + musicPlayer.RemoveSongAt(num - 1); await e.Channel.SendMessage($"🎵**Track at position `#{num}` has been removed.**"); }); cgb.CreateCommand("debug") .Description("Writes some music data to console. **BOT OWNER ONLY**") + .AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly()) .Do(e => { - if (NadekoBot.OwnerID != e.User.Id) - return; var output = "SERVER_NAME---SERVER_ID-----USERCOUNT----QUEUED\n" + - string.Join("\n", musicPlayers.Select(kvp => kvp.Key.Name + "--" + kvp.Key.Id + " --" + kvp.Key.Users.Count() + "--" + kvp.Value.SongQueue.Count)); + string.Join("\n", musicPlayers.Select(kvp => kvp.Key.Name + "--" + kvp.Key.Id + " --" + kvp.Key.Users.Count() + "--" + kvp.Value.Playlist.Count)); Console.WriteLine(output); }); }); } - private async Task QueueSong(CommandEventArgs e, string query, bool silent = false, MusicType musicType = MusicType.Normal) { - if (e.User.VoiceChannel?.Server != e.Server) { - if (!silent) - 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."); - return; + 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."); + throw new ArgumentNullException(nameof(VoiceCh)); } - if (string.IsNullOrWhiteSpace(query) || query.Length < 3) - return; - - query = query.Trim(); - if (musicType != MusicType.Local && IsRadioLink(query)) { - musicType = MusicType.Radio; - query = await HandleStreamContainers(query) ?? query; - } - - if (musicPlayers.ContainsKey(e.Server) == false) { + throw new ArgumentException("💢 Invalid query for queue song.", nameof(query)); + MusicPlayer musicPlayer = null; + if (!musicPlayers.TryGetValue(TextCh.Server, out musicPlayer)) { float? vol = null; float throwAway; - if (musicVolumes.TryGetValue(e.Server.Id, out throwAway)) + if (defaultMusicVolumes.TryGetValue(TextCh.Server.Id, out throwAway)) vol = throwAway; - - if (!musicPlayers.TryAdd(e.Server, new MusicPlayer(e.User.VoiceChannel, e, vol))) { - await e.Channel.SendMessage("Failed to create a music player for this server."); - return; - } - } - - var player = musicPlayers[e.Server]; - - if (player.SongQueue.Count >= 50) return; - - try { - var sr = new StreamRequest(e, query, player, musicType); - - if (sr == null) - throw new NullReferenceException("StreamRequest is null."); - - Message qmsg = null; - Message msg = null; - if (!silent) { - try { - qmsg = await e.Channel.SendMessage("🎵 `Searching / Resolving...`"); - sr.OnResolvingFailed += async (err) => { - try { - await qmsg.Delete(); - await e.Channel.Send($"💢 🎵 `Resolving failed` for **{query}**"); - } - catch { } - }; - sr.OnQueued += async () => { - try { - await qmsg.Delete(); - await e.Channel.Send($"🎵`Queued`{sr.FullPrettyName}"); - } - catch { } - }; - } - catch { } - } - sr.OnCompleted += async () => { - try { - MusicPlayer mc; - if (musicPlayers.TryGetValue(e.Server, out mc)) { - if (mc.SongQueue.Count == 0) - mc.Stop(); + musicPlayer = new MusicPlayer(VoiceCh) { + OnCompleted = async (song) => { + try { + await TextCh.SendMessage($"🎵`Finished`{song.PrettyName}"); } - await e.Channel.SendMessage($"🎵`Finished`{sr.FullPrettyName}"); - } - catch { } + catch { } + }, + OnStarted = async (song) => { + try { + var msgTxt = $"🎵`Playing`{song.PrettyName} `Vol: {(int)(musicPlayer.Volume * 100)}%`"; + await TextCh.SendMessage(msgTxt); + } + catch { } + }, }; + musicPlayers.TryAdd(TextCh.Server, musicPlayer); + } + var resolvedSong = await Song.ResolveSong(query, musicType); - sr.OnStarted += async () => { - try { - var msgTxt = $"🎵`Playing`{sr.FullPrettyName} `Vol: {(int)(player.Volume * 100)}%`"; - if (qmsg != null) - await qmsg.Delete(); - await e.Channel.SendMessage(msgTxt); - } - catch { } - }; - - await sr.Resolve(); - } - catch (Exception ex) { - await e.Channel.SendMessage($"💢 {ex.Message}"); - return; - } - } - - 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 { - file = await SearchHelper.GetResponseAsync(query); - } - 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 { - Console.WriteLine($"Failed reading .pls:\n{file}"); - return null; - } - } - else 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 { - Console.WriteLine($"Failed reading .m3u:\n{file}"); - return null; - } - - } - else if (query.Contains(".asx")) { - // - try { - var m = Regex.Match(file, ".*?)\""); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch { - Console.WriteLine($"Failed reading .asx:\n{file}"); - return null; - } - } - else 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 { - Console.WriteLine($"Failed reading .xspf:\n{file}"); - return null; - } - } - - return query; + if(!silent) + await TextCh.Send($"🎵`Queued`{resolvedSong.PrettyName}"); + musicPlayer.AddSong(resolvedSong); } } } diff --git a/NadekoBot/NadekoBot.csproj b/NadekoBot/NadekoBot.csproj index 97f7abc2..3adfb15b 100644 --- a/NadekoBot/NadekoBot.csproj +++ b/NadekoBot/NadekoBot.csproj @@ -118,6 +118,7 @@ +