add files?

This commit is contained in:
appelemac 2016-08-22 12:06:44 +02:00
parent fd5916a54d
commit aa69703768
5 changed files with 1015 additions and 0 deletions

View File

@ -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<Song> playlist = new List<Song>();
public IReadOnlyCollection<Song> 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<Song> OnCompleted = delegate { };
public event EventHandler<Song> 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<Action> actionQueue { get; set; } = new ConcurrentQueue<Action>();
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();
}
}
}

View File

@ -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.") { }
}
}

View File

@ -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; }
/// <summary>
/// Will be set only if the providertype is normal
/// </summary>
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<Song> 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=(?<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<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 = Regex.Match(file, "File1=(?<url>.*?)\\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, "(?<url>^[^#].*)", 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"))
{
//<ref href="http://armitunes.com:8000"/>
try
{
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
Console.WriteLine($"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 = Regex.Match(file, "<location>(?<url>.*?)</location>");
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"));
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// Return the next file to read, and delete the old one
/// </summary>
/// <returns>Name of the file to read</returns>
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 { }
}
}
}
}

View File

@ -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<SoundCloudVideo> 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<SoundCloudVideo>(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<SoundCloudVideo> 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<SoundCloudVideo[]>(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"}
*/
}