Removed module projects because it can't work like that atm. Commented out package commands.
This commit is contained in:
		| @@ -0,0 +1,9 @@ | ||||
| using System; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.Exceptions | ||||
| { | ||||
|     public class NotInVoiceChannelException : Exception | ||||
|     { | ||||
|         public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| using System; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.Exceptions | ||||
| { | ||||
|     public class QueueFullException : Exception | ||||
|     { | ||||
|         public QueueFullException(string message) : base(message) | ||||
|         { | ||||
|         } | ||||
|         public QueueFullException() : base("Queue is full.") { } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| using System; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.Exceptions | ||||
| { | ||||
|     public class SongNotFoundException : Exception | ||||
|     { | ||||
|         public SongNotFoundException(string message) : base(message) | ||||
|         { | ||||
|         } | ||||
|         public SongNotFoundException() : base("Song is not found.") { } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										670
									
								
								NadekoBot.Core/Modules/Music/Common/MusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										670
									
								
								NadekoBot.Core/Modules/Music/Common/MusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,670 @@ | ||||
| using Discord; | ||||
| using Discord.Audio; | ||||
| using System; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using NLog; | ||||
| using System.Linq; | ||||
| using NadekoBot.Extensions; | ||||
| using System.Diagnostics; | ||||
| using NadekoBot.Common.Collections; | ||||
| using NadekoBot.Modules.Music.Services; | ||||
| using NadekoBot.Core.Services; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common | ||||
| { | ||||
|     public enum StreamState | ||||
|     { | ||||
|         Resolving, | ||||
|         Queued, | ||||
|         Playing, | ||||
|         Completed | ||||
|     } | ||||
|     public class MusicPlayer | ||||
|     { | ||||
|         private readonly Thread _player; | ||||
|         public IVoiceChannel VoiceChannel { get; private set; } | ||||
|         private readonly Logger _log; | ||||
|  | ||||
|         private MusicQueue Queue { get; } = new MusicQueue(); | ||||
|  | ||||
|         public bool Exited { get; set; } = false; | ||||
|         public bool Stopped { get; private set; } = false; | ||||
|         public float Volume { get; private set; } = 1.0f; | ||||
|         public bool Paused => pauseTaskSource != null; | ||||
|         private TaskCompletionSource<bool> pauseTaskSource { get; set; } = null; | ||||
|  | ||||
|         public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; | ||||
|         public string PrettyCurrentTime | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 var time = CurrentTime.ToString(@"mm\:ss"); | ||||
|                 var hrs = (int)CurrentTime.TotalHours; | ||||
|  | ||||
|                 if (hrs > 0) | ||||
|                     return hrs + ":" + time; | ||||
|                 else | ||||
|                     return time; | ||||
|             } | ||||
|         } | ||||
|         public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?"); | ||||
|         private CancellationTokenSource SongCancelSource { get; set; } | ||||
|         public ITextChannel OutputTextChannel { get; set; } | ||||
|         public (int Index, SongInfo Current) Current | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (Stopped) | ||||
|                     return (0, null); | ||||
|                 return Queue.Current; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public bool RepeatCurrentSong { get; private set; } | ||||
|         public bool Shuffle { get; private set; } | ||||
|         public bool Autoplay { get; private set; } | ||||
|         public bool RepeatPlaylist { get; private set; } = false; | ||||
|         public uint MaxQueueSize | ||||
|         { | ||||
|             get => Queue.MaxQueueSize; | ||||
|             set { lock (locker) Queue.MaxQueueSize = value; } | ||||
|         } | ||||
|         private bool _fairPlay; | ||||
|         public bool FairPlay | ||||
|         { | ||||
|             get => _fairPlay; | ||||
|             set | ||||
|             { | ||||
|                 if (value) | ||||
|                 { | ||||
|                     var cur = Queue.Current; | ||||
|                     if (cur.Song != null) | ||||
|                         RecentlyPlayedUsers.Add(cur.Song.QueuerName); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     RecentlyPlayedUsers.Clear(); | ||||
|                 } | ||||
|  | ||||
|                 _fairPlay = value; | ||||
|             } | ||||
|         } | ||||
|         public bool AutoDelete { get; set; } | ||||
|         public uint MaxPlaytimeSeconds { get; set; } | ||||
|  | ||||
|  | ||||
|         const int _frameBytes = 3840; | ||||
|         const float _miliseconds = 20.0f; | ||||
|         public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); | ||||
|  | ||||
|         private int _bytesSent = 0; | ||||
|  | ||||
|         private IAudioClient _audioClient; | ||||
|         private readonly object locker = new object(); | ||||
|         private MusicService _musicService; | ||||
|  | ||||
|         #region events | ||||
|         public event Action<MusicPlayer, (int Index, SongInfo Song)> OnStarted; | ||||
|         public event Action<MusicPlayer, SongInfo> OnCompleted; | ||||
|         public event Action<MusicPlayer, bool> OnPauseChanged; | ||||
|         #endregion | ||||
|  | ||||
|  | ||||
|         private bool manualSkip = false; | ||||
|         private bool manualIndex = false; | ||||
|         private bool newVoiceChannel = false; | ||||
|         private readonly IGoogleApiService _google; | ||||
|  | ||||
|         private bool cancel = false; | ||||
|  | ||||
|         private ConcurrentHashSet<string> RecentlyPlayedUsers { get; } = new ConcurrentHashSet<string>(); | ||||
|         public TimeSpan TotalPlaytime | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 var songs = Queue.ToArray().Songs; | ||||
|                 return songs.Any(s => s.TotalTime == TimeSpan.MaxValue) | ||||
|                     ? TimeSpan.MaxValue | ||||
|                     : new TimeSpan(songs.Sum(s => s.TotalTime.Ticks)); | ||||
|             } | ||||
|         } | ||||
|              | ||||
|  | ||||
|         public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume) | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|             this.Volume = volume; | ||||
|             this.VoiceChannel = vch; | ||||
|             this.SongCancelSource = new CancellationTokenSource(); | ||||
|             this.OutputTextChannel = output; | ||||
|             this._musicService = musicService; | ||||
|             this._google = google; | ||||
|  | ||||
|             _log.Info("Initialized"); | ||||
|  | ||||
|             _player = new Thread(new ThreadStart(PlayerLoop)); | ||||
|             _player.Start(); | ||||
|             _log.Info("Loop started"); | ||||
|         } | ||||
|  | ||||
|         private async void PlayerLoop() | ||||
|         { | ||||
|             while (!Exited) | ||||
|             { | ||||
|                 _bytesSent = 0; | ||||
|                 cancel = false; | ||||
|                 CancellationToken cancelToken; | ||||
|                 (int Index, SongInfo Song) data; | ||||
|                 lock (locker) | ||||
|                 { | ||||
|                     data = Queue.Current; | ||||
|                     cancelToken = SongCancelSource.Token; | ||||
|                     manualSkip = false; | ||||
|                     manualIndex = false; | ||||
|                 } | ||||
|                 if (data.Song != null) | ||||
|                 { | ||||
|                     _log.Info("Starting"); | ||||
|                     AudioOutStream pcm = null; | ||||
|                     SongBuffer b = null; | ||||
|                     try | ||||
|                     { | ||||
|                         b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local); | ||||
|                         //_log.Info("Created buffer, buffering..."); | ||||
|  | ||||
|                         //var bufferTask = b.StartBuffering(cancelToken); | ||||
|                         //var timeout = Task.Delay(10000); | ||||
|                         //if (Task.WhenAny(bufferTask, timeout) == timeout) | ||||
|                         //{ | ||||
|                         //    _log.Info("Buffering failed due to a timeout."); | ||||
|                         //    continue; | ||||
|                         //} | ||||
|                         //else if (!bufferTask.Result) | ||||
|                         //{ | ||||
|                         //    _log.Info("Buffering failed due to a cancel or error."); | ||||
|                         //    continue; | ||||
|                         //} | ||||
|                         //_log.Info("Buffered. Getting audio client..."); | ||||
|                         var ac = await GetAudioClient(); | ||||
|                         _log.Info("Got Audio client"); | ||||
|                         if (ac == null) | ||||
|                         { | ||||
|                             _log.Info("Can't join"); | ||||
|                             await Task.Delay(900, cancelToken); | ||||
|                             // just wait some time, maybe bot doesn't even have perms to join that voice channel,  | ||||
|                             // i don't want to spam connection attempts | ||||
|                             continue; | ||||
|                         } | ||||
|                         pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); | ||||
|                         _log.Info("Created pcm stream"); | ||||
|                         OnStarted?.Invoke(this, data); | ||||
|  | ||||
|                         byte[] buffer = new byte[3840]; | ||||
|                         int bytesRead = 0; | ||||
|  | ||||
|                         while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 | ||||
|                         && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) | ||||
|                         { | ||||
|                             AdjustVolume(buffer, Volume); | ||||
|                             await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); | ||||
|                             unchecked { _bytesSent += bytesRead; } | ||||
|  | ||||
|                             await (pauseTaskSource?.Task ?? Task.CompletedTask); | ||||
|                         } | ||||
|                     } | ||||
|                     catch (OperationCanceledException) | ||||
|                     { | ||||
|                         _log.Info("Song Canceled"); | ||||
|                         cancel = true; | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         _log.Warn(ex); | ||||
|                     } | ||||
|                     finally | ||||
|                     { | ||||
|                         if (pcm != null) | ||||
|                         { | ||||
|                             // flush is known to get stuck from time to time,  | ||||
|                             // just skip flushing 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(); | ||||
|                             pcm.Dispose(); | ||||
|                         } | ||||
|  | ||||
|                         if (b != null) | ||||
|                             b.Dispose(); | ||||
|  | ||||
|                         OnCompleted?.Invoke(this, data.Song); | ||||
|  | ||||
|                         if (_bytesSent == 0 && !cancel) | ||||
|                         { | ||||
|                             lock (locker) | ||||
|                                 Queue.RemoveSong(data.Song); | ||||
|                             _log.Info("Song removed because it can't play"); | ||||
|                         } | ||||
|                     } | ||||
|                     try | ||||
|                     { | ||||
|                         //if repeating current song, just ignore other settings,  | ||||
|                         // and play this song again (don't change the index) | ||||
|                         // ignore rcs if song is manually skipped | ||||
|  | ||||
|                         int queueCount; | ||||
|                         bool stopped; | ||||
|                         int currentIndex; | ||||
|                         lock (locker) | ||||
|                         { | ||||
|                             queueCount = Queue.Count; | ||||
|                             stopped = Stopped; | ||||
|                             currentIndex = Queue.CurrentIndex; | ||||
|                         } | ||||
|  | ||||
|                         if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null) | ||||
|                         { | ||||
|                             Queue.RemoveSong(data.Song); | ||||
|                         } | ||||
|  | ||||
|                         if (!manualIndex && (!RepeatCurrentSong || manualSkip)) | ||||
|                         { | ||||
|                             if (Shuffle) | ||||
|                             { | ||||
|                                 _log.Info("Random song"); | ||||
|                                 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 (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube) | ||||
|                                 { | ||||
|                                     try | ||||
|                                     { | ||||
|                                         _log.Info("Loading related song"); | ||||
|                                         await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel); | ||||
|                                         if(!AutoDelete) | ||||
|                                             Queue.Next(); | ||||
|                                     } | ||||
|                                     catch | ||||
|                                     { | ||||
|                                         _log.Info("Loading related song failed."); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 else if (FairPlay) | ||||
|                                 { | ||||
|                                     lock (locker) | ||||
|                                     { | ||||
|                                         _log.Info("Next fair song"); | ||||
|                                         var q = Queue.ToArray().Songs.Shuffle().ToArray(); | ||||
|  | ||||
|                                         bool found = false; | ||||
|                                         for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently | ||||
|                                         { | ||||
|                                             var item = q[i]; | ||||
|                                             if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index | ||||
|                                             { | ||||
|                                                 Queue.CurrentIndex = i; | ||||
|                                                 found = true; | ||||
|                                                 break; | ||||
|                                             } | ||||
|                                         } | ||||
|                                         if (!found) //if it's not | ||||
|                                         { | ||||
|                                             RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) | ||||
|                                             Queue.Random(); //go to a random song (to prevent looping on the first few songs) | ||||
|                                             var cur = Current; | ||||
|                                             if (cur.Current != null) // add newely scheduled song's queuer to the recently played list | ||||
|                                                 RecentlyPlayedUsers.Add(cur.Current.QueuerName); | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                                 else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) | ||||
|                                 { | ||||
|                                     _log.Info("Stopping because repeatplaylist is disabled"); | ||||
|                                     lock (locker) | ||||
|                                     { | ||||
|                                         Stop(); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 else | ||||
|                                 { | ||||
|                                     _log.Info("Next song"); | ||||
|                                     lock (locker) | ||||
|                                     { | ||||
|                                         if (!Stopped) | ||||
|                                             if(!AutoDelete) | ||||
|                                                 Queue.Next(); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         _log.Error(ex); | ||||
|                     } | ||||
|                 } | ||||
|                 do | ||||
|                 { | ||||
|                     await Task.Delay(500); | ||||
|                 } | ||||
|                 while ((Queue.Count == 0 || Stopped) && !Exited); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private async Task<IAudioClient> GetAudioClient(bool reconnect = false) | ||||
|         { | ||||
|             if (_audioClient == null || | ||||
|                 _audioClient.ConnectionState != ConnectionState.Connected || | ||||
|                 reconnect || | ||||
|                 newVoiceChannel) | ||||
|                 try | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         var t = _audioClient?.StopAsync(); | ||||
|                         if (t != null) | ||||
|                         { | ||||
|  | ||||
|                             _log.Info("Stopping audio client"); | ||||
|                             await t; | ||||
|  | ||||
|                             _log.Info("Disposing audio client"); | ||||
|                             _audioClient.Dispose(); | ||||
|                         } | ||||
|                     } | ||||
|                     catch | ||||
|                     { | ||||
|                     } | ||||
|                     newVoiceChannel = false; | ||||
|  | ||||
|                     _log.Info("Get current user"); | ||||
|                     var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); | ||||
|                     if (curUser.VoiceChannel != null) | ||||
|                     { | ||||
|                         _log.Info("Connecting"); | ||||
|                         var ac = await VoiceChannel.ConnectAsync(); | ||||
|                         _log.Info("Connected, stopping"); | ||||
|                         await ac.StopAsync(); | ||||
|                         _log.Info("Disconnected"); | ||||
|                         await Task.Delay(1000); | ||||
|                     } | ||||
|                     _log.Info("Connecting"); | ||||
|                     _audioClient = await VoiceChannel.ConnectAsync(); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|             return _audioClient; | ||||
|         } | ||||
|  | ||||
|         public int Enqueue(SongInfo song) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (Exited) | ||||
|                     return -1; | ||||
|                 Queue.Add(song); | ||||
|                 return Queue.Count - 1; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public int EnqueueNext(SongInfo song) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (Exited) | ||||
|                     return -1; | ||||
|                 return Queue.AddNext(song); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void SetIndex(int index) | ||||
|         { | ||||
|             if (index < 0) | ||||
|                 throw new ArgumentOutOfRangeException(nameof(index)); | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (Exited) | ||||
|                     return; | ||||
|                 if (AutoDelete && index >= Queue.CurrentIndex && index > 0) | ||||
|                     index--; | ||||
|                 Queue.CurrentIndex = index; | ||||
|                 manualIndex = true; | ||||
|                 Stopped = false; | ||||
|                 CancelCurrentSong(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Next(int skipCount = 1) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (Exited) | ||||
|                     return; | ||||
|                 manualSkip = true; | ||||
|                 // if player is stopped, and user uses .n, it should play current song.   | ||||
|                 // It's a bit weird, but that's the least annoying solution | ||||
|                 if (!Stopped) | ||||
|                     if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled | ||||
|                     { //stop the queue | ||||
|                         Stop(); | ||||
|                         return; | ||||
|                     } | ||||
|                     else | ||||
|                         Queue.Next(skipCount - 1); | ||||
|                 else | ||||
|                     Queue.CurrentIndex = 0; | ||||
|                 Stopped = false; | ||||
|                 CancelCurrentSong(); | ||||
|                 Unpause(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Stop(bool clearQueue = false) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Stopped = true; | ||||
|                 //Queue.ResetCurrent(); | ||||
|                 if (clearQueue) | ||||
|                     Queue.Clear(); | ||||
|                 Unpause(); | ||||
|                 CancelCurrentSong(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void Unpause() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (pauseTaskSource != null) | ||||
|                 { | ||||
|                     pauseTaskSource.TrySetResult(true); | ||||
|                     pauseTaskSource = null; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void TogglePause() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (pauseTaskSource == null) | ||||
|                     pauseTaskSource = new TaskCompletionSource<bool>(); | ||||
|                 else | ||||
|                 { | ||||
|                     Unpause(); | ||||
|                 } | ||||
|             } | ||||
|             OnPauseChanged?.Invoke(this, pauseTaskSource != null); | ||||
|         } | ||||
|  | ||||
|         public void SetVolume(int volume) | ||||
|         { | ||||
|             if (volume < 0 || volume > 100) | ||||
|                 throw new ArgumentOutOfRangeException(nameof(volume)); | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Volume = ((float)volume) / 100; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public SongInfo RemoveAt(int index) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 var cur = Queue.Current; | ||||
|                 var toReturn = Queue.RemoveAt(index); | ||||
|                 if (cur.Index == index) | ||||
|                     Next(); | ||||
|                 return toReturn; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void CancelCurrentSong() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 var cs = SongCancelSource; | ||||
|                 SongCancelSource = new CancellationTokenSource(); | ||||
|                 cs.Cancel(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void ClearQueue() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Queue.Clear(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public (int CurrentIndex, SongInfo[] Songs) QueueArray() | ||||
|         { | ||||
|             lock (locker) | ||||
|                 return Queue.ToArray(); | ||||
|         } | ||||
|  | ||||
|         //aidiakapi ftw | ||||
|         public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) | ||||
|         { | ||||
|             if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; | ||||
|  | ||||
|             // 16-bit precision for the multiplication | ||||
|             var volumeFixed = (int)Math.Round(volume * 65536d); | ||||
|  | ||||
|             var count = audioSamples.Length / 2; | ||||
|  | ||||
|             fixed (byte* srcBytes = audioSamples) | ||||
|             { | ||||
|                 var src = (short*)srcBytes; | ||||
|  | ||||
|                 for (var i = count; i != 0; i--, src++) | ||||
|                     *src = (short)(((*src) * volumeFixed) >> 16); | ||||
|             } | ||||
|  | ||||
|             return audioSamples; | ||||
|         } | ||||
|  | ||||
|         public bool ToggleRepeatSong() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 return RepeatCurrentSong = !RepeatCurrentSong; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task Destroy() | ||||
|         { | ||||
|             _log.Info("Destroying"); | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Stop(); | ||||
|                 Exited = true; | ||||
|                 Unpause(); | ||||
|  | ||||
|                 OnCompleted = null; | ||||
|                 OnPauseChanged = null; | ||||
|                 OnStarted = null; | ||||
|             } | ||||
|             var ac = _audioClient; | ||||
|             if (ac != null) | ||||
|                 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 async Task SetVoiceChannel(IVoiceChannel vch) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (Exited) | ||||
|                     return; | ||||
|                 VoiceChannel = vch; | ||||
|             } | ||||
|             _audioClient = await vch.ConnectAsync(); | ||||
|         } | ||||
|  | ||||
|         public async Task UpdateSongDurationsAsync() | ||||
|         { | ||||
|             var sw = Stopwatch.StartNew(); | ||||
|             var (_, songs) = Queue.ToArray(); | ||||
|             var toUpdate = songs | ||||
|                 .Where(x => x.ProviderType == MusicType.YouTube | ||||
|                     && x.TotalTime == TimeSpan.Zero); | ||||
|  | ||||
|             var vIds = toUpdate.Select(x => x.VideoId); | ||||
|  | ||||
|             sw.Stop(); | ||||
|             _log.Info(sw.Elapsed.TotalSeconds); | ||||
|             if (!vIds.Any()) | ||||
|                 return; | ||||
|  | ||||
|             var durations = await _google.GetVideoDurationsAsync(vIds); | ||||
|  | ||||
|             foreach (var x in toUpdate) | ||||
|             { | ||||
|                 if (durations.TryGetValue(x.VideoId, out var dur)) | ||||
|                     x.TotalTime = dur; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public SongInfo MoveSong(int n1, int n2) | ||||
|             => Queue.MoveSong(n1, n2); | ||||
|  | ||||
|         //// this should be written better | ||||
|         //public TimeSpan TotalPlaytime =>  | ||||
|         //    _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ?  | ||||
|         //    TimeSpan.MaxValue :  | ||||
|         //    new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks));         | ||||
|     } | ||||
| } | ||||
							
								
								
									
										215
									
								
								NadekoBot.Core/Modules/Music/Common/MusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								NadekoBot.Core/Modules/Music/Common/MusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Modules.Music.Common.Exceptions; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using NadekoBot.Common; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common | ||||
| { | ||||
|     public class MusicQueue : IDisposable | ||||
|     { | ||||
|         private LinkedList<SongInfo> Songs { get; set; } = new LinkedList<SongInfo>(); | ||||
|         private int _currentIndex = 0; | ||||
|         public int CurrentIndex | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 return _currentIndex; | ||||
|             } | ||||
|             set | ||||
|             { | ||||
|                 lock (locker) | ||||
|                 { | ||||
|                     if (Songs.Count == 0) | ||||
|                         _currentIndex = 0; | ||||
|                     else | ||||
|                         _currentIndex = value %= Songs.Count; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         public (int Index, SongInfo Song) Current | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 var cur = CurrentIndex; | ||||
|                 return (cur, Songs.ElementAtOrDefault(cur)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private readonly object locker = new object(); | ||||
|         private TaskCompletionSource<bool> nextSource { get; } = new TaskCompletionSource<bool>(); | ||||
|         public int Count | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 lock (locker) | ||||
|                 { | ||||
|                     return Songs.Count; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private uint _maxQueueSize; | ||||
|         public uint MaxQueueSize | ||||
|         { | ||||
|             get => _maxQueueSize; | ||||
|             set | ||||
|             { | ||||
|                 if (value < 0) | ||||
|                     throw new ArgumentOutOfRangeException(nameof(value)); | ||||
|  | ||||
|                 lock (locker) | ||||
|                 { | ||||
|                     _maxQueueSize = value; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Add(SongInfo song) | ||||
|         { | ||||
|             song.ThrowIfNull(nameof(song)); | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize) | ||||
|                     throw new QueueFullException(); | ||||
|                 Songs.AddLast(song); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public int AddNext(SongInfo song) | ||||
|         { | ||||
|             song.ThrowIfNull(nameof(song)); | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (MaxQueueSize != 0 && Songs.Count >= MaxQueueSize) | ||||
|                     throw new QueueFullException(); | ||||
|                 var curSong = Current.Song; | ||||
|                 if (curSong == null) | ||||
|                 { | ||||
|                     Songs.AddLast(song); | ||||
|                     return Songs.Count; | ||||
|                 } | ||||
|  | ||||
|                 var songlist = Songs.ToList(); | ||||
|                 songlist.Insert(CurrentIndex + 1, song); | ||||
|                 Songs = new LinkedList<SongInfo>(songlist); | ||||
|                 return CurrentIndex + 1; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Next(int skipCount = 1) | ||||
|         { | ||||
|             lock(locker) | ||||
|                 CurrentIndex += skipCount; | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Clear(); | ||||
|         } | ||||
|  | ||||
|         public SongInfo RemoveAt(int index) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 if (index < 0 || index >= Songs.Count) | ||||
|                     throw new ArgumentOutOfRangeException(nameof(index)); | ||||
|  | ||||
|                 var current = Songs.First.Value; | ||||
|                 for (int i = 0; i < Songs.Count; i++) | ||||
|                 { | ||||
|                     if (i == index) | ||||
|                     { | ||||
|                         current = Songs.ElementAt(index); | ||||
|                         Songs.Remove(current); | ||||
|                         if (CurrentIndex != 0) | ||||
|                         { | ||||
|                             if (CurrentIndex >= index) | ||||
|                             { | ||||
|                                 --CurrentIndex; | ||||
|                             } | ||||
|                         } | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 return current; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Clear() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Songs.Clear(); | ||||
|                 CurrentIndex = 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public (int CurrentIndex, SongInfo[] Songs) ToArray() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 return (CurrentIndex, Songs.ToArray()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void ResetCurrent() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 CurrentIndex = 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Random() | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 CurrentIndex = new NadekoRandom().Next(Songs.Count); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public SongInfo MoveSong(int n1, int n2) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 var currentSong = Current.Song; | ||||
|                 var playlist = Songs.ToList(); | ||||
|                 if (n1 >= playlist.Count || n2 >= playlist.Count || n1 == n2) | ||||
|                     return null; | ||||
|  | ||||
|                 var s = playlist[n1]; | ||||
|  | ||||
|                 playlist.RemoveAt(n1); | ||||
|                 playlist.Insert(n2, s); | ||||
|  | ||||
|                 Songs = new LinkedList<SongInfo>(playlist); | ||||
|  | ||||
|  | ||||
|                 if (currentSong != null) | ||||
|                     CurrentIndex = playlist.IndexOf(currentSong); | ||||
|  | ||||
|                 return s; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void RemoveSong(SongInfo song) | ||||
|         { | ||||
|             lock (locker) | ||||
|             { | ||||
|                 Songs.Remove(song); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public bool IsLast() | ||||
|         { | ||||
|             lock (locker) | ||||
|                 return CurrentIndex == Songs.Count - 1; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| //O O [O] O O O O | ||||
| // | ||||
| // 3 | ||||
							
								
								
									
										95
									
								
								NadekoBot.Core/Modules/Music/Common/SongBuffer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								NadekoBot.Core/Modules/Music/Common/SongBuffer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| using NLog; | ||||
| using System; | ||||
| using System.Diagnostics; | ||||
| using System.IO; | ||||
| using System.Threading; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common | ||||
| { | ||||
|     public class SongBuffer : IDisposable | ||||
|     { | ||||
|         const int readSize = 81920; | ||||
|         private Process p; | ||||
|         private Stream _outStream; | ||||
|  | ||||
|         private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); | ||||
|         private readonly Logger _log; | ||||
|  | ||||
|         public string SongUri { get; private set; } | ||||
|  | ||||
|         public SongBuffer(string songUri, string skipTo, bool isLocal) | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|             this.SongUri = songUri; | ||||
|             this._isLocal = isLocal; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 this.p = StartFFmpegProcess(SongUri, 0); | ||||
|                 this._outStream = this.p.StandardOutput.BaseStream; | ||||
|             } | ||||
|             catch (System.ComponentModel.Win32Exception) | ||||
|             { | ||||
|                 _log.Error(@"You have not properly installed or configured FFMPEG.  | ||||
| Please install and configure FFMPEG to play music.  | ||||
| Check the guides for your platform on how to setup ffmpeg correctly: | ||||
|     Windows Guide: https://goo.gl/OjKk8F | ||||
|     Linux Guide:  https://goo.gl/ShjCUo"); | ||||
|             } | ||||
|             catch (OperationCanceledException) { } | ||||
|             catch (InvalidOperationException) { } // when ffmpeg is disposed | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _log.Info(ex); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private Process StartFFmpegProcess(string songUri, float skipTo = 0) | ||||
|         { | ||||
|             var args = $"-err_detect ignore_err -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error"; | ||||
|             if (!_isLocal) | ||||
|                 args = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 " + args; | ||||
|  | ||||
|             return Process.Start(new ProcessStartInfo | ||||
|             { | ||||
|                 FileName = "ffmpeg", | ||||
|                 Arguments = args, | ||||
|                 UseShellExecute = false, | ||||
|                 RedirectStandardOutput = true, | ||||
|                 RedirectStandardError = false, | ||||
|                 CreateNoWindow = true, | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         private readonly object locker = new object(); | ||||
|         private readonly bool _isLocal; | ||||
|  | ||||
|         public int Read(byte[] b, int offset, int toRead) | ||||
|         { | ||||
|             lock (locker) | ||||
|                 return _outStream.Read(b, offset, toRead); | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 this.p.StandardOutput.Dispose(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _log.Error(ex); | ||||
|             } | ||||
|             try | ||||
|             { | ||||
|                 if(!this.p.HasExited) | ||||
|                     this.p.Kill(); | ||||
|             } | ||||
|             catch | ||||
|             { | ||||
|             } | ||||
|             _outStream.Dispose(); | ||||
|             this.p.Dispose(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								NadekoBot.Core/Modules/Music/Common/SongHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								NadekoBot.Core/Modules/Music/Common/SongHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| using NLog; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common | ||||
| { | ||||
|     public static class SongHandler | ||||
|     { | ||||
|         private static readonly Logger _log = LogManager.GetCurrentClassLogger(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										79
									
								
								NadekoBot.Core/Modules/Music/Common/SongInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								NadekoBot.Core/Modules/Music/Common/SongInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using Discord; | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using System; | ||||
| using System.Net; | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common | ||||
| { | ||||
|     public class SongInfo | ||||
|     { | ||||
|         public string Provider { get; set; } | ||||
|         public MusicType ProviderType { get; set; } | ||||
|         public string Query { get; set; } | ||||
|         public string Title { get; set; } | ||||
|         public Func<Task<string>> Uri { get; set; } | ||||
|         public string Thumbnail { get; set; } | ||||
|         public string QueuerName { get; set; } | ||||
|         public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; | ||||
|  | ||||
|         public string PrettyProvider => (Provider ?? "???"); | ||||
|         //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; | ||||
|         public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**"; | ||||
|         public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; | ||||
|         public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`"; | ||||
|         public string PrettyTotalTime | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (TotalTime == TimeSpan.Zero) | ||||
|                     return "(?)"; | ||||
|                 if (TotalTime == TimeSpan.MaxValue) | ||||
|                     return "∞"; | ||||
|                 var time = TotalTime.ToString(@"mm\:ss"); | ||||
|                 var hrs = (int)TotalTime.TotalHours; | ||||
|  | ||||
|                 if (hrs > 0) | ||||
|                     return hrs + ":" + time; | ||||
|                 return time; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public string SongUrl | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 switch (ProviderType) | ||||
|                 { | ||||
|                     case MusicType.YouTube: | ||||
|                         return Query; | ||||
|                     case MusicType.Soundcloud: | ||||
|                         return Query; | ||||
|                     case MusicType.Local: | ||||
|                         return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }"; | ||||
|                     case MusicType.Radio: | ||||
|                         return $"https://google.com/search?q={Title}"; | ||||
|                     default: | ||||
|                         return ""; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         private string _videoId = null; | ||||
|         public string VideoId | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (ProviderType == MusicType.YouTube) | ||||
|                     return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString(); | ||||
|  | ||||
|                 return _videoId ?? ""; | ||||
|             } | ||||
|  | ||||
|             set => _videoId = value; | ||||
|         } | ||||
|  | ||||
|         private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| using NadekoBot.Modules.Music.Common.SongResolver.Strategies; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver | ||||
| { | ||||
|     public interface ISongResolverFactory | ||||
|     { | ||||
|         Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| using System.Threading.Tasks; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using NadekoBot.Modules.Music.Common.SongResolver.Strategies; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver | ||||
| { | ||||
|     public class SongResolverFactory : ISongResolverFactory | ||||
|     { | ||||
|         private readonly SoundCloudApiService _sc; | ||||
|  | ||||
|         public SongResolverFactory(SoundCloudApiService sc) | ||||
|         { | ||||
|             _sc = sc; | ||||
|         } | ||||
|  | ||||
|         public async Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType) | ||||
|         { | ||||
|             await Task.Yield(); //for async warning | ||||
|             switch (musicType) | ||||
|             { | ||||
|                 case MusicType.YouTube: | ||||
|                     return new YoutubeResolveStrategy(); | ||||
|                 case MusicType.Radio: | ||||
|                     return new RadioResolveStrategy(); | ||||
|                 case MusicType.Local: | ||||
|                     return new LocalSongResolveStrategy(); | ||||
|                 case MusicType.Soundcloud: | ||||
|                     return new SoundcloudResolveStrategy(_sc); | ||||
|                 default: | ||||
|                     if (_sc.IsSoundCloudLink(query)) | ||||
|                         return new SoundcloudResolveStrategy(_sc); | ||||
|                     else if (RadioResolveStrategy.IsRadioLink(query)) | ||||
|                         return new RadioResolveStrategy(); | ||||
|                     // maybe add a check for local files in the future | ||||
|                     else | ||||
|                         return new YoutubeResolveStrategy(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies | ||||
| { | ||||
|     public interface IResolveStrategy | ||||
|     { | ||||
|         Task<SongInfo> ResolveSong(string query); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using System.IO; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies | ||||
| { | ||||
|     public class LocalSongResolveStrategy : IResolveStrategy | ||||
|     { | ||||
|         public Task<SongInfo> ResolveSong(string query) | ||||
|         { | ||||
|             return Task.FromResult(new SongInfo | ||||
|             { | ||||
|                 Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""), | ||||
|                 Title = Path.GetFileNameWithoutExtension(query), | ||||
|                 Provider = "Local File", | ||||
|                 ProviderType = MusicType.Local, | ||||
|                 Query = query, | ||||
|                 Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png", | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,138 @@ | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using NLog; | ||||
| using System; | ||||
| using System.Net.Http; | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies | ||||
| { | ||||
|     public class RadioResolveStrategy : IResolveStrategy | ||||
|     { | ||||
|         private readonly Regex plsRegex = new Regex("File1=(?<url>.*?)\\n", RegexOptions.Compiled); | ||||
|         private readonly Regex m3uRegex = new Regex("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); | ||||
|         private readonly Regex asxRegex = new Regex("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled); | ||||
|         private readonly Regex xspfRegex = new Regex("<location>(?<url>.*?)</location>", RegexOptions.Compiled); | ||||
|         private readonly Logger _log; | ||||
|  | ||||
|         public RadioResolveStrategy() | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|         } | ||||
|  | ||||
|         public async Task<SongInfo> ResolveSong(string query) | ||||
|         { | ||||
|             if (IsRadioLink(query)) | ||||
|                 query = await HandleStreamContainers(query); | ||||
|  | ||||
|             return new SongInfo | ||||
|             { | ||||
|                 Uri = () => Task.FromResult(query), | ||||
|                 Title = query, | ||||
|                 Provider = "Radio Stream", | ||||
|                 ProviderType = MusicType.Radio, | ||||
|                 Query = query, | ||||
|                 TotalTime = TimeSpan.MaxValue, | ||||
|                 Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png", | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public static 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<string> HandleStreamContainers(string query) | ||||
|         { | ||||
|             string file = null; | ||||
|             try | ||||
|             { | ||||
|                 using (var http = new HttpClient()) | ||||
|                 { | ||||
|                     file = await http.GetStringAsync(query).ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|             catch | ||||
|             { | ||||
|                 return query; | ||||
|             } | ||||
|             if (query.Contains(".pls")) | ||||
|             { | ||||
|                 //File1=http://armitunes.com:8000/ | ||||
|                 //Regex.Match(query) | ||||
|                 try | ||||
|                 { | ||||
|                     var m = plsRegex.Match(file); | ||||
|                     var res = m.Groups["url"]?.ToString(); | ||||
|                     return res?.Trim(); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     _log.Warn($"Failed reading .pls:\n{file}"); | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|             if (query.Contains(".m3u")) | ||||
|             { | ||||
|                 /*  | ||||
| # This is a comment | ||||
|                    C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 | ||||
|                    C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 | ||||
|                 */ | ||||
|                 try | ||||
|                 { | ||||
|                     var m = m3uRegex.Match(file); | ||||
|                     var res = m.Groups["url"]?.ToString(); | ||||
|                     return res?.Trim(); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     _log.Warn($"Failed reading .m3u:\n{file}"); | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|             if (query.Contains(".asx")) | ||||
|             { | ||||
|                 //<ref href="http://armitunes.com:8000"/> | ||||
|                 try | ||||
|                 { | ||||
|                     var m = asxRegex.Match(file); | ||||
|                     var res = m.Groups["url"]?.ToString(); | ||||
|                     return res?.Trim(); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     _log.Warn($"Failed reading .asx:\n{file}"); | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|             if (query.Contains(".xspf")) | ||||
|             { | ||||
|                 /* | ||||
|                 <?xml version="1.0" encoding="UTF-8"?> | ||||
|                     <playlist version="1" xmlns="http://xspf.org/ns/0/"> | ||||
|                         <trackList> | ||||
|                             <track><location>file:///mp3s/song_1.mp3</location></track> | ||||
|                 */ | ||||
|                 try | ||||
|                 { | ||||
|                     var m = xspfRegex.Match(file); | ||||
|                     var res = m.Groups["url"]?.ToString(); | ||||
|                     return res?.Trim(); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     _log.Warn($"Failed reading .xspf:\n{file}"); | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return query; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| using NadekoBot.Modules.Music.Extensions; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies | ||||
| { | ||||
|     public class SoundcloudResolveStrategy : IResolveStrategy | ||||
|     { | ||||
|         private readonly SoundCloudApiService _sc; | ||||
|  | ||||
|         public SoundcloudResolveStrategy(SoundCloudApiService sc) | ||||
|         { | ||||
|             _sc = sc; | ||||
|         } | ||||
|  | ||||
|         public async Task<SongInfo> ResolveSong(string query) | ||||
|         { | ||||
|             var svideo = !_sc.IsSoundCloudLink(query) ? | ||||
|                 await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) : | ||||
|                 await _sc.ResolveVideoAsync(query).ConfigureAwait(false); | ||||
|  | ||||
|             if (svideo == null) | ||||
|                 return null; | ||||
|             return await svideo.GetSongInfo(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using NLog; | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies | ||||
| { | ||||
|     public class YoutubeResolveStrategy : IResolveStrategy | ||||
|     { | ||||
|         private readonly Logger _log; | ||||
|  | ||||
|         public YoutubeResolveStrategy() | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|         } | ||||
|  | ||||
|         public async Task<SongInfo> ResolveSong(string query) | ||||
|         { | ||||
|             _log.Info("Getting link"); | ||||
|             string[] data; | ||||
|             try | ||||
|             { | ||||
|                 using (var ytdl = new YtdlOperation()) | ||||
|                 { | ||||
|                     data = (await ytdl.GetDataAsync(query)).Split('\n'); | ||||
|                 } | ||||
|                 if (data.Length < 6) | ||||
|                 { | ||||
|                     _log.Info("No song found. Data less than 6"); | ||||
|                     return null; | ||||
|                 } | ||||
|                 TimeSpan time; | ||||
|                 if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) | ||||
|                     time = TimeSpan.FromHours(24); | ||||
|  | ||||
|                 return new SongInfo() | ||||
|                 { | ||||
|                     Title = data[0], | ||||
|                     VideoId = data[1], | ||||
|                     Uri = async () => | ||||
|                     { | ||||
|                         using (var ytdl = new YtdlOperation()) | ||||
|                         { | ||||
|                             data = (await ytdl.GetDataAsync(query)).Split('\n'); | ||||
|                         } | ||||
|                         if (data.Length < 6) | ||||
|                         { | ||||
|                             _log.Info("No song found. Data less than 6"); | ||||
|                             return null; | ||||
|                         } | ||||
|                         return data[2]; | ||||
|                     }, | ||||
|                     Thumbnail = data[3], | ||||
|                     TotalTime = time, | ||||
|                     Provider = "YouTube", | ||||
|                     ProviderType = MusicType.YouTube, | ||||
|                     Query = "https://youtube.com/watch?v=" + data[1], | ||||
|                 }; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _log.Warn(ex); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								NadekoBot.Core/Modules/Music/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								NadekoBot.Core/Modules/Music/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| using NadekoBot.Modules.Music.Common; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Extensions | ||||
| { | ||||
|     public static class Extensions | ||||
|     { | ||||
|         public static Task<SongInfo> GetSongInfo(this SoundCloudVideo svideo) => | ||||
|             Task.FromResult(new SongInfo | ||||
|             { | ||||
|                 Title = svideo.FullName, | ||||
|                 Provider = "SoundCloud", | ||||
|                 Uri = () => svideo.StreamLink(), | ||||
|                 ProviderType = MusicType.Soundcloud, | ||||
|                 Query = svideo.TrackLink, | ||||
|                 Thumbnail = svideo.artwork_url, | ||||
|                 TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) | ||||
|             }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										901
									
								
								NadekoBot.Core/Modules/Music/Music.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										901
									
								
								NadekoBot.Core/Modules/Music/Music.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,901 @@ | ||||
| using Discord.Commands; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Core.Services; | ||||
| using Discord; | ||||
| using System.Threading.Tasks; | ||||
| using System; | ||||
| using System.Linq; | ||||
| using NadekoBot.Extensions; | ||||
| using System.Collections.Generic; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using System.IO; | ||||
| using System.Net.Http; | ||||
| using NadekoBot.Common; | ||||
| using NadekoBot.Common.Attributes; | ||||
| using NadekoBot.Common.Collections; | ||||
| using Newtonsoft.Json.Linq; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using NadekoBot.Modules.Music.Services; | ||||
| using NadekoBot.Modules.Music.Common.Exceptions; | ||||
| using NadekoBot.Modules.Music.Common; | ||||
| using NadekoBot.Modules.Music.Extensions; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music | ||||
| { | ||||
|     [NoPublicBot] | ||||
|     public class Music : NadekoTopLevelModule<MusicService> | ||||
|     { | ||||
|         private readonly DiscordSocketClient _client; | ||||
|         private readonly IBotCredentials _creds; | ||||
|         private readonly IGoogleApiService _google; | ||||
|         private readonly DbService _db; | ||||
|  | ||||
|         public Music(DiscordSocketClient client,  | ||||
|             IBotCredentials creds,  | ||||
|             IGoogleApiService google, | ||||
|             DbService db) | ||||
|         { | ||||
|             _client = client; | ||||
|             _creds = creds; | ||||
|             _google = google; | ||||
|             _db = db; | ||||
|         } | ||||
|  | ||||
|         //todo 50 changing server region is bugged again | ||||
|         //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) | ||||
|         //{ | ||||
|         //    var t = Task.Run(() => | ||||
|         //    { | ||||
|         //        var usr = iusr as SocketGuildUser; | ||||
|         //        if (usr == null || | ||||
|         //            oldState.VoiceChannel == newState.VoiceChannel) | ||||
|         //            return; | ||||
|  | ||||
|         //        var player = _music.GetPlayerOrDefault(usr.Guild.Id); | ||||
|  | ||||
|         //        if (player == null) | ||||
|         //            return; | ||||
|  | ||||
|         //        try | ||||
|         //        { | ||||
|         //            //if bot moved | ||||
|         //            if ((player.VoiceChannel == oldState.VoiceChannel) && | ||||
|         //                    usr.Id == _client.CurrentUser.Id) | ||||
|         //            { | ||||
|         //                //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel | ||||
|         //                //    player.TogglePause(); | ||||
|         //                //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel | ||||
|         //                //    player.TogglePause(); | ||||
|                         | ||||
|         //               // player.SetVoiceChannel(newState.VoiceChannel); | ||||
|         //                return; | ||||
|         //            } | ||||
|  | ||||
|         //            ////if some other user moved | ||||
|         //            //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause  | ||||
|         //            //        player.Paused && | ||||
|         //            //        newState.VoiceChannel.Users.Count >= 2) ||  // keep in mind bot is in the channel (+1) | ||||
|         //            //    (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause | ||||
|         //            //        !player.Paused && | ||||
|         //            //        oldState.VoiceChannel.Users.Count == 1)) | ||||
|         //            //{ | ||||
|         //            //    player.TogglePause(); | ||||
|         //            //    return; | ||||
|         //            //} | ||||
|         //        } | ||||
|         //        catch | ||||
|         //        { | ||||
|         //            // ignored | ||||
|         //        } | ||||
|         //    }); | ||||
|         //    return Task.CompletedTask; | ||||
|         //} | ||||
|  | ||||
|         private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent, bool queueFirst = false) | ||||
|         { | ||||
|             if (songInfo == null) | ||||
|             { | ||||
|                 if(!silent) | ||||
|                     await ReplyErrorLocalized("song_not_found").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             int index; | ||||
|             try | ||||
|             { | ||||
|                 index = queueFirst | ||||
|                     ? mp.EnqueueNext(songInfo) | ||||
|                     : mp.Enqueue(songInfo); | ||||
|             } | ||||
|             catch (QueueFullException) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); | ||||
|                 throw; | ||||
|             } | ||||
|             if (index != -1) | ||||
|             { | ||||
|                 if (!silent) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         var embed = new EmbedBuilder().WithOkColor() | ||||
|                                         .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon()) | ||||
|                                         .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") | ||||
|                                         .WithFooter(ef => ef.WithText(songInfo.PrettyProvider)); | ||||
|  | ||||
|                         if (Uri.IsWellFormedUriString(songInfo.Thumbnail, UriKind.Absolute)) | ||||
|                             embed.WithThumbnailUrl(songInfo.Thumbnail); | ||||
|  | ||||
|                         var queuedMessage = await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); | ||||
|                         if (mp.Stopped) | ||||
|                         { | ||||
|                             (await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); | ||||
|                         } | ||||
|                         queuedMessage?.DeleteAfter(10); | ||||
|                     } | ||||
|                     catch | ||||
|                     { | ||||
|                         // ignored | ||||
|                     }  | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Play([Remainder] string query = null) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             if (string.IsNullOrWhiteSpace(query)) | ||||
|             { | ||||
|                 await Next(); | ||||
|             } | ||||
|             else if (int.TryParse(query, out var index)) | ||||
|                 if (index >= 1) | ||||
|                     mp.SetIndex(index - 1); | ||||
|                 else | ||||
|                     return; | ||||
|             else | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     await Queue(query); | ||||
|                 } | ||||
|                 catch { } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Queue([Remainder] string query) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var songInfo = await _service.ResolveSong(query, Context.User.ToString()); | ||||
|             try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } | ||||
|             if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) | ||||
|             { | ||||
|                 Context.Message.DeleteAfter(10); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task QueueNext([Remainder] string query) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var songInfo = await _service.ResolveSong(query, Context.User.ToString()); | ||||
|             try { await InternalQueue(mp, songInfo, false, true); } catch (QueueFullException) { return; } | ||||
|             if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) | ||||
|             { | ||||
|                 Context.Message.DeleteAfter(10); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task QueueSearch([Remainder] string query) | ||||
|         { | ||||
|             var videos = (await _google.GetVideoInfosByKeywordAsync(query, 5)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             if (!videos.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("song_not_found").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var msg = await Context.Channel.SendConfirmAsync(string.Join("\n", videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Name)}\n\t{x.Url}"))); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var input = await GetUserInputAsync(Context.User.Id, Context.Channel.Id); | ||||
|                 if (input == null | ||||
|                     || !int.TryParse(input, out var index) | ||||
|                     || (index -= 1) < 0 | ||||
|                     || index >= videos.Length) | ||||
|                 { | ||||
|                     try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 query = videos[index].Url; | ||||
|  | ||||
|                 await Queue(query).ConfigureAwait(false); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task ListQueue(int page = 0) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var (current, songs) = mp.QueueArray(); | ||||
|  | ||||
|             if (!songs.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_player").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             if (--page < -1) | ||||
|                 return; | ||||
|              | ||||
|             try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } | ||||
|  | ||||
|             const int itemsPerPage = 10; | ||||
|  | ||||
|             if (page == -1) | ||||
|                 page = current / itemsPerPage; | ||||
|  | ||||
|             //if page is 0 (-1 after this decrement) that means default to the page current song is playing from | ||||
|             var total = mp.TotalPlaytime; | ||||
|             var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", | ||||
|                 (int)total.TotalHours, | ||||
|                 total.Minutes, | ||||
|                 total.Seconds); | ||||
|             var maxPlaytime = mp.MaxPlaytimeSeconds; | ||||
|             var lastPage = songs.Length / itemsPerPage; | ||||
|             Func<int, EmbedBuilder> printAction = curPage => | ||||
|             { | ||||
|                 var startAt = itemsPerPage * curPage; | ||||
|                 var number = 0 + startAt; | ||||
|                 var desc = string.Join("\n", songs | ||||
|                         .Skip(startAt) | ||||
|                         .Take(itemsPerPage) | ||||
|                         .Select(v => | ||||
|                         { | ||||
|                             if(number++ == current) | ||||
|                                 return $"**⇒**`{number}.` {v.PrettyFullName}"; | ||||
|                             else | ||||
|                                 return $"`{number}.` {v.PrettyFullName}"; | ||||
|                         })); | ||||
|  | ||||
|                 desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; | ||||
|  | ||||
|                 var add = ""; | ||||
|                 if (mp.Stopped) | ||||
|                     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 (mp.RepeatCurrentSong) | ||||
|                     add += "🔂 " + GetText("repeating_cur_song") + "\n"; | ||||
|                 else if (mp.Shuffle) | ||||
|                     add += "🔀 " + GetText("shuffling_playlist") + "\n"; | ||||
|                 else | ||||
|                 { | ||||
|                     if (mp.Autoplay) | ||||
|                         add += "↪ " + GetText("autoplaying") + "\n"; | ||||
|                     if (mp.FairPlay && !mp.Autoplay) | ||||
|                         add += " " + GetText("fairplay") + "\n"; | ||||
|                     else if (mp.RepeatPlaylist) | ||||
|                         add += "🔁 " + GetText("repeating_playlist") + "\n"; | ||||
|                 } | ||||
|  | ||||
|                 if (!string.IsNullOrWhiteSpace(add)) | ||||
|                     desc = add + "\n" + desc; | ||||
|                  | ||||
|                 var embed = new EmbedBuilder() | ||||
|                     .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) | ||||
|                         .WithMusicIcon()) | ||||
|                     .WithDescription(desc) | ||||
|                     .WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " + | ||||
|                                                   $"{("tracks".SnPl(songs.Length))} | {totalStr}")) | ||||
|                     .WithOkColor(); | ||||
|  | ||||
|                 return embed; | ||||
|             }; | ||||
|             await Context.Channel.SendPaginatedConfirmAsync(_client, page, printAction, lastPage, false).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Next(int skipCount = 1) | ||||
|         { | ||||
|             if (skipCount < 1) | ||||
|                 return; | ||||
|              | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             mp.Next(skipCount); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Stop() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             mp.Stop(); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Destroy() | ||||
|         { | ||||
|             await _service.DestroyPlayer(Context.Guild.Id); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Pause() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             mp.TogglePause(); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Volume(int val) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             if (val < 0 || val > 100) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             mp.SetVolume(val); | ||||
|             await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Defvol([Remainder] int val) | ||||
|         { | ||||
|             if (val < 0 || val > 100) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 uow.GuildConfigs.For(Context.Guild.Id, set => set).DefaultMusicVolume = val / 100.0f; | ||||
|                 uow.Complete(); | ||||
|             } | ||||
|             await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         [Priority(1)] | ||||
|         public async Task SongRemove(int index) | ||||
|         { | ||||
|             if (index < 1) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             try | ||||
|             { | ||||
|                 var song = mp.RemoveAt(index - 1); | ||||
|                 var embed = new EmbedBuilder() | ||||
|                             .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon()) | ||||
|                             .WithDescription(song.PrettyName) | ||||
|                             .WithFooter(ef => ef.WithText(song.PrettyInfo)) | ||||
|                             .WithErrorColor(); | ||||
|  | ||||
|                 await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); | ||||
|             } | ||||
|             catch (ArgumentOutOfRangeException) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public enum All { All } | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         [Priority(0)] | ||||
|         public async Task SongRemove(All all) | ||||
|         { | ||||
|             var mp = _service.GetPlayerOrDefault(Context.Guild.Id); | ||||
|             if (mp == null) | ||||
|                 return; | ||||
|             mp.Stop(true); | ||||
|             await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Playlists([Remainder] int num = 1) | ||||
|         { | ||||
|             if (num <= 0) | ||||
|                 return; | ||||
|  | ||||
|             List<MusicPlaylist> playlists; | ||||
|  | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num); | ||||
|             } | ||||
|  | ||||
|             var embed = new EmbedBuilder() | ||||
|                 .WithAuthor(eab => eab.WithName(GetText("playlists_page", num)).WithMusicIcon()) | ||||
|                 .WithDescription(string.Join("\n", playlists.Select(r => | ||||
|                     GetText("playlists", r.Id, r.Name, r.Author, r.Songs.Count)))) | ||||
|                 .WithOkColor(); | ||||
|             await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task DeletePlaylist([Remainder] int id) | ||||
|         { | ||||
|             var success = false; | ||||
|             try | ||||
|             { | ||||
|                 using (var uow = _db.UnitOfWork) | ||||
|                 { | ||||
|                     var pl = uow.MusicPlaylists.Get(id); | ||||
|  | ||||
|                     if (pl != null) | ||||
|                     { | ||||
|                         if (_creds.IsOwner(Context.User) || pl.AuthorId == Context.User.Id) | ||||
|                         { | ||||
|                             uow.MusicPlaylists.Remove(pl); | ||||
|                             await uow.CompleteAsync().ConfigureAwait(false); | ||||
|                             success = true; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (!success) | ||||
|                     await ReplyErrorLocalized("playlist_delete_fail").ConfigureAwait(false); | ||||
|                 else | ||||
|                     await ReplyConfirmLocalized("playlist_deleted").ConfigureAwait(false); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _log.Warn(ex); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Save([Remainder] string name) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             var songs = mp.QueueArray().Songs | ||||
|                 .Select(s => new PlaylistSong() | ||||
|                 { | ||||
|                     Provider = s.Provider, | ||||
|                     ProviderType = s.ProviderType, | ||||
|                     Title = s.Title, | ||||
|                     Query = s.Query, | ||||
|                 }).ToList(); | ||||
|  | ||||
|             MusicPlaylist playlist; | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 playlist = new MusicPlaylist | ||||
|                 { | ||||
|                     Name = name, | ||||
|                     Author = Context.User.Username, | ||||
|                     AuthorId = Context.User.Id, | ||||
|                     Songs = songs.ToList(), | ||||
|                 }; | ||||
|                 uow.MusicPlaylists.Add(playlist); | ||||
|                 await uow.CompleteAsync().ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|             await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                 .WithTitle(GetText("playlist_saved")) | ||||
|                 .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) | ||||
|                 .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); | ||||
|         } | ||||
|  | ||||
|         private static readonly ConcurrentHashSet<ulong> PlaylistLoadBlacklist = new ConcurrentHashSet<ulong>(); | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Load([Remainder] int id) | ||||
|         { | ||||
|             if (!PlaylistLoadBlacklist.Add(Context.Guild.Id)) | ||||
|                 return; | ||||
|             try | ||||
|             { | ||||
|                 var mp = await _service.GetOrCreatePlayer(Context); | ||||
|                 MusicPlaylist mpl; | ||||
|                 using (var uow = _db.UnitOfWork) | ||||
|                 { | ||||
|                     mpl = uow.MusicPlaylists.GetWithSongs(id); | ||||
|                 } | ||||
|  | ||||
|                 if (mpl == null) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|                 IUserMessage msg = null; | ||||
|                 try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } | ||||
|                 foreach (var item in mpl.Songs) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         await Task.Yield(); | ||||
|                          | ||||
|                         await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _service.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); | ||||
|                     } | ||||
|                     catch (SongNotFoundException) { } | ||||
|                     catch { break; } | ||||
|                 } | ||||
|                 if (msg != null) | ||||
|                     await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 PlaylistLoadBlacklist.TryRemove(Context.Guild.Id); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Fairplay() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var val = mp.FairPlay = !mp.FairPlay; | ||||
|  | ||||
|             if (val) | ||||
|             { | ||||
|                 await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task SongAutoDelete() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var val = mp.AutoDelete = !mp.AutoDelete; | ||||
|  | ||||
|             if (val) | ||||
|             { | ||||
|                 await ReplyConfirmLocalized("sad_enabled").ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyConfirmLocalized("sad_disabled").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task SoundCloudQueue([Remainder] string query) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var song = await _service.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); | ||||
|             await InternalQueue(mp, song, false).ConfigureAwait(false); | ||||
|         } | ||||
|          | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task SoundCloudPl([Remainder] string pl) | ||||
|         { | ||||
|             pl = pl?.Trim(); | ||||
|  | ||||
|             if (string.IsNullOrWhiteSpace(pl)) | ||||
|                 return; | ||||
|  | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             using (var http = new HttpClient()) | ||||
|             { | ||||
|                 var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject<SoundCloudVideo[]>(); | ||||
|                 IUserMessage msg = null; | ||||
|                 try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { } | ||||
|                 foreach (var svideo in scvids) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         await Task.Yield(); | ||||
|                         var sinfo = await svideo.GetSongInfo(); | ||||
|                         sinfo.QueuerName = Context.User.ToString(); | ||||
|                         await InternalQueue(mp, sinfo, true); | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         _log.Warn(ex); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 if (msg != null) | ||||
|                     await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task NowPlaying() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var (_, currentSong) = mp.Current; | ||||
|             if (currentSong == null) | ||||
|                 return; | ||||
|             try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } | ||||
|  | ||||
|             var embed = new EmbedBuilder().WithOkColor() | ||||
|                             .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) | ||||
|                             .WithDescription(currentSong.PrettyName) | ||||
|                             .WithThumbnailUrl(currentSong.Thumbnail) | ||||
|                             .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime  + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); | ||||
|  | ||||
|             await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task ShufflePlaylist() | ||||
|         { | ||||
|             var mp = await _service.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)] | ||||
|         public async Task Playlist([Remainder] string playlist) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(playlist)) | ||||
|                 return; | ||||
|  | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault(); | ||||
|             if (plId == null) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); | ||||
|             if (!ids.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             var count = ids.Count(); | ||||
|             var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", | ||||
|                 Format.Bold(count.ToString()))).ConfigureAwait(false); | ||||
|              | ||||
|             foreach (var song in ids) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     if (mp.Exited) | ||||
|                         return; | ||||
|  | ||||
|                     await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _service.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); | ||||
|                 } | ||||
|                 catch (SongNotFoundException) { } | ||||
|                 catch { break; } | ||||
|             } | ||||
|  | ||||
|             await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Radio(string radioLink) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var song = await _service.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio); | ||||
|             await InternalQueue(mp, song, false).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         [OwnerOnly] | ||||
|         public async Task Local([Remainder] string path) | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var song = await _service.ResolveSong(path, Context.User.ToString(), MusicType.Local); | ||||
|             await InternalQueue(mp, song, false).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         [OwnerOnly] | ||||
|         public async Task LocalPl([Remainder] string dirPath) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(dirPath)) | ||||
|                 return; | ||||
|  | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             DirectoryInfo dir; | ||||
|             try { dir = new DirectoryInfo(dirPath); } catch { return; } | ||||
|             var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) | ||||
|                                 .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png"); | ||||
|             foreach (var file in fileEnum) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     await Task.Yield(); | ||||
|                     var song = await _service.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); | ||||
|                     await InternalQueue(mp, song, true).ConfigureAwait(false); | ||||
|                 } | ||||
|                 catch (QueueFullException) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     _log.Warn(ex); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task Move() | ||||
|         { | ||||
|             var vch = ((IGuildUser)Context.User).VoiceChannel; | ||||
|  | ||||
|             if (vch == null) | ||||
|                 return; | ||||
|  | ||||
|             var mp = _service.GetPlayerOrDefault(Context.Guild.Id); | ||||
|  | ||||
|             if (mp == null) | ||||
|                 return; | ||||
|  | ||||
|             await mp.SetVoiceChannel(vch); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task MoveSong([Remainder] string fromto) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(fromto)) | ||||
|                 return; | ||||
|  | ||||
|             MusicPlayer mp = _service.GetPlayerOrDefault(Context.Guild.Id); | ||||
|             if (mp == null) | ||||
|                 return; | ||||
|  | ||||
|             fromto = fromto?.Trim(); | ||||
|             var fromtoArr = fromto.Split('>'); | ||||
|  | ||||
|             SongInfo s; | ||||
|             if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) || | ||||
|                 !int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2 | ||||
|                 || (s = mp.MoveSong(--n1, --n2)) == null) | ||||
|             { | ||||
|                 await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var embed = new EmbedBuilder() | ||||
|                 .WithTitle(s.Title.TrimTo(65)) | ||||
|                 .WithUrl(s.SongUrl) | ||||
|             .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) | ||||
|             .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1 + 1}").WithIsInline(true)) | ||||
|             .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2 + 1}").WithIsInline(true)) | ||||
|             .WithColor(NadekoBot.OkColor); | ||||
|             await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task SetMaxQueue(uint size = 0) | ||||
|         { | ||||
|             if (size < 0) | ||||
|                 return; | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             mp.MaxQueueSize = size; | ||||
|  | ||||
|             if (size == 0) | ||||
|                 await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); | ||||
|             else | ||||
|                 await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task SetMaxPlaytime(uint seconds) | ||||
|         { | ||||
|             if (seconds < 15 && seconds != 0) | ||||
|                 return; | ||||
|  | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             mp.MaxPlaytimeSeconds = seconds; | ||||
|             if (seconds == 0) | ||||
|                 await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); | ||||
|             else | ||||
|                 await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task ReptCurSong() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|             var (_, currentSong) = mp.Current; | ||||
|             if (currentSong == null) | ||||
|                 return; | ||||
|             var currentValue = mp.ToggleRepeatSong(); | ||||
|  | ||||
|             if (currentValue) | ||||
|                 await Context.Channel.EmbedAsync(new EmbedBuilder() | ||||
|                     .WithOkColor() | ||||
|                     .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track"))) | ||||
|                     .WithDescription(currentSong.PrettyName) | ||||
|                     .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); | ||||
|             else | ||||
|                 await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) | ||||
|                                             .ConfigureAwait(false); | ||||
|         } | ||||
|          | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         public async Task RepeatPl() | ||||
|         { | ||||
|             var mp = await _service.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)] | ||||
|         public async Task Autoplay() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             if (!mp.ToggleAutoplay()) | ||||
|                 await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); | ||||
|             else | ||||
|                 await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [RequireContext(ContextType.Guild)] | ||||
|         [RequireUserPermission(GuildPermission.ManageMessages)] | ||||
|         public async Task SetMusicChannel() | ||||
|         { | ||||
|             var mp = await _service.GetOrCreatePlayer(Context); | ||||
|  | ||||
|             mp.OutputTextChannel = (ITextChannel)Context.Channel; | ||||
|  | ||||
|             await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										239
									
								
								NadekoBot.Core/Modules/Music/Services/MusicService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								NadekoBot.Core/Modules/Music/Services/MusicService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Discord; | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Core.Services.Database.Models; | ||||
| using NLog; | ||||
| using System.IO; | ||||
| using Discord.Commands; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Common; | ||||
| using NadekoBot.Core.Services.Impl; | ||||
| using NadekoBot.Core.Services; | ||||
| using NadekoBot.Modules.Music.Common; | ||||
| using NadekoBot.Modules.Music.Common.Exceptions; | ||||
| using NadekoBot.Modules.Music.Common.SongResolver; | ||||
|  | ||||
| namespace NadekoBot.Modules.Music.Services | ||||
| { | ||||
|     public class MusicService : INService, IUnloadableService | ||||
|     { | ||||
|         public const string MusicDataPath = "data/musicdata"; | ||||
|  | ||||
|         private readonly IGoogleApiService _google; | ||||
|         private readonly NadekoStrings _strings; | ||||
|         private readonly ILocalization _localization; | ||||
|         private readonly DbService _db; | ||||
|         private readonly Logger _log; | ||||
|         private readonly SoundCloudApiService _sc; | ||||
|         private readonly IBotCredentials _creds; | ||||
|         private readonly ConcurrentDictionary<ulong, float> _defaultVolumes; | ||||
|         private readonly DiscordSocketClient _client; | ||||
|  | ||||
|         public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>(); | ||||
|  | ||||
|         public MusicService(DiscordSocketClient client, IGoogleApiService google, | ||||
|             NadekoStrings strings, ILocalization localization, DbService db, | ||||
|             SoundCloudApiService sc, IBotCredentials creds, NadekoBot bot) | ||||
|         { | ||||
|             _client = client; | ||||
|             _google = google; | ||||
|             _strings = strings; | ||||
|             _localization = localization; | ||||
|             _db = db; | ||||
|             _sc = sc; | ||||
|             _creds = creds; | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|  | ||||
|             _client.LeftGuild += _client_LeftGuild; | ||||
|  | ||||
|             try { Directory.Delete(MusicDataPath, true); } catch { } | ||||
|  | ||||
|             _defaultVolumes = new ConcurrentDictionary<ulong, float>( | ||||
|                 bot.AllGuildConfigs | ||||
|                     .ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume)); | ||||
|  | ||||
|             Directory.CreateDirectory(MusicDataPath); | ||||
|         } | ||||
|  | ||||
|         public Task Unload() | ||||
|         { | ||||
|             _client.LeftGuild -= _client_LeftGuild; | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|  | ||||
|         private Task _client_LeftGuild(SocketGuild arg) | ||||
|         { | ||||
|             var t = DestroyPlayer(arg.Id); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|  | ||||
|         public float GetDefaultVolume(ulong guildId) | ||||
|         { | ||||
|             return _defaultVolumes.GetOrAdd(guildId, (id) => | ||||
|             { | ||||
|                 using (var uow = _db.UnitOfWork) | ||||
|                 { | ||||
|                     return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context) | ||||
|         { | ||||
|             var gUsr = (IGuildUser)context.User; | ||||
|             var txtCh = (ITextChannel)context.Channel; | ||||
|             var vCh = gUsr.VoiceChannel; | ||||
|             return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh); | ||||
|         } | ||||
|  | ||||
|         public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) | ||||
|         { | ||||
|             string GetText(string text, params object[] replacements) => | ||||
|                 _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); | ||||
|  | ||||
|             _log.Info("Checks"); | ||||
|             if (voiceCh == null || voiceCh.Guild != textCh.Guild) | ||||
|             { | ||||
|                 if (textCh != null) | ||||
|                 { | ||||
|                     await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); | ||||
|                 } | ||||
|                 throw new NotInVoiceChannelException(); | ||||
|             } | ||||
|             _log.Info("Get or add"); | ||||
|             return MusicPlayers.GetOrAdd(guildId, _ => | ||||
|             { | ||||
|                 _log.Info("Getting default volume"); | ||||
|                 var vol = GetDefaultVolume(guildId); | ||||
|                 _log.Info("Creating musicplayer instance"); | ||||
|                 var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol); | ||||
|  | ||||
|                 IUserMessage playingMessage = null; | ||||
|                 IUserMessage lastFinishedMessage = null; | ||||
|  | ||||
|                 _log.Info("Subscribing"); | ||||
|                 mp.OnCompleted += async (s, song) => | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         lastFinishedMessage?.DeleteAfter(0); | ||||
|  | ||||
|                         try | ||||
|                         { | ||||
|                             lastFinishedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                                     .WithAuthor(eab => eab.WithName(GetText("finished_song")).WithMusicIcon()) | ||||
|                                     .WithDescription(song.PrettyName) | ||||
|                                     .WithFooter(ef => ef.WithText(song.PrettyInfo))) | ||||
|                                 .ConfigureAwait(false); | ||||
|                         } | ||||
|                         catch | ||||
|                         { | ||||
|                             // ignored | ||||
|                         } | ||||
|                     } | ||||
|                     catch | ||||
|                     { | ||||
|                         // ignored | ||||
|                     } | ||||
|                 }; | ||||
|                 mp.OnStarted += async (player, song) => | ||||
|                 { | ||||
|                     //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } | ||||
|                     //catch | ||||
|                     //{ | ||||
|                     //    // ignored | ||||
|                     //} | ||||
|                     var sender = player; | ||||
|                     if (sender == null) | ||||
|                         return; | ||||
|                     try | ||||
|                     { | ||||
|                         playingMessage?.DeleteAfter(0); | ||||
|  | ||||
|                         playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                                                     .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon()) | ||||
|                                                     .WithDescription(song.Song.PrettyName) | ||||
|                                                     .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo))) | ||||
|                                                     .ConfigureAwait(false); | ||||
|                     } | ||||
|                     catch | ||||
|                     { | ||||
|                         // ignored | ||||
|                     } | ||||
|                 }; | ||||
|                 mp.OnPauseChanged += async (player, paused) => | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         IUserMessage msg; | ||||
|                         if (paused) | ||||
|                             msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("paused")).ConfigureAwait(false); | ||||
|                         else | ||||
|                             msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("resumed")).ConfigureAwait(false); | ||||
|  | ||||
|                         msg?.DeleteAfter(10); | ||||
|                     } | ||||
|                     catch | ||||
|                     { | ||||
|                         // ignored | ||||
|                     } | ||||
|                 }; | ||||
|                 _log.Info("Done creating"); | ||||
|                 return mp; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         public MusicPlayer GetPlayerOrDefault(ulong guildId) | ||||
|         { | ||||
|             if (MusicPlayers.TryGetValue(guildId, out var mp)) | ||||
|                 return mp; | ||||
|             else | ||||
|                 return null; | ||||
|         } | ||||
|  | ||||
|         public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch) | ||||
|         { | ||||
|             var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4)).ToArray(); | ||||
|             if (!related.Any()) | ||||
|                 return; | ||||
|  | ||||
|             var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube); | ||||
|             if (si == null) | ||||
|                 throw new SongNotFoundException(); | ||||
|             var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); | ||||
|             mp.Enqueue(si); | ||||
|         } | ||||
|  | ||||
|         public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null) | ||||
|         { | ||||
|             query.ThrowIfNull(nameof(query)); | ||||
|  | ||||
|             ISongResolverFactory resolverFactory = new SongResolverFactory(_sc); | ||||
|             var strategy = await resolverFactory.GetResolveStrategy(query, musicType); | ||||
|             var sinfo = await strategy.ResolveSong(query); | ||||
|  | ||||
|             if (sinfo == null) | ||||
|                 return null; | ||||
|  | ||||
|             sinfo.QueuerName = queuerName; | ||||
|  | ||||
|             return sinfo; | ||||
|         } | ||||
|  | ||||
|         public async Task DestroyAllPlayers() | ||||
|         { | ||||
|             foreach (var key in MusicPlayers.Keys) | ||||
|             { | ||||
|                 await DestroyPlayer(key); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task DestroyPlayer(ulong id) | ||||
|         { | ||||
|             if (MusicPlayers.TryRemove(id, out var mp)) | ||||
|                 await mp.Destroy(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user