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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user