diff --git a/src/NadekoBot/Modules/Music/Classes/MusicControls.cs b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs new file mode 100644 index 00000000..03aaccef --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs @@ -0,0 +1,281 @@ +using Discord; +using Discord.Audio; +using NadekoBot.Extensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +namespace NadekoBot.Modules.Music.Classes +{ + + public enum MusicType + { + Radio, + Normal, + Local, + Soundcloud + } + + public enum StreamState + { + Resolving, + Queued, + Playing, + Completed + } + + public class MusicPlayer + { + private IAudioClient audioClient { get; set; } + + private readonly List playlist = new List(); + public IReadOnlyCollection Playlist => playlist; + + public Song CurrentSong { get; private set; } + public CancellationTokenSource SongCancelSource { get; private set; } + private CancellationToken cancelToken { get; set; } + + public bool Paused { get; set; } + + public float Volume { get; private set; } + + public event EventHandler OnCompleted = delegate { }; + public event EventHandler OnStarted = delegate { }; + + public IVoiceChannel PlaybackVoiceChannel { get; private set; } + + private bool Destroyed { get; set; } = false; + public bool RepeatSong { get; private set; } = false; + public bool RepeatPlaylist { get; private set; } = false; + public bool Autoplay { get; set; } = false; + public uint MaxQueueSize { get; set; } = 0; + + private ConcurrentQueue actionQueue { get; set; } = new ConcurrentQueue(); + + public MusicPlayer(IVoiceChannel startingVoiceChannel, float? defaultVolume) + { + if (startingVoiceChannel == null) + throw new ArgumentNullException(nameof(startingVoiceChannel)); + Volume = defaultVolume ?? 1.0f; + + PlaybackVoiceChannel = startingVoiceChannel; + SongCancelSource = new CancellationTokenSource(); + cancelToken = SongCancelSource.Token; + + Task.Run(async () => + { + try + { + while (!Destroyed) + { + try + { + Action action; + if (actionQueue.TryDequeue(out action)) + { + action(); + } + } + finally + { + await Task.Delay(100).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + Console.WriteLine("Action queue crashed"); + Console.WriteLine(ex); + } + }).ConfigureAwait(false); + + var t = new Thread(new ThreadStart(async () => + { + try + { + while (!Destroyed) + { + try + { + if (audioClient?.ConnectionState != ConnectionState.Connected) + { + audioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); + continue; + } + + CurrentSong = GetNextSong(); + RemoveSongAt(0); + + if (CurrentSong == null) + continue; + + + OnStarted(this, CurrentSong); + await CurrentSong.Play(audioClient, cancelToken); + + OnCompleted(this, CurrentSong); + + if (RepeatPlaylist) + AddSong(CurrentSong, CurrentSong.QueuerName); + + if (RepeatSong) + AddSong(CurrentSong, 0); + + } + finally + { + if (!cancelToken.IsCancellationRequested) + { + SongCancelSource.Cancel(); + } + SongCancelSource = new CancellationTokenSource(); + cancelToken = SongCancelSource.Token; + CurrentSong = null; + await Task.Delay(300).ConfigureAwait(false); + } + } + } + catch (Exception ex) { + Console.WriteLine("Music thread crashed."); + Console.WriteLine(ex); + } + })); + + t.Start(); + } + + public void Next() + { + actionQueue.Enqueue(() => + { + Paused = false; + SongCancelSource.Cancel(); + }); + } + + public void Stop() + { + actionQueue.Enqueue(() => + { + RepeatPlaylist = false; + RepeatSong = false; + playlist.Clear(); + if (!SongCancelSource.IsCancellationRequested) + SongCancelSource.Cancel(); + }); + } + + public void TogglePause() => Paused = !Paused; + + public int SetVolume(int volume) + { + if (volume < 0) + volume = 0; + if (volume > 100) + volume = 100; + + Volume = volume / 100.0f; + return volume; + } + + private Song GetNextSong() => + playlist.FirstOrDefault(); + + public void Shuffle() + { + actionQueue.Enqueue(() => + { + playlist.Shuffle(); + }); + } + + public void AddSong(Song s, string username) + { + if (s == null) + throw new ArgumentNullException(nameof(s)); + ThrowIfQueueFull(); + actionQueue.Enqueue(() => + { + s.MusicPlayer = this; + s.QueuerName = username.TrimTo(10); + playlist.Add(s); + }); + } + + public void AddSong(Song s, int index) + { + if (s == null) + throw new ArgumentNullException(nameof(s)); + actionQueue.Enqueue(() => + { + playlist.Insert(index, s); + }); + } + + public void RemoveSong(Song s) + { + if (s == null) + throw new ArgumentNullException(nameof(s)); + actionQueue.Enqueue(() => + { + playlist.Remove(s); + }); + } + + public void RemoveSongAt(int index) + { + actionQueue.Enqueue(() => + { + if (index < 0 || index >= playlist.Count) + return; + playlist.RemoveAt(index); + }); + } + + internal void ClearQueue() + { + actionQueue.Enqueue(() => + { + playlist.Clear(); + }); + } + + public void Destroy() + { + actionQueue.Enqueue(async () => + { + RepeatPlaylist = false; + RepeatSong = false; + Destroyed = true; + playlist.Clear(); + if (!SongCancelSource.IsCancellationRequested) + SongCancelSource.Cancel(); + await audioClient.DisconnectAsync(); + }); + } + + internal Task MoveToVoiceChannel(IVoiceChannel voiceChannel) + { + if (audioClient?.ConnectionState != ConnectionState.Connected) + throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); + PlaybackVoiceChannel = voiceChannel; + return PlaybackVoiceChannel.ConnectAsync(); + } + + internal bool ToggleRepeatSong() => this.RepeatSong = !this.RepeatSong; + + internal bool ToggleRepeatPlaylist() => this.RepeatPlaylist = !this.RepeatPlaylist; + + internal bool ToggleAutoplay() => this.Autoplay = !this.Autoplay; + + internal void ThrowIfQueueFull() + { + if (MaxQueueSize == 0) + return; + if (playlist.Count >= MaxQueueSize) + throw new PlaylistFullException(); + } + } +} diff --git a/src/NadekoBot/Modules/Music/Classes/PlaylistFullException.cs b/src/NadekoBot/Modules/Music/Classes/PlaylistFullException.cs new file mode 100644 index 00000000..15541d42 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/PlaylistFullException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NadekoBot.Modules.Music.Classes +{ + class PlaylistFullException : Exception + { + public PlaylistFullException(string message) : base(message) + { + } + public PlaylistFullException() : base("Queue is full.") { } + } +} diff --git a/src/NadekoBot/Modules/Music/Classes/Song.cs b/src/NadekoBot/Modules/Music/Classes/Song.cs new file mode 100644 index 00000000..db4b06ea --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/Song.cs @@ -0,0 +1,422 @@ +using Discord.Audio; +using NadekoBot.Classes; +using NadekoBot.Extensions; +using System; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using VideoLibrary; + +namespace NadekoBot.Modules.Music.Classes +{ + public class SongInfo + { + public string Provider { get; internal set; } + public MusicType ProviderType { get; internal set; } + /// + /// Will be set only if the providertype is normal + /// + public string Query { get; internal set; } + public string Title { get; internal set; } + public string Uri { get; internal set; } + } + public class Song + { + public StreamState State { get; internal set; } + public string PrettyName => + $"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}` `by {QueuerName}`"; + public SongInfo SongInfo { get; } + public string QueuerName { get; set; } + + public MusicPlayer MusicPlayer { get; set; } + + public string PrettyCurrentTime() + { + var time = TimeSpan.FromSeconds(bytesSent / 3840 / 50); + return $"【{(int)time.TotalMinutes}m {time.Seconds}s】"; + } + + private ulong bytesSent { get; set; } = 0; + + public bool PrintStatusMessage { get; set; } = true; + + private int skipTo = 0; + public int SkipTo { + get { return SkipTo; } + set { + skipTo = value; + bytesSent = (ulong)skipTo * 3840 * 50; + } + } + + public Song(SongInfo songInfo) + { + this.SongInfo = songInfo; + } + + public Song Clone() + { + var s = new Song(SongInfo); + s.MusicPlayer = MusicPlayer; + s.State = StreamState.Queued; + return s; + } + + public Song SetMusicPlayer(MusicPlayer mp) + { + this.MusicPlayer = mp; + return this; + } + + internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) + { + var filename = Path.Combine(Music.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); + + SongBuffer sb = new SongBuffer(filename, SongInfo, skipTo); + var bufferTask = sb.BufferSong(cancelToken).ConfigureAwait(false); + + var inStream = new FileStream(sb.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); ; + + bytesSent = 0; + + try + { + var attempt = 0; + + var prebufferingTask = CheckPrebufferingAsync(inStream, sb, cancelToken); + var sw = new Stopwatch(); + sw.Start(); + var t = await Task.WhenAny(prebufferingTask, Task.Delay(5000, cancelToken)); + if (t != prebufferingTask) + { + Console.WriteLine("Prebuffering timed out or canceled. Cannot get any data from the stream."); + return; + } + else if(prebufferingTask.IsCanceled) + { + Console.WriteLine("Prebuffering timed out. Cannot get any data from the stream."); + return; + } + sw.Stop(); + Console.WriteLine("Prebuffering successfully completed in "+ sw.Elapsed); + + + var outStream = voiceClient.CreatePCMStream(3840); + + const int blockSize = 3840; + byte[] buffer = new byte[blockSize]; + while (!cancelToken.IsCancellationRequested) + { + //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); + var read = inStream.Read(buffer, 0, buffer.Length); + //await inStream.CopyToAsync(voiceClient.OutputStream); + unchecked + { + bytesSent += (ulong)read; + } + if (read < blockSize) + { + if (sb.IsNextFileReady()) + { + inStream.Dispose(); + inStream = new FileStream(sb.GetNextFile(), FileMode.Open, FileAccess.Read, FileShare.Write); + read += inStream.Read(buffer, read, buffer.Length - read); + attempt = 0; + } + if (read == 0) + { + if (sb.BufferingCompleted) + break; + if (attempt++ == 20) + { + MusicPlayer.SongCancelSource.Cancel(); + break; + } + else + await Task.Delay(100, cancelToken).ConfigureAwait(false); + } + else + attempt = 0; + } + else + attempt = 0; + + while (this.MusicPlayer.Paused) + await Task.Delay(200, cancelToken).ConfigureAwait(false); + + buffer = AdjustVolume(buffer, MusicPlayer.Volume); + await outStream.WriteAsync(buffer, 0, read); + } + } + finally + { + await bufferTask; + if(inStream != null) + inStream.Dispose(); + Console.WriteLine("l"); + sb.CleanFiles(); + } + } + + private async Task CheckPrebufferingAsync(Stream inStream, SongBuffer sb, CancellationToken cancelToken) + { + while (!sb.BufferingCompleted && inStream.Length < 2.MiB()) + { + await Task.Delay(100, cancelToken); + } + Console.WriteLine("Buffering successfull"); + } + + /* + //stackoverflow ftw + private static byte[] AdjustVolume(byte[] audioSamples, float volume) + { + if (Math.Abs(volume - 1.0f) < 0.01f) + return audioSamples; + var array = new byte[audioSamples.Length]; + for (var i = 0; i < array.Length; i += 2) + { + + // convert byte pair to int + short buf1 = audioSamples[i + 1]; + short buf2 = audioSamples[i]; + + buf1 = (short)((buf1 & 0xff) << 8); + buf2 = (short)(buf2 & 0xff); + + var res = (short)(buf1 | buf2); + res = (short)(res * volume); + + // convert back + array[i] = (byte)res; + array[i + 1] = (byte)(res >> 8); + + } + return array; + } + */ + + //aidiakapi ftw + public unsafe static byte[] AdjustVolume(byte[] audioSamples, float volume) + { + Contract.Requires(audioSamples != null); + Contract.Requires(audioSamples.Length % 2 == 0); + Contract.Requires(volume >= 0f && volume <= 1f); + Contract.Assert(BitConverter.IsLittleEndian); + + if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; + + // 16-bit precision for the multiplication + int volumeFixed = (int)Math.Round(volume * 65536d); + + int count = audioSamples.Length / 2; + + fixed (byte* srcBytes = audioSamples) + { + short* src = (short*)srcBytes; + + for (int i = count; i != 0; i--, src++) + *src = (short)(((*src) * volumeFixed) >> 16); + } + + return audioSamples; + } + + public static async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) + { + if (string.IsNullOrWhiteSpace(query)) + throw new ArgumentNullException(nameof(query)); + + if (musicType != MusicType.Local && IsRadioLink(query)) + { + musicType = MusicType.Radio; + query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; + } + + try + { + switch (musicType) + { + case MusicType.Local: + return new Song(new SongInfo + { + Uri = "\"" + Path.GetFullPath(query) + "\"", + Title = Path.GetFileNameWithoutExtension(query), + Provider = "Local File", + ProviderType = musicType, + Query = query, + }); + case MusicType.Radio: + return new Song(new SongInfo + { + Uri = query, + Title = $"{query}", + Provider = "Radio Stream", + ProviderType = musicType, + Query = query + }); + } + if (SoundCloud.Default.IsSoundCloudLink(query)) + { + var svideo = await SoundCloud.Default.ResolveVideoAsync(query).ConfigureAwait(false); + return new Song(new SongInfo + { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = svideo.StreamLink, + ProviderType = musicType, + Query = svideo.TrackLink, + }); + } + + if (musicType == MusicType.Soundcloud) + { + var svideo = await SoundCloud.Default.GetVideoByQueryAsync(query).ConfigureAwait(false); + return new Song(new SongInfo + { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = svideo.StreamLink, + ProviderType = MusicType.Normal, + Query = svideo.TrackLink, + }); + } + + var link = (await NadekoBot.Google.GetVideosByKeywordsAsync(query).ConfigureAwait(false)).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(link)) + throw new OperationCanceledException("Not a valid youtube query."); + var allVideos = await Task.Run(async () => await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false)).ConfigureAwait(false); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 192) + .OrderByDescending(v => v.AudioBitrate) + .FirstOrDefault(); + + if (video == null) // do something with this error + throw new Exception("Could not load any video elements based on the query."); + var m = Regex.Match(query, @"\?t=(?\d*)"); + int gotoTime = 0; + if (m.Captures.Count > 0) + int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new Song(new SongInfo + { + Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + Provider = "YouTube", + Uri = video.Uri, + Query = link, + ProviderType = musicType, + }); + song.SkipTo = gotoTime; + return song; + } + catch (Exception ex) + { + Console.WriteLine($"Failed resolving the link.{ex.Message}"); + return null; + } + } + + private static async Task 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 = Regex.Match(file, "File1=(?.*?)\\n"); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Console.WriteLine($"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 = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Console.WriteLine($"Failed reading .m3u:\n{file}"); + return null; + } + + } + if (query.Contains(".asx")) + { + // + try + { + var m = Regex.Match(file, ".*?)\""); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Console.WriteLine($"Failed reading .asx:\n{file}"); + return null; + } + } + if (query.Contains(".xspf")) + { + /* + + + + file:///mp3s/song_1.mp3 + */ + try + { + var m = Regex.Match(file, "(?.*?)"); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Console.WriteLine($"Failed reading .xspf:\n{file}"); + return null; + } + } + + return query; + } + + private static bool IsRadioLink(string query) => + (query.StartsWith("http") || + query.StartsWith("ww")) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + } +} diff --git a/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs b/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs new file mode 100644 index 00000000..d9192940 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs @@ -0,0 +1,159 @@ +using NadekoBot.Extensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Music.Classes +{ + /// + /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. + /// It also help for large music by deleting files that are already seen. + /// + class SongBuffer + { + + public SongBuffer(string basename, SongInfo songInfo, int skipTo) + { + Basename = basename; + SongInfo = songInfo; + SkipTo = skipTo; + } + + private string Basename; + + private SongInfo SongInfo; + + private int SkipTo; + + private static int MAX_FILE_SIZE = 20.MiB(); + + private long FileNumber = -1; + + private long NextFileToRead = 0; + + public bool BufferingCompleted { get; private set;} = false; + + private ulong CurrentBufferSize = 0; + + public Task BufferSong(CancellationToken cancelToken) => + Task.Factory.StartNew(async () => + { + Process p = null; + FileStream outStream = null; + try + { + p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + + byte[] buffer = new byte[81920]; + int currentFileSize = 0; + ulong prebufferSize = 100ul.MiB(); + + outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); + while (!p.HasExited) //Also fix low bandwidth + { + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); + if (currentFileSize >= MAX_FILE_SIZE) + { + try + { + outStream.Dispose(); + }catch { } + outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); + currentFileSize = bytesRead; + } + else + { + currentFileSize += bytesRead; + } + CurrentBufferSize += Convert.ToUInt64(bytesRead); + await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + while (CurrentBufferSize > prebufferSize) + await Task.Delay(100, cancelToken); + } + BufferingCompleted = true; + } + catch (System.ComponentModel.Win32Exception) + { + var oldclr = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(@"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/SCv72y + Linux Guide: https://goo.gl/rRhjCp"); + Console.ForegroundColor = oldclr; + } + catch (Exception ex) + { + Console.WriteLine($"Buffering stopped: {ex.Message}"); + } + finally + { + if(outStream != null) + outStream.Dispose(); + Console.WriteLine($"Buffering done."); + if (p != null) + { + try + { + p.Kill(); + } + catch { } + p.Dispose(); + } + } + }, TaskCreationOptions.LongRunning); + + /// + /// Return the next file to read, and delete the old one + /// + /// Name of the file to read + public string GetNextFile() + { + string filename = Basename + "-" + NextFileToRead; + + if (NextFileToRead != 0) + { + try + { + CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); + File.Delete(Basename + "-" + (NextFileToRead - 1)); + } + catch { } + } + NextFileToRead++; + return filename; + } + + public bool IsNextFileReady() + { + return NextFileToRead <= FileNumber; + } + + public void CleanFiles() + { + for (long i = NextFileToRead - 1 ; i <= FileNumber; i++) + { + try + { + File.Delete(Basename + "-" + i); + } + catch { } + } + } + } +} diff --git a/src/NadekoBot/Modules/Music/Classes/SoundCloud.cs b/src/NadekoBot/Modules/Music/Classes/SoundCloud.cs new file mode 100644 index 00000000..88c5bdbe --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/SoundCloud.cs @@ -0,0 +1,141 @@ +using NadekoBot.Classes; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Music.Classes +{ + public class SoundCloud + { + private static readonly SoundCloud _instance = new SoundCloud(); + public static SoundCloud Default => _instance; + + static SoundCloud() { } + public SoundCloud() { } + + public async Task ResolveVideoAsync(string url) + { + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentNullException(nameof(url)); + if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId)) + throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId)); + + string response = ""; + + using (var http = new HttpClient()) + { + response = await http.GetStringAsync($"http://api.soundcloud.com/resolve?url={url}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false); + + } + + + var responseObj = Newtonsoft.Json.JsonConvert.DeserializeObject(response); + if (responseObj?.Kind != "track") + throw new InvalidOperationException("Url is either not a track, or it doesn't exist."); + + return responseObj; + } + + public bool IsSoundCloudLink(string url) => + System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)"); + + internal async Task GetVideoByQueryAsync(string query) + { + if (string.IsNullOrWhiteSpace(query)) + throw new ArgumentNullException(nameof(query)); + if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId)) + throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId)); + + var response = ""; + using (var http = new HttpClient()) + { + await http.GetStringAsync($"http://api.soundcloud.com/tracks?q={Uri.EscapeDataString(query)}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false); + } + + var responseObj = JsonConvert.DeserializeObject(response).Where(s => s.Streamable).FirstOrDefault(); + if (responseObj?.Kind != "track") + throw new InvalidOperationException("Query yielded no results."); + + return responseObj; + } + } + + public class SoundCloudVideo + { + public string Kind { get; set; } = ""; + public long Id { get; set; } = 0; + public SoundCloudUser User { get; set; } = new SoundCloudUser(); + public string Title { get; set; } = ""; + [JsonIgnore] + public string FullName => User.Name + " - " + Title; + public bool Streamable { get; set; } = false; + [JsonProperty("permalink_url")] + public string TrackLink { get; set; } = ""; + [JsonIgnore] + public string StreamLink => $"https://api.soundcloud.com/tracks/{Id}/stream?client_id={NadekoBot.Credentials.SoundCloudClientId}"; + } + public class SoundCloudUser + { + [Newtonsoft.Json.JsonProperty("username")] + public string Name { get; set; } + } + /* + {"kind":"track", + "id":238888167, + "created_at":"2015/12/24 01:04:52 +0000", + "user_id":43141975, + "duration":120852, + "commentable":true, + "state":"finished", + "original_content_size":4834829, + "last_modified":"2015/12/24 01:17:59 +0000", + "sharing":"public", + "tag_list":"Funky", + "permalink":"18-fd", + "streamable":true, + "embeddable_by":"all", + "downloadable":false, + "purchase_url":null, + "label_id":null, + "purchase_title":null, + "genre":"Disco", + "title":"18 Ж", + "description":"", + "label_name":null, + "release":null, + "track_type":null, + "key_signature":null, + "isrc":null, + "video_url":null, + "bpm":null, + "release_year":null, + "release_month":null, + "release_day":null, + "original_format":"mp3", + "license":"all-rights-reserved", + "uri":"https://api.soundcloud.com/tracks/238888167", + "user":{ + "id":43141975, + "kind":"user", + "permalink":"mrb00gi", + "username":"Mrb00gi", + "last_modified":"2015/12/01 16:06:57 +0000", + "uri":"https://api.soundcloud.com/users/43141975", + "permalink_url":"http://soundcloud.com/mrb00gi", + "avatar_url":"https://a1.sndcdn.com/images/default_avatar_large.png" + }, + "permalink_url":"http://soundcloud.com/mrb00gi/18-fd", + "artwork_url":null, + "waveform_url":"https://w1.sndcdn.com/gsdLfvEW1cUK_m.png", + "stream_url":"https://api.soundcloud.com/tracks/238888167/stream", + "playback_count":7, + "download_count":0, + "favoritings_count":1, + "comment_count":0, + "attachments_uri":"https://api.soundcloud.com/tracks/238888167/attachments"} + + */ + +}