mirror of
https://github.com/Sarsoo/Spotify.NET.git
synced 2024-12-23 14:46:26 +00:00
Started with Unit Tests and added .editorconfig for syntax settings
Disabled examples for now
This commit is contained in:
parent
2c4463529b
commit
9f6729ad60
28
.editorconfig
Normal file
28
.editorconfig
Normal file
@ -0,0 +1,28 @@
|
||||
[*]
|
||||
indent_style = space
|
||||
|
||||
[*.{cs,csx,vb,vbx}]]
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
charset = utf-8-bom
|
||||
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_style_require_accessibility_modifiers = always:warning
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = true:warning
|
||||
csharp_prefer_simple_using_statement = true:warning
|
@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
internal static class AuthUtil
|
||||
{
|
||||
public static bool OpenBrowser(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NETSTANDARD2_0
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
url = url.Replace("&", "^&");
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}"));
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", url);
|
||||
}
|
||||
#else
|
||||
url = url.Replace("&", "^&");
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}"));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
using Unosquare.Labs.EmbedIO;
|
||||
using Unosquare.Labs.EmbedIO.Constants;
|
||||
using Unosquare.Labs.EmbedIO.Modules;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
public class AuthorizationCodeAuth : SpotifyAuthServer<AuthorizationCode>
|
||||
{
|
||||
public string SecretId { get; set; }
|
||||
|
||||
public ProxyConfig ProxyConfig { get; set; }
|
||||
|
||||
public AuthorizationCodeAuth(string redirectUri, string serverUri, Scope scope = Scope.None, string state = "") : base("code", "AuthorizationCodeAuth", redirectUri, serverUri, scope, state)
|
||||
{ }
|
||||
|
||||
public AuthorizationCodeAuth(string clientId, string secretId, string redirectUri, string serverUri, Scope scope = Scope.None, string state = "") : this(redirectUri, serverUri, scope, state)
|
||||
{
|
||||
ClientId = clientId;
|
||||
SecretId = secretId;
|
||||
}
|
||||
|
||||
private bool ShouldRegisterNewApp()
|
||||
{
|
||||
return string.IsNullOrEmpty(SecretId) || string.IsNullOrEmpty(ClientId);
|
||||
}
|
||||
|
||||
public override string GetUri()
|
||||
{
|
||||
return ShouldRegisterNewApp() ? $"{RedirectUri}/start.html#{State}" : base.GetUri();
|
||||
}
|
||||
|
||||
protected override void AdaptWebServer(WebServer webServer)
|
||||
{
|
||||
webServer.Module<WebApiModule>().RegisterController<AuthorizationCodeAuthController>();
|
||||
}
|
||||
|
||||
private string GetAuthHeader() => $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(ClientId + ":" + SecretId))}";
|
||||
|
||||
public async Task<Token> RefreshToken(string refreshToken)
|
||||
{
|
||||
List<KeyValuePair<string, string>> args = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "refresh_token"),
|
||||
new KeyValuePair<string, string>("refresh_token", refreshToken)
|
||||
};
|
||||
|
||||
return await GetToken(args);
|
||||
}
|
||||
|
||||
public async Task<Token> ExchangeCode(string code)
|
||||
{
|
||||
List<KeyValuePair<string, string>> args = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", RedirectUri)
|
||||
};
|
||||
|
||||
return await GetToken(args);
|
||||
}
|
||||
|
||||
private async Task<Token> GetToken(IEnumerable<KeyValuePair<string, string>> args)
|
||||
{
|
||||
HttpClientHandler handler = ProxyConfig.CreateClientHandler(ProxyConfig);
|
||||
HttpClient client = new HttpClient(handler);
|
||||
client.DefaultRequestHeaders.Add("Authorization", GetAuthHeader());
|
||||
HttpContent content = new FormUrlEncodedContent(args);
|
||||
|
||||
HttpResponseMessage resp = await client.PostAsync("https://accounts.spotify.com/api/token", content);
|
||||
string msg = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<Token>(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorizationCode
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
internal class AuthorizationCodeAuthController : WebApiController
|
||||
{
|
||||
[WebApiHandler(HttpVerbs.Get, "/")]
|
||||
public Task<bool> GetEmpty()
|
||||
{
|
||||
string state = Request.QueryString["state"];
|
||||
AuthorizationCodeAuth.Instances.TryGetValue(state, out SpotifyAuthServer<AuthorizationCode> auth);
|
||||
|
||||
string code = null;
|
||||
string error = Request.QueryString["error"];
|
||||
if (error == null)
|
||||
code = Request.QueryString["code"];
|
||||
|
||||
Task.Factory.StartNew(() => auth?.TriggerAuth(new AuthorizationCode
|
||||
{
|
||||
Code = code,
|
||||
Error = error
|
||||
}));
|
||||
|
||||
return HttpContext.HtmlResponseAsync("<html><script type=\"text/javascript\">window.close();</script>OK - This window can be closed now</html>");
|
||||
}
|
||||
|
||||
[WebApiHandler(HttpVerbs.Post, "/")]
|
||||
public async Task<bool> PostValues()
|
||||
{
|
||||
Dictionary<string, object> formParams = await HttpContext.RequestFormDataDictionaryAsync();
|
||||
|
||||
string state = (string) formParams["state"];
|
||||
AuthorizationCodeAuth.Instances.TryGetValue(state, out SpotifyAuthServer<AuthorizationCode> authServer);
|
||||
|
||||
AuthorizationCodeAuth auth = (AuthorizationCodeAuth) authServer;
|
||||
auth.ClientId = (string) formParams["clientId"];
|
||||
auth.SecretId = (string) formParams["secretId"];
|
||||
|
||||
string uri = auth.GetUri();
|
||||
return HttpContext.Redirect(uri, false);
|
||||
}
|
||||
|
||||
public AuthorizationCodeAuthController(IHttpContext context) : base(context)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using SpotifyAPI.Web.Models;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
public class CredentialsAuth
|
||||
{
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
public string ClientId { get; set; }
|
||||
|
||||
public ProxyConfig ProxyConfig { get; set; }
|
||||
|
||||
public CredentialsAuth(string clientId, string clientSecret)
|
||||
{
|
||||
ClientId = clientId;
|
||||
ClientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public async Task<Token> GetToken()
|
||||
{
|
||||
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(ClientId + ":" + ClientSecret));
|
||||
|
||||
List<KeyValuePair<string, string>> args = new List<KeyValuePair<string, string>>
|
||||
{new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||
};
|
||||
|
||||
HttpClientHandler handler = ProxyConfig.CreateClientHandler(ProxyConfig);
|
||||
HttpClient client = new HttpClient(handler);
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
|
||||
HttpContent content = new FormUrlEncodedContent(args);
|
||||
|
||||
HttpResponseMessage resp = await client.PostAsync("https://accounts.spotify.com/api/token", content);
|
||||
string msg = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<Token>(msg);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
using Unosquare.Labs.EmbedIO;
|
||||
using Unosquare.Labs.EmbedIO.Constants;
|
||||
using Unosquare.Labs.EmbedIO.Modules;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
public class ImplicitGrantAuth : SpotifyAuthServer<Token>
|
||||
{
|
||||
public ImplicitGrantAuth(string clientId, string redirectUri, string serverUri, Scope scope = Scope.None, string state = "") : base("token", "ImplicitGrantAuth", redirectUri, serverUri, scope, state)
|
||||
{
|
||||
ClientId = clientId;
|
||||
}
|
||||
|
||||
protected override void AdaptWebServer(WebServer webServer)
|
||||
{
|
||||
webServer.Module<WebApiModule>().RegisterController<ImplicitGrantAuthController>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ImplicitGrantAuthController : WebApiController
|
||||
{
|
||||
[WebApiHandler(HttpVerbs.Get, "/auth")]
|
||||
public Task<bool> GetAuth()
|
||||
{
|
||||
string state = Request.QueryString["state"];
|
||||
SpotifyAuthServer<Token> auth = ImplicitGrantAuth.GetByState(state);
|
||||
if (auth == null)
|
||||
return HttpContext.StringResponseAsync(
|
||||
$"Failed - Unable to find auth request with state \"{state}\" - Please retry");
|
||||
|
||||
Token token;
|
||||
string error = Request.QueryString["error"];
|
||||
if (error == null)
|
||||
{
|
||||
string accessToken = Request.QueryString["access_token"];
|
||||
string tokenType = Request.QueryString["token_type"];
|
||||
string expiresIn = Request.QueryString["expires_in"];
|
||||
token = new Token
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
ExpiresIn = double.Parse(expiresIn),
|
||||
TokenType = tokenType
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
token = new Token
|
||||
{
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(() => auth.TriggerAuth(token));
|
||||
return HttpContext.HtmlResponseAsync("<html><script type=\"text/javascript\">window.close();</script>OK - This window can be closed now</html>");
|
||||
}
|
||||
|
||||
public ImplicitGrantAuthController(IHttpContext context) : base(context)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using Unosquare.Labs.EmbedIO;
|
||||
using Unosquare.Labs.EmbedIO.Modules;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
public abstract class SpotifyAuthServer<T>
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
public string ServerUri { get; set; }
|
||||
public string RedirectUri { get; set; }
|
||||
public string State { get; set; }
|
||||
public Scope Scope { get; set; }
|
||||
public bool ShowDialog { get; set; }
|
||||
|
||||
private readonly string _folder;
|
||||
private readonly string _type;
|
||||
private WebServer _server;
|
||||
protected CancellationTokenSource _serverSource;
|
||||
|
||||
public delegate void OnAuthReceived(object sender, T payload);
|
||||
public event OnAuthReceived AuthReceived;
|
||||
|
||||
internal static readonly Dictionary<string, SpotifyAuthServer<T>> Instances = new Dictionary<string, SpotifyAuthServer<T>>();
|
||||
|
||||
internal SpotifyAuthServer(string type, string folder, string redirectUri, string serverUri, Scope scope = Scope.None, string state = "")
|
||||
{
|
||||
_type = type;
|
||||
_folder = folder;
|
||||
ServerUri = serverUri;
|
||||
RedirectUri = redirectUri;
|
||||
Scope = scope;
|
||||
State = string.IsNullOrEmpty(state) ? string.Join("", Guid.NewGuid().ToString("n").Take(8)) : state;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Instances.Add(State, this);
|
||||
_serverSource = new CancellationTokenSource();
|
||||
|
||||
_server = WebServer.Create(ServerUri);
|
||||
_server.RegisterModule(new WebApiModule());
|
||||
AdaptWebServer(_server);
|
||||
_server.RegisterModule(new ResourceFilesModule(Assembly.GetExecutingAssembly(), $"SpotifyAPI.Web.Auth.Resources.{_folder}"));
|
||||
#pragma warning disable 4014
|
||||
_server.RunAsync(_serverSource.Token);
|
||||
#pragma warning restore 4014
|
||||
}
|
||||
|
||||
public virtual string GetUri()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder("https://accounts.spotify.com/authorize/?");
|
||||
builder.Append("client_id=" + ClientId);
|
||||
builder.Append($"&response_type={_type}");
|
||||
builder.Append("&redirect_uri=" + RedirectUri);
|
||||
builder.Append("&state=" + State);
|
||||
builder.Append("&scope=" + Scope.GetStringAttribute(" "));
|
||||
builder.Append("&show_dialog=" + ShowDialog);
|
||||
return Uri.EscapeUriString(builder.ToString());
|
||||
}
|
||||
|
||||
public void Stop(int delay = 2000)
|
||||
{
|
||||
if (_serverSource == null) return;
|
||||
_serverSource.CancelAfter(delay);
|
||||
Instances.Remove(State);
|
||||
}
|
||||
|
||||
public void OpenBrowser()
|
||||
{
|
||||
string uri = GetUri();
|
||||
AuthUtil.OpenBrowser(uri);
|
||||
}
|
||||
|
||||
internal void TriggerAuth(T payload)
|
||||
{
|
||||
AuthReceived?.Invoke(this, payload);
|
||||
}
|
||||
|
||||
internal static SpotifyAuthServer<T> GetByState(string state)
|
||||
{
|
||||
return Instances.TryGetValue(state, out SpotifyAuthServer<T> auth) ? auth : null;
|
||||
}
|
||||
|
||||
protected abstract void AdaptWebServer(WebServer webServer);
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
using Unosquare.Labs.EmbedIO;
|
||||
using Unosquare.Labs.EmbedIO.Constants;
|
||||
using Unosquare.Labs.EmbedIO.Modules;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A version of <see cref="AuthorizationCodeAuth"/> that does not store your client secret, client ID or redirect URI, enforcing a secure authorization flow. Requires an exchange server that will return the authorization code to its callback server via GET request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It's recommended that you use <see cref="TokenSwapWebAPIFactory"/> if you would like to use the TokenSwap method.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class TokenSwapAuth : SpotifyAuthServer<AuthorizationCode>
|
||||
{
|
||||
readonly string _exchangeServerUri;
|
||||
|
||||
/// <summary>
|
||||
/// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival.
|
||||
/// </summary>
|
||||
public string HtmlResponse { get; set; } = "<script>window.close();</script>";
|
||||
|
||||
/// <summary>
|
||||
/// If true, will time how long it takes for access to expire. On expiry, the <see cref="OnAccessTokenExpired"/> event fires.
|
||||
/// </summary>
|
||||
public bool TimeAccessExpiry { get; set; }
|
||||
|
||||
public ProxyConfig ProxyConfig { get; set; }
|
||||
|
||||
/// <param name="exchangeServerUri">The URI to an exchange server that will perform the key exchange.</param>
|
||||
/// <param name="serverUri">The URI to host the server at that your exchange server should return the authorization code to by GET request. (e.g. http://localhost:4002)</param>
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="state">Stating none will randomly generate a state parameter.</param>
|
||||
/// <param name="htmlResponse">The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival.</param>
|
||||
public TokenSwapAuth(string exchangeServerUri, string serverUri, Scope scope = Scope.None, string state = "",
|
||||
string htmlResponse = "") : base("code", "", "", serverUri, scope, state)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(htmlResponse))
|
||||
{
|
||||
HtmlResponse = htmlResponse;
|
||||
}
|
||||
|
||||
_exchangeServerUri = exchangeServerUri;
|
||||
}
|
||||
|
||||
protected override void AdaptWebServer(WebServer webServer)
|
||||
{
|
||||
webServer.Module<WebApiModule>().RegisterController<TokenSwapAuthController>();
|
||||
}
|
||||
|
||||
public override string GetUri()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(_exchangeServerUri);
|
||||
builder.Append("?");
|
||||
builder.Append("response_type=code");
|
||||
builder.Append("&state=" + State);
|
||||
builder.Append("&scope=" + Scope.GetStringAttribute(" "));
|
||||
builder.Append("&show_dialog=" + ShowDialog);
|
||||
return Uri.EscapeUriString(builder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of times to retry getting a token.
|
||||
/// <para/>
|
||||
/// A token get is attempted every time you <see cref="RefreshAuthAsync(string)"/> and <see cref="ExchangeCodeAsync(string)"/>.
|
||||
/// </summary>
|
||||
public int MaxGetTokenRetries { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HTTP request to obtain a token object.<para/>
|
||||
/// Parameter grantType can only be "refresh_token" or "authorization_code". authorizationCode and refreshToken are not mandatory, but at least one must be provided for your desired grant_type request otherwise an invalid response will be given and an exception is likely to be thrown.
|
||||
/// <para>
|
||||
/// Will re-attempt on error, on null or on no access token <see cref="MaxGetTokenRetries"/> times before finally returning null.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="grantType">Can only be "refresh_token" or "authorization_code".</param>
|
||||
/// <param name="authorizationCode">This needs to be defined if "grantType" is "authorization_code".</param>
|
||||
/// <param name="refreshToken">This needs to be defined if "grantType" is "refresh_token".</param>
|
||||
/// <param name="currentRetries">Does not need to be defined. Used internally for retry attempt recursion.</param>
|
||||
/// <returns>Attempts to return a full <see cref="Token"/>, but after retry attempts, may return a <see cref="Token"/> with no <see cref="Token.AccessToken"/>, or null.</returns>
|
||||
async Task<Token> GetToken(string grantType, string authorizationCode = "", string refreshToken = "",
|
||||
int currentRetries = 0)
|
||||
{
|
||||
FormUrlEncodedContent content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{ { "grant_type", grantType },
|
||||
{ "code", authorizationCode },
|
||||
{ "refresh_token", refreshToken }
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
HttpClientHandler handler = ProxyConfig.CreateClientHandler(ProxyConfig);
|
||||
HttpClient client = new HttpClient(handler);
|
||||
HttpResponseMessage siteResponse = await client.PostAsync(_exchangeServerUri, content);
|
||||
|
||||
Token token = JsonConvert.DeserializeObject<Token>(await siteResponse.Content.ReadAsStringAsync());
|
||||
// Don't need to check if it was null - if it is, it will resort to the catch block.
|
||||
if (!token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
if (currentRetries >= MaxGetTokenRetries)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentRetries++;
|
||||
// The reason I chose to implement the retries system this way is because a static or instance
|
||||
// variable keeping track would inhibit parallelism i.e. using this function on multiple threads/tasks.
|
||||
// It's not clear why someone would like to do that, but it's better to cater for all kinds of uses.
|
||||
return await GetToken(grantType, authorizationCode, refreshToken, currentRetries);
|
||||
}
|
||||
}
|
||||
|
||||
System.Timers.Timer _accessTokenExpireTimer;
|
||||
|
||||
/// <summary>
|
||||
/// When Spotify authorization has expired. Will only trigger if <see cref="TimeAccessExpiry"/> is true.
|
||||
/// </summary>
|
||||
public event EventHandler OnAccessTokenExpired;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="TimeAccessExpiry"/> is true, sets a timer for how long access will take to expire.
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
void SetAccessExpireTimer(Token token)
|
||||
{
|
||||
if (!TimeAccessExpiry) return;
|
||||
|
||||
if (_accessTokenExpireTimer != null)
|
||||
{
|
||||
_accessTokenExpireTimer.Stop();
|
||||
_accessTokenExpireTimer.Dispose();
|
||||
}
|
||||
|
||||
_accessTokenExpireTimer = new System.Timers.Timer
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = token.ExpiresIn * 1000,
|
||||
AutoReset = false
|
||||
};
|
||||
_accessTokenExpireTimer.Elapsed += (sender, e) => OnAccessTokenExpired?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the authorization code to silently (doesn't open a browser) obtain both an access token and refresh token, where the refresh token would be required for you to use <see cref="RefreshAuthAsync(string)"/>.
|
||||
/// </summary>
|
||||
/// <param name="authorizationCode"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Token> ExchangeCodeAsync(string authorizationCode)
|
||||
{
|
||||
Token token = await GetToken("authorization_code", authorizationCode : authorizationCode);
|
||||
if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
SetAccessExpireTimer(token);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the refresh token to silently (doesn't open a browser) obtain a fresh access token, no refresh token is given however (as it does not change).
|
||||
/// </summary>
|
||||
/// <param name="refreshToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Token> RefreshAuthAsync(string refreshToken)
|
||||
{
|
||||
Token token = await GetToken("refresh_token", refreshToken : refreshToken);
|
||||
if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
SetAccessExpireTimer(token);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TokenSwapAuthController : WebApiController
|
||||
{
|
||||
public TokenSwapAuthController(IHttpContext context) : base(context)
|
||||
{ }
|
||||
|
||||
[WebApiHandler(HttpVerbs.Get, "/auth")]
|
||||
public Task<bool> GetAuth()
|
||||
{
|
||||
string state = Request.QueryString["state"];
|
||||
SpotifyAuthServer<AuthorizationCode> auth = TokenSwapAuth.GetByState(state);
|
||||
|
||||
string code = null;
|
||||
string error = Request.QueryString["error"];
|
||||
if (error == null)
|
||||
{
|
||||
code = Request.QueryString["code"];
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(() => auth?.TriggerAuth(new AuthorizationCode
|
||||
{
|
||||
Code = code,
|
||||
Error = error
|
||||
}));
|
||||
return HttpContext.HtmlResponseAsync(((TokenSwapAuth) auth).HtmlResponse);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a <see cref="SpotifyWebAPI"/> using the TokenSwapAuth process.
|
||||
/// </summary>
|
||||
public class TokenSwapWebAPIFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Access provided by Spotify expires after 1 hour. If true, <see cref="TokenSwapAuth"/> will time the access tokens, and access will attempt to be silently (without opening a browser) refreshed automatically. This will not make <see cref="OnAccessTokenExpired"/> fire, see <see cref="TimeAccessExpiry"/> for that.
|
||||
/// </summary>
|
||||
public bool AutoRefresh { get; set; }
|
||||
/// <summary>
|
||||
/// If true when calling <see cref="GetWebApiAsync"/>, will time how long it takes for access to Spotify to expire. The event <see cref="OnAccessTokenExpired"/> fires when the timer elapses.
|
||||
/// </summary>
|
||||
public bool TimeAccessExpiry { get; set; }
|
||||
/// <summary>
|
||||
/// The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.
|
||||
/// </summary>
|
||||
public int Timeout { get; set; }
|
||||
public Scope Scope { get; set; }
|
||||
/// <summary>
|
||||
/// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.
|
||||
/// </summary>
|
||||
public string ExchangeServerUri { get; set; }
|
||||
/// <summary>
|
||||
/// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002
|
||||
/// </summary>
|
||||
public string HostServerUri { get; set; }
|
||||
/// <summary>
|
||||
/// Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.
|
||||
/// </summary>
|
||||
public bool OpenBrowser { get; set; }
|
||||
/// <summary>
|
||||
/// The HTML to respond with when the callback server has been reached. By default, it is set to close the window on arrival.
|
||||
/// </summary>
|
||||
public string HtmlResponse { get; set; }
|
||||
/// <summary>
|
||||
/// Whether or not to show a dialog saying "Is this you?" during the initial key exchange. It should be noted that this would allow a user the opportunity to change accounts.
|
||||
/// </summary>
|
||||
public bool ShowDialog { get; set; }
|
||||
/// <summary>
|
||||
/// The maximum amount of times to retry getting a token.
|
||||
/// <para/>
|
||||
/// A token get is attempted every time you <see cref="GetWebApiAsync"/> and <see cref="RefreshAuthAsync"/>. Increasing this may improve how often these actions succeed - although it won't solve any underlying problems causing a get token failure.
|
||||
/// </summary>
|
||||
public int MaxGetTokenRetries { get; set; } = 10;
|
||||
/// <summary>
|
||||
/// Returns a SpotifyWebAPI using the TokenSwapAuth process.
|
||||
/// </summary>
|
||||
/// <param name="exchangeServerUri">The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.</param>
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="hostServerUri">The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002</param>
|
||||
/// <param name="timeout">The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.</param>
|
||||
/// <param name="autoRefresh">Access provided by Spotify expires after 1 hour. If true, access will attempt to be silently (without opening a browser) refreshed automatically.</param>
|
||||
/// <param name="openBrowser">Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.</param>
|
||||
public TokenSwapWebAPIFactory(string exchangeServerUri, Scope scope = Scope.None, string hostServerUri = "http://localhost:4002", int timeout = 10, bool autoRefresh = false, bool openBrowser = true)
|
||||
{
|
||||
AutoRefresh = autoRefresh;
|
||||
Timeout = timeout;
|
||||
Scope = scope;
|
||||
ExchangeServerUri = exchangeServerUri;
|
||||
HostServerUri = hostServerUri;
|
||||
OpenBrowser = openBrowser;
|
||||
|
||||
OnAccessTokenExpired += async(sender, e) =>
|
||||
{
|
||||
if (AutoRefresh)
|
||||
{
|
||||
await RefreshAuthAsync();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Token lastToken;
|
||||
SpotifyWebAPI lastWebApi;
|
||||
TokenSwapAuth lastAuth;
|
||||
|
||||
public class ExchangeReadyEventArgs : EventArgs
|
||||
{
|
||||
public string ExchangeUri { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// When the URI to get an authorization code is ready to be used to be visited. Not required if <see cref="OpenBrowser"/> is true as the exchange URI will automatically be visited for you.
|
||||
/// </summary>
|
||||
public event EventHandler<ExchangeReadyEventArgs> OnExchangeReady;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the access for a SpotifyWebAPI returned by this factory.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task RefreshAuthAsync()
|
||||
{
|
||||
Token token = await lastAuth.RefreshAuthAsync(lastToken.RefreshToken);
|
||||
|
||||
if (token == null)
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"Token not returned by server."));
|
||||
}
|
||||
else if (token.HasError())
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"{token.Error} {token.ErrorDescription}"));
|
||||
}
|
||||
else if (string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Token had no access token attached."));
|
||||
}
|
||||
else
|
||||
{
|
||||
lastWebApi.AccessToken = token.AccessToken;
|
||||
OnAuthSuccess?.Invoke(this, new AuthSuccessEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
// By defining empty EventArgs objects, you can specify additional information later on as you see fit and it won't
|
||||
// be considered a breaking change to consumers of this API.
|
||||
//
|
||||
// They don't even need to be constructed for their associated events to be invoked - just pass the static Empty property.
|
||||
public class AccessTokenExpiredEventArgs : EventArgs
|
||||
{
|
||||
public static new AccessTokenExpiredEventArgs Empty { get; } = new AccessTokenExpiredEventArgs();
|
||||
|
||||
public AccessTokenExpiredEventArgs()
|
||||
{ }
|
||||
}
|
||||
/// <summary>
|
||||
/// When the authorization from Spotify expires. This will only occur if <see cref="AutoRefresh"/> is true.
|
||||
/// </summary>
|
||||
public event EventHandler<AccessTokenExpiredEventArgs> OnAccessTokenExpired;
|
||||
|
||||
public class AuthSuccessEventArgs : EventArgs
|
||||
{
|
||||
public static new AuthSuccessEventArgs Empty { get; } = new AuthSuccessEventArgs();
|
||||
|
||||
public AuthSuccessEventArgs()
|
||||
{ }
|
||||
}
|
||||
/// <summary>
|
||||
/// When an authorization attempt succeeds and gains authorization.
|
||||
/// </summary>
|
||||
public event EventHandler<AuthSuccessEventArgs> OnAuthSuccess;
|
||||
|
||||
public class AuthFailureEventArgs : EventArgs
|
||||
{
|
||||
public static new AuthFailureEventArgs Empty { get; } = new AuthFailureEventArgs("");
|
||||
|
||||
public string Error { get; }
|
||||
|
||||
public AuthFailureEventArgs(string error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// When an authorization attempt fails to gain authorization.
|
||||
/// </summary>
|
||||
public event EventHandler<AuthFailureEventArgs> OnAuthFailure;
|
||||
|
||||
/// <summary>
|
||||
/// Manually triggers the timeout for any ongoing get web API request.
|
||||
/// </summary>
|
||||
public void CancelGetWebApiRequest()
|
||||
{
|
||||
if (webApiTimeoutTimer == null) return;
|
||||
|
||||
// The while loop in GetWebApiSync() will react and trigger the timeout.
|
||||
webApiTimeoutTimer.Stop();
|
||||
webApiTimeoutTimer.Dispose();
|
||||
webApiTimeoutTimer = null;
|
||||
}
|
||||
|
||||
System.Timers.Timer webApiTimeoutTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authorized and ready to use SpotifyWebAPI by following the SecureAuthorizationCodeAuth process with its current settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<SpotifyWebAPI> GetWebApiAsync()
|
||||
{
|
||||
return await Task<SpotifyWebAPI>.Factory.StartNew(() =>
|
||||
{
|
||||
bool currentlyAuthorizing = true;
|
||||
|
||||
// Cancel any ongoing get web API requests
|
||||
CancelGetWebApiRequest();
|
||||
|
||||
lastAuth = new TokenSwapAuth(
|
||||
exchangeServerUri: ExchangeServerUri,
|
||||
serverUri: HostServerUri,
|
||||
scope: Scope,
|
||||
htmlResponse: HtmlResponse)
|
||||
{
|
||||
ShowDialog = ShowDialog,
|
||||
MaxGetTokenRetries = MaxGetTokenRetries,
|
||||
TimeAccessExpiry = AutoRefresh || TimeAccessExpiry
|
||||
};
|
||||
lastAuth.AuthReceived += async(sender, response) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(response.Error) || string.IsNullOrEmpty(response.Code))
|
||||
{
|
||||
// We only want one auth failure to be fired, if the request timed out then don't bother.
|
||||
if (!webApiTimeoutTimer.Enabled) return;
|
||||
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs(response.Error));
|
||||
currentlyAuthorizing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
lastToken = await lastAuth.ExchangeCodeAsync(response.Code);
|
||||
|
||||
if (lastToken == null || lastToken.HasError() || string.IsNullOrEmpty(lastToken.AccessToken))
|
||||
{
|
||||
// We only want one auth failure to be fired, if the request timed out then don't bother.
|
||||
if (!webApiTimeoutTimer.Enabled) return;
|
||||
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Exchange token not returned by server."));
|
||||
currentlyAuthorizing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWebApi != null)
|
||||
{
|
||||
lastWebApi.Dispose();
|
||||
}
|
||||
lastWebApi = new SpotifyWebAPI()
|
||||
{
|
||||
TokenType = lastToken.TokenType,
|
||||
AccessToken = lastToken.AccessToken
|
||||
};
|
||||
|
||||
lastAuth.Stop();
|
||||
|
||||
OnAuthSuccess?.Invoke(this, AuthSuccessEventArgs.Empty);
|
||||
currentlyAuthorizing = false;
|
||||
};
|
||||
lastAuth.OnAccessTokenExpired += async(sender, e) =>
|
||||
{
|
||||
if (TimeAccessExpiry)
|
||||
{
|
||||
OnAccessTokenExpired?.Invoke(sender, AccessTokenExpiredEventArgs.Empty);
|
||||
}
|
||||
|
||||
if (AutoRefresh)
|
||||
{
|
||||
await RefreshAuthAsync();
|
||||
}
|
||||
};
|
||||
lastAuth.Start();
|
||||
OnExchangeReady?.Invoke(this, new ExchangeReadyEventArgs { ExchangeUri = lastAuth.GetUri() });
|
||||
if (OpenBrowser)
|
||||
{
|
||||
lastAuth.OpenBrowser();
|
||||
}
|
||||
|
||||
webApiTimeoutTimer = new System.Timers.Timer
|
||||
{
|
||||
AutoReset = false,
|
||||
Enabled = true,
|
||||
Interval = Timeout * 1000
|
||||
};
|
||||
|
||||
while (currentlyAuthorizing && webApiTimeoutTimer.Enabled);
|
||||
|
||||
// If a timeout occurred
|
||||
if (lastWebApi == null && currentlyAuthorizing)
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Authorization request has timed out."));
|
||||
}
|
||||
|
||||
return lastWebApi;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -10,18 +10,18 @@ namespace SpotifyAPI.Web.Examples.ASP.Controllers
|
||||
[Authorize(AuthenticationSchemes = "Spotify")]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index()
|
||||
public IActionResult Index()
|
||||
{
|
||||
var accessToken = await HttpContext.GetTokenAsync("Spotify", "access_token");
|
||||
SpotifyWebAPI api = new SpotifyWebAPI
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
TokenType = "Bearer"
|
||||
};
|
||||
// var accessToken = await HttpContext.GetTokenAsync("Spotify", "access_token");
|
||||
// SpotifyWebAPI api = new SpotifyWebAPI
|
||||
// {
|
||||
// AccessToken = accessToken,
|
||||
// TokenType = "Bearer"
|
||||
// };
|
||||
|
||||
var savedTracks = await api.GetSavedTracksAsync(50);
|
||||
// var savedTracks = await api.GetSavedTracksAsync(50);
|
||||
|
||||
return View(new IndexModel { SavedTracks = savedTracks });
|
||||
return View(new IndexModel { SavedTracks = null });
|
||||
}
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
|
@ -5,6 +5,6 @@ namespace SpotifyAPI.Web.Examples.ASP.Models
|
||||
{
|
||||
public class IndexModel
|
||||
{
|
||||
public Paging<SavedTrack> SavedTracks;
|
||||
public Paging<object> SavedTracks;
|
||||
}
|
||||
}
|
@ -6,10 +6,10 @@
|
||||
<div class="text-center">
|
||||
You have @Model.SavedTracks.Total saved tracks in your library! Here are 50 of them:
|
||||
|
||||
<ul>
|
||||
@* <ul>
|
||||
@foreach (var item in Model.SavedTracks.Items)
|
||||
{
|
||||
<li>@item.Track.Name</li>
|
||||
}
|
||||
</ul>
|
||||
</ul> *@
|
||||
</div>
|
||||
|
@ -1,75 +1,72 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using SpotifyAPI.Web.Auth;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
|
||||
namespace SpotifyAPI.Web.Examples.CLI
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static string _clientId = ""; //"";
|
||||
private static string _secretId = ""; //"";
|
||||
// private static string _clientId = ""; //"";
|
||||
// private static string _secretId = ""; //"";
|
||||
|
||||
// ReSharper disable once UnusedParameter.Local
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
_clientId = string.IsNullOrEmpty(_clientId) ?
|
||||
Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") :
|
||||
_clientId;
|
||||
// public static void Main(string[] args)
|
||||
// {
|
||||
// _clientId = string.IsNullOrEmpty(_clientId) ?
|
||||
// Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") :
|
||||
// _clientId;
|
||||
|
||||
_secretId = string.IsNullOrEmpty(_secretId) ?
|
||||
Environment.GetEnvironmentVariable("SPOTIFY_SECRET_ID") :
|
||||
_secretId;
|
||||
// _secretId = string.IsNullOrEmpty(_secretId) ?
|
||||
// Environment.GetEnvironmentVariable("SPOTIFY_SECRET_ID") :
|
||||
// _secretId;
|
||||
|
||||
Console.WriteLine("####### Spotify API Example #######");
|
||||
Console.WriteLine("This example uses AuthorizationCodeAuth.");
|
||||
Console.WriteLine(
|
||||
"Tip: If you want to supply your ClientID and SecretId beforehand, use env variables (SPOTIFY_CLIENT_ID and SPOTIFY_SECRET_ID)");
|
||||
// Console.WriteLine("####### Spotify API Example #######");
|
||||
// Console.WriteLine("This example uses AuthorizationCodeAuth.");
|
||||
// Console.WriteLine(
|
||||
// "Tip: If you want to supply your ClientID and SecretId beforehand, use env variables (SPOTIFY_CLIENT_ID and SPOTIFY_SECRET_ID)");
|
||||
|
||||
var auth =
|
||||
new AuthorizationCodeAuth(_clientId, _secretId, "http://localhost:4002", "http://localhost:4002",
|
||||
Scope.PlaylistReadPrivate | Scope.PlaylistReadCollaborative);
|
||||
auth.AuthReceived += AuthOnAuthReceived;
|
||||
auth.Start();
|
||||
auth.OpenBrowser();
|
||||
// var auth =
|
||||
// new AuthorizationCodeAuth(_clientId, _secretId, "http://localhost:4002", "http://localhost:4002",
|
||||
// Scope.PlaylistReadPrivate | Scope.PlaylistReadCollaborative);
|
||||
// auth.AuthReceived += AuthOnAuthReceived;
|
||||
// auth.Start();
|
||||
// auth.OpenBrowser();
|
||||
|
||||
Console.ReadLine();
|
||||
auth.Stop(0);
|
||||
// Console.ReadLine();
|
||||
// auth.Stop(0);
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
private static async void AuthOnAuthReceived(object sender, AuthorizationCode payload)
|
||||
{
|
||||
var auth = (AuthorizationCodeAuth) sender;
|
||||
auth.Stop();
|
||||
// private static async void AuthOnAuthReceived(object sender, AuthorizationCode payload)
|
||||
// {
|
||||
// var auth = (AuthorizationCodeAuth)sender;
|
||||
// auth.Stop();
|
||||
|
||||
Token token = await auth.ExchangeCode(payload.Code);
|
||||
var api = new SpotifyWebAPI
|
||||
{
|
||||
AccessToken = token.AccessToken,
|
||||
TokenType = token.TokenType
|
||||
};
|
||||
await PrintUsefulData(api);
|
||||
}
|
||||
// Token token = await auth.ExchangeCode(payload.Code);
|
||||
// var api = new SpotifyWebAPI
|
||||
// {
|
||||
// AccessToken = token.AccessToken,
|
||||
// TokenType = token.TokenType
|
||||
// };
|
||||
// await PrintUsefulData(api);
|
||||
// }
|
||||
|
||||
private static async Task PrintAllPlaylistTracks(SpotifyWebAPI api, Paging<SimplePlaylist> playlists)
|
||||
{
|
||||
if (playlists.Items == null) return;
|
||||
// private static async Task PrintAllPlaylistTracks(SpotifyWebAPI api, Paging<SimplePlaylist> playlists)
|
||||
// {
|
||||
// if (playlists.Items == null) return;
|
||||
|
||||
playlists.Items.ForEach(playlist => Console.WriteLine($"- {playlist.Name}"));
|
||||
if (playlists.HasNextPage())
|
||||
await PrintAllPlaylistTracks(api, await api.GetNextPageAsync(playlists));
|
||||
}
|
||||
// playlists.Items.ForEach(playlist => Console.WriteLine($"- {playlist.Name}"));
|
||||
// if (playlists.HasNextPage())
|
||||
// await PrintAllPlaylistTracks(api, await api.GetNextPageAsync(playlists));
|
||||
// }
|
||||
|
||||
private static async Task PrintUsefulData(SpotifyWebAPI api)
|
||||
{
|
||||
PrivateProfile profile = await api.GetPrivateProfileAsync();
|
||||
string name = string.IsNullOrEmpty(profile.DisplayName) ? profile.Id : profile.DisplayName;
|
||||
Console.WriteLine($"Hello there, {name}!");
|
||||
// private static async Task PrintUsefulData(SpotifyWebAPI api)
|
||||
// {
|
||||
// PrivateProfile profile = await api.GetPrivateProfileAsync();
|
||||
// string name = string.IsNullOrEmpty(profile.DisplayName) ? profile.Id : profile.DisplayName;
|
||||
// Console.WriteLine($"Hello there, {name}!");
|
||||
|
||||
Console.WriteLine("Your playlists:");
|
||||
await PrintAllPlaylistTracks(api, api.GetUserPlaylists(profile.Id));
|
||||
}
|
||||
// Console.WriteLine("Your playlists:");
|
||||
// await PrintAllPlaylistTracks(api, api.GetUserPlaylists(profile.Id));
|
||||
// }
|
||||
}
|
||||
}
|
20
SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs
Normal file
20
SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SpotifyAPI.Web.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class NetHTTPClientTest
|
||||
{
|
||||
[TestFixture]
|
||||
public class BuildRequestsMethod
|
||||
{
|
||||
public void AddsHeaders()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs
Normal file
90
SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using SpotifyAPI.Web.Http;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace SpotifyAPI.Web.Tests
|
||||
{
|
||||
public class NewtonsoftJSONSerializerTest
|
||||
{
|
||||
|
||||
public static IEnumerable<object> DontSerializeTestSource
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData(null);
|
||||
yield return new TestCaseData("string");
|
||||
yield return new TestCaseData(new MemoryStream(Encoding.UTF8.GetBytes("string")));
|
||||
yield return new TestCaseData(new StringContent("string"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(DontSerializeTestSource))]
|
||||
public void SerializeRequest_SkipsAlreadySerialized(object input)
|
||||
{
|
||||
var serializer = new NewtonsoftJSONSerializer();
|
||||
var request = new Mock<IRequest>();
|
||||
request.SetupGet(r => r.Body).Returns(input);
|
||||
|
||||
serializer.SerializeRequest(request.Object);
|
||||
|
||||
Assert.AreEqual(input, request.Object.Body);
|
||||
}
|
||||
|
||||
public static IEnumerable<object> SerializeTestSource
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData(new { Uppercase = true }, "{\"uppercase\":true}");
|
||||
yield return new TestCaseData(new { CamelCase = true }, "{\"camel_case\":true}");
|
||||
yield return new TestCaseData(new { CamelCase = true, UPPER = true }, "{\"camel_case\":true,\"upper\":true}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(SerializeTestSource))]
|
||||
public void SerializeRequest_CorrectNaming(object input, string result)
|
||||
{
|
||||
var serializer = new NewtonsoftJSONSerializer();
|
||||
var request = new Mock<IRequest>();
|
||||
request.SetupGet(r => r.Body).Returns(input);
|
||||
|
||||
serializer.SerializeRequest(request.Object);
|
||||
|
||||
request.VerifySet(r => r.Body = result);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializeResponse_SkipsNonJson()
|
||||
{
|
||||
var serializer = new NewtonsoftJSONSerializer();
|
||||
var response = new Mock<IResponse>();
|
||||
response.SetupGet(r => r.Body).Returns("hello");
|
||||
response.SetupGet(r => r.ContentType).Returns("media/mp4");
|
||||
|
||||
IAPIResponse<object> apiResonse = serializer.DeserializeResponse<object>(response.Object);
|
||||
Assert.AreEqual(apiResonse.Body, null);
|
||||
Assert.AreEqual(apiResonse.Response, response.Object);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializeResponse_HandlesJson()
|
||||
{
|
||||
var serializer = new NewtonsoftJSONSerializer();
|
||||
var response = new Mock<IResponse>();
|
||||
response.SetupGet(r => r.Body).Returns("{\"hello_world\": false}");
|
||||
response.SetupGet(r => r.ContentType).Returns("application/json");
|
||||
|
||||
IAPIResponse<TestResponseObject> apiResonse = serializer.DeserializeResponse<TestResponseObject>(response.Object);
|
||||
Assert.AreEqual(apiResonse.Body?.HelloWorld, false);
|
||||
Assert.AreEqual(apiResonse.Response, response.Object);
|
||||
}
|
||||
|
||||
public class TestResponseObject
|
||||
{
|
||||
public bool HelloWorld { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
22
SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs
Normal file
22
SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using SpotifyAPI.Web.Http;
|
||||
|
||||
namespace SpotifyAPI.Web.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TokenHeaderAuthenticatorTest
|
||||
{
|
||||
[Test]
|
||||
public void Apply_AddsCorrectHeader()
|
||||
{
|
||||
var authenticator = new TokenHeaderAuthenticator("MyToken", "Bearer");
|
||||
var request = new Mock<IRequest>();
|
||||
request.SetupGet(r => r.Headers).Returns(new Dictionary<string, string>());
|
||||
|
||||
authenticator.Apply(request.Object);
|
||||
Assert.AreEqual(request.Object.Headers["Authorization"], "Bearer MyToken");
|
||||
}
|
||||
}
|
||||
}
|
@ -2,20 +2,20 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SpotifyAPI.Web.Tests
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
[TestFixture]
|
||||
public class Test
|
||||
public class Testing
|
||||
{
|
||||
|
||||
[Test]
|
||||
public async Task Testing()
|
||||
public async Task TestingYo()
|
||||
{
|
||||
var token = "";
|
||||
var config = SpotifyClientConfig.CreateDefault("BQAODnY4uqYj_KCddlDm10KLPDZSpZhVUtMDjdh1zfG-xd5pAV3htRjnaGfO7ob92HHzNP05a-4mDnts337gdnZlRtjrDPnuWNFx75diY540H0cD1bS9UzI5cfO27N2O6lmzKb_jAYTaRoqPKHoG93KGiXxwg4vblGKSBY1vIloP");
|
||||
var spotify = new SpotifyClient(config);
|
||||
|
||||
var spotify = new SpotifyClient(token);
|
||||
|
||||
var categories = await spotify.Browse.GetCategories();
|
||||
var playlists = await spotify.Browse.GetCategoryPlaylists(categories.Categories.Items[0].Id);
|
||||
var playlists = await spotify.Browse.GetCategoryPlaylists("toplists", new CategoriesPlaylistsRequest() { Offset = 1 });
|
||||
Console.WriteLine(playlists.Playlists.Items[0].Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace SpotifyAPI.Web.Tests
|
||||
|
||||
var user = "wizzler";
|
||||
var formatter = new URIParameterFormatProvider();
|
||||
Func<FormattableString, string> func = (FormattableString str) => str.ToString(formatter);
|
||||
string func(FormattableString str) => str.ToString(formatter);
|
||||
|
||||
Assert.AreEqual(expected, func($"/users/{user}"));
|
||||
}
|
||||
@ -26,7 +26,7 @@ namespace SpotifyAPI.Web.Tests
|
||||
|
||||
var user = " wizzler";
|
||||
var formatter = new URIParameterFormatProvider();
|
||||
Func<FormattableString, string> func = (FormattableString str) => str.ToString(formatter);
|
||||
string func(FormattableString str) => str.ToString(formatter);
|
||||
|
||||
Assert.AreEqual(expected, func($"/users/{user}"));
|
||||
}
|
||||
|
@ -51,5 +51,12 @@ namespace SpotifyAPI.Web
|
||||
|
||||
return API.Get<CategoryPlaylistsResponse>(URLs.CategoryPlaylists(categoryId), request.BuildQueryParams());
|
||||
}
|
||||
|
||||
public Task<RecommendationsResponse> GetRecommendations(RecommendationsRequest request)
|
||||
{
|
||||
Ensure.ArgumentNotNull(request, nameof(request));
|
||||
|
||||
return API.Get<RecommendationsResponse>(URLs.Recommendations(), request.BuildQueryParams());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,7 @@ namespace SpotifyAPI.Web
|
||||
|
||||
Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId);
|
||||
Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request);
|
||||
|
||||
Task<RecommendationsResponse> GetRecommendations(RecommendationsRequest request);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
interface ISpotifyClient
|
||||
public interface ISpotifyClient
|
||||
{
|
||||
IUserProfileClient UserProfile { get; }
|
||||
|
||||
|
@ -4,21 +4,17 @@ namespace SpotifyAPI.Web
|
||||
{
|
||||
public class SpotifyClient : ISpotifyClient
|
||||
{
|
||||
private IAPIConnector _apiConnector;
|
||||
private readonly IAPIConnector _apiConnector;
|
||||
|
||||
public SpotifyClient(string token, string tokenType = "Bearer") :
|
||||
this(new TokenHeaderAuthenticator(token, tokenType))
|
||||
this(SpotifyClientConfig.CreateDefault(token, tokenType))
|
||||
{ }
|
||||
|
||||
public SpotifyClient(IAuthenticator authenticator) :
|
||||
this(new APIConnector(SpotifyUrls.API_V1, authenticator))
|
||||
{ }
|
||||
|
||||
public SpotifyClient(IAPIConnector apiConnector)
|
||||
public SpotifyClient(SpotifyClientConfig config)
|
||||
{
|
||||
Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector));
|
||||
Ensure.ArgumentNotNull(config, nameof(config));
|
||||
|
||||
_apiConnector = apiConnector;
|
||||
_apiConnector = config.CreateAPIConnector();
|
||||
UserProfile = new UserProfileClient(_apiConnector);
|
||||
Browse = new BrowseClient(_apiConnector);
|
||||
}
|
||||
|
86
SpotifyAPI.Web/Clients/SpotifyClientConfig.cs
Normal file
86
SpotifyAPI.Web/Clients/SpotifyClientConfig.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using SpotifyAPI.Web.Http;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class SpotifyClientConfig
|
||||
{
|
||||
public Uri BaseAddress { get; }
|
||||
public IAuthenticator Authenticator { get; }
|
||||
public IJSONSerializer JSONSerializer { get; }
|
||||
public IHTTPClient HTTPClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This config spefies the internal parts of the SpotifyClient.
|
||||
/// In apps where multiple different access tokens are used, one should create a default config and then use
|
||||
/// <see cref="WithToken" /> or <see cref="WithAuthenticator" /> to specify the auth details.
|
||||
/// </summary>
|
||||
/// <param name="baseAddress"></param>
|
||||
/// <param name="authenticator"></param>
|
||||
/// <param name="jsonSerializer"></param>
|
||||
/// <param name="httpClient"></param>
|
||||
public SpotifyClientConfig(
|
||||
Uri baseAddress,
|
||||
IAuthenticator authenticator,
|
||||
IJSONSerializer jsonSerializer,
|
||||
IHTTPClient httpClient
|
||||
)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
Authenticator = authenticator;
|
||||
JSONSerializer = jsonSerializer;
|
||||
HTTPClient = httpClient;
|
||||
}
|
||||
|
||||
internal IAPIConnector CreateAPIConnector()
|
||||
{
|
||||
Ensure.PropertyNotNull(BaseAddress, nameof(BaseAddress));
|
||||
Ensure.PropertyNotNull(Authenticator, nameof(Authenticator),
|
||||
". Use WithToken or WithAuthenticator to specify a authentication");
|
||||
Ensure.PropertyNotNull(JSONSerializer, nameof(JSONSerializer));
|
||||
Ensure.PropertyNotNull(HTTPClient, nameof(HTTPClient));
|
||||
|
||||
return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient);
|
||||
}
|
||||
|
||||
public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer")
|
||||
{
|
||||
Ensure.ArgumentNotNull(token, nameof(token));
|
||||
|
||||
return WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType));
|
||||
}
|
||||
|
||||
public SpotifyClientConfig WithAuthenticator(IAuthenticator authenticator)
|
||||
{
|
||||
Ensure.ArgumentNotNull(authenticator, nameof(authenticator));
|
||||
|
||||
return new SpotifyClientConfig(BaseAddress, Authenticator, JSONSerializer, HTTPClient);
|
||||
}
|
||||
|
||||
public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer")
|
||||
{
|
||||
Ensure.ArgumentNotNull(token, nameof(token));
|
||||
|
||||
return CreateDefault(new TokenHeaderAuthenticator(token, tokenType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default configuration, which is not useable without calling <see cref="WithToken" /> or
|
||||
/// <see cref="WithAuthenticator" />
|
||||
/// </summary>
|
||||
public static SpotifyClientConfig CreateDefault()
|
||||
{
|
||||
return CreateDefault(null);
|
||||
}
|
||||
|
||||
public static SpotifyClientConfig CreateDefault(IAuthenticator authenticator)
|
||||
{
|
||||
return new SpotifyClientConfig(
|
||||
SpotifyUrls.API_V1,
|
||||
authenticator,
|
||||
new NewtonsoftJSONSerializer(),
|
||||
new NetHttpClient()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,10 +8,10 @@ namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
public class APIConnector : IAPIConnector
|
||||
{
|
||||
private Uri _baseAddress;
|
||||
private IAuthenticator _authenticator;
|
||||
private IJSONSerializer _jsonSerializer;
|
||||
private IHTTPClient _httpClient;
|
||||
private readonly Uri _baseAddress;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IJSONSerializer _jsonSerializer;
|
||||
private readonly IHTTPClient _httpClient;
|
||||
|
||||
public APIConnector(Uri baseAddress, IAuthenticator authenticator) :
|
||||
this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient())
|
||||
@ -119,10 +119,10 @@ namespace SpotifyAPI.Web.Http
|
||||
var request = new Request
|
||||
{
|
||||
BaseAddress = _baseAddress,
|
||||
ContentType = "application/json",
|
||||
Parameters = parameters,
|
||||
Endpoint = uri,
|
||||
Method = method
|
||||
Method = method,
|
||||
Body = body
|
||||
};
|
||||
|
||||
_jsonSerializer.SerializeRequest(request);
|
||||
@ -143,13 +143,11 @@ namespace SpotifyAPI.Web.Http
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.StatusCode)
|
||||
throw response.StatusCode switch
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new APIUnauthorizedException(response);
|
||||
default:
|
||||
throw new APIException(response);
|
||||
}
|
||||
HttpStatusCode.Unauthorized => new APIUnauthorizedException(response),
|
||||
_ => new APIException(response),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
public class APIResponse<T> : IAPIResponse<T>
|
||||
{
|
||||
public APIResponse(IResponse response, T body = default(T))
|
||||
public APIResponse(IResponse response, T body = default)
|
||||
{
|
||||
Ensure.ArgumentNotNull(response, nameof(response));
|
||||
|
||||
|
@ -16,8 +16,6 @@ namespace SpotifyAPI.Web.Http
|
||||
|
||||
HttpMethod Method { get; }
|
||||
|
||||
string ContentType { get; }
|
||||
|
||||
object Body { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
public class NetHttpClient : IHTTPClient
|
||||
{
|
||||
private HttpClient _httpClient;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public NetHttpClient()
|
||||
{
|
||||
@ -19,14 +19,12 @@ namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
Ensure.ArgumentNotNull(request, nameof(request));
|
||||
|
||||
using (HttpRequestMessage requestMsg = BuildRequestMessage(request))
|
||||
{
|
||||
var responseMsg = await _httpClient
|
||||
.SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead)
|
||||
.ConfigureAwait(false);
|
||||
using HttpRequestMessage requestMsg = BuildRequestMessage(request);
|
||||
var responseMsg = await _httpClient
|
||||
.SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await BuildResponse(responseMsg).ConfigureAwait(false);
|
||||
}
|
||||
return await BuildResponse(responseMsg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IResponse> BuildResponse(HttpResponseMessage responseMsg)
|
||||
@ -34,19 +32,17 @@ namespace SpotifyAPI.Web.Http
|
||||
Ensure.ArgumentNotNull(responseMsg, nameof(responseMsg));
|
||||
|
||||
// We only support text stuff for now
|
||||
using (var content = responseMsg.Content)
|
||||
{
|
||||
var headers = responseMsg.Headers.ToDictionary(header => header.Key, header => header.Value.First());
|
||||
var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var contentType = content.Headers?.ContentType?.MediaType;
|
||||
using var content = responseMsg.Content;
|
||||
var headers = responseMsg.Headers.ToDictionary(header => header.Key, header => header.Value.First());
|
||||
var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var contentType = content.Headers?.ContentType?.MediaType;
|
||||
|
||||
return new Response(headers)
|
||||
{
|
||||
ContentType = contentType,
|
||||
StatusCode = responseMsg.StatusCode,
|
||||
Body = body
|
||||
};
|
||||
}
|
||||
return new Response(headers)
|
||||
{
|
||||
ContentType = contentType,
|
||||
StatusCode = responseMsg.StatusCode,
|
||||
Body = body
|
||||
};
|
||||
}
|
||||
|
||||
private HttpRequestMessage BuildRequestMessage(IRequest request)
|
||||
|
@ -9,7 +9,7 @@ namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
public class NewtonsoftJSONSerializer : IJSONSerializer
|
||||
{
|
||||
JsonSerializerSettings _serializerSettings;
|
||||
private readonly JsonSerializerSettings _serializerSettings;
|
||||
|
||||
public NewtonsoftJSONSerializer()
|
||||
{
|
||||
|
@ -4,7 +4,7 @@ using System.Net.Http;
|
||||
|
||||
namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
class Request : IRequest
|
||||
public class Request : IRequest
|
||||
{
|
||||
public Request()
|
||||
{
|
||||
@ -22,8 +22,6 @@ namespace SpotifyAPI.Web.Http
|
||||
|
||||
public HttpMethod Method { get; set; }
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public object Body { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpotifyAPI.Web.Http
|
||||
{
|
||||
class TokenHeaderAuthenticator : IAuthenticator
|
||||
public class TokenHeaderAuthenticator : IAuthenticator
|
||||
{
|
||||
public TokenHeaderAuthenticator(string token, string tokenType)
|
||||
{
|
||||
|
58
SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs
Normal file
58
SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class RecommendationsRequest : RequestParams
|
||||
{
|
||||
public RecommendationsRequest()
|
||||
{
|
||||
Min = new Dictionary<string, string>();
|
||||
Max = new Dictionary<string, string>();
|
||||
Target = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
[QueryParam("seed_artists")]
|
||||
public string SeedArtists { get; set; }
|
||||
|
||||
[QueryParam("seed_genres")]
|
||||
public string SeedGenres { get; set; }
|
||||
|
||||
[QueryParam("seed_tracks")]
|
||||
public string SeedTracks { get; set; }
|
||||
|
||||
[QueryParam("limit")]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
[QueryParam("market")]
|
||||
public string Market { get; set; }
|
||||
|
||||
public Dictionary<string, string> Min { get; set; }
|
||||
public Dictionary<string, string> Max { get; set; }
|
||||
public Dictionary<string, string> Target { get; set; }
|
||||
|
||||
protected override void Ensure()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SeedTracks) && string.IsNullOrEmpty(SeedGenres) && string.IsNullOrEmpty(SeedArtists))
|
||||
{
|
||||
throw new ArgumentException("At least one of the seeds has to be non-empty");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AddCustomQueryParams(System.Collections.Generic.Dictionary<string, string> queryParams)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> pair in Min)
|
||||
{
|
||||
queryParams.Add($"min_{pair.Key}", pair.Value);
|
||||
}
|
||||
foreach (KeyValuePair<string, string> pair in Min)
|
||||
{
|
||||
queryParams.Add($"max_{pair.Key}", pair.Value);
|
||||
}
|
||||
foreach (KeyValuePair<string, string> pair in Min)
|
||||
{
|
||||
queryParams.Add($"target_{pair.Key}", pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,22 +8,30 @@ namespace SpotifyAPI.Web
|
||||
{
|
||||
public Dictionary<string, string> BuildQueryParams()
|
||||
{
|
||||
var queryProps = this.GetType().GetProperties()
|
||||
// Make sure everything is okay before building query params
|
||||
Ensure();
|
||||
|
||||
var queryProps = GetType().GetProperties()
|
||||
.Where(prop => prop.GetCustomAttributes(typeof(QueryParamAttribute), true).Length > 0);
|
||||
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
foreach (var prop in queryProps)
|
||||
{
|
||||
var attribute = prop.GetCustomAttribute(typeof(QueryParamAttribute)) as QueryParamAttribute;
|
||||
var value = prop.GetValue(this);
|
||||
object value = prop.GetValue(this);
|
||||
if (value != null)
|
||||
{
|
||||
queryParams.Add(attribute.Key ?? prop.Name, value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
AddCustomQueryParams(queryParams);
|
||||
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
protected virtual void Ensure() { }
|
||||
protected virtual void AddCustomQueryParams(Dictionary<string, string> queryParams) { }
|
||||
}
|
||||
|
||||
public class QueryParamAttribute : Attribute
|
||||
|
@ -19,7 +19,7 @@ namespace SpotifyAPI.Web
|
||||
public string ReleaseDatePrecision { get; set; }
|
||||
public ResumePoint ResumePoint { get; set; }
|
||||
public SimpleShow Show { get; set; }
|
||||
public PlaylistElementType Type { get; set; }
|
||||
public ElementType Type { get; set; }
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace SpotifyAPI.Web
|
||||
public int Popularity { get; set; }
|
||||
public string PreviewUrl { get; set; }
|
||||
public int TrackNumber { get; set; }
|
||||
public PlaylistElementType Type { get; set; }
|
||||
public ElementType Type { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public bool IsLocal { get; set; }
|
||||
}
|
||||
|
@ -3,14 +3,15 @@ using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public enum PlaylistElementType
|
||||
public enum ElementType
|
||||
{
|
||||
Track,
|
||||
Episode
|
||||
}
|
||||
|
||||
public interface IPlaylistElement
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PlaylistElementType Type { get; set; }
|
||||
public ElementType Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,6 @@ namespace SpotifyAPI.Web
|
||||
public string Href { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string uri { get; set; }
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class PlaylistTrack
|
||||
@ -6,6 +8,7 @@ namespace SpotifyAPI.Web
|
||||
public DateTime? AddedAt { get; set; }
|
||||
public PublicUser AddedBy { get; set; }
|
||||
public bool IsLocal { get; set; }
|
||||
[JsonConverter(typeof(PlaylistElementConverter))]
|
||||
public IPlaylistElement Track { get; set; }
|
||||
}
|
||||
}
|
||||
|
18
SpotifyAPI.Web/Models/Response/RecommendationSeed.cs
Normal file
18
SpotifyAPI.Web/Models/Response/RecommendationSeed.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class RecommendationSeed
|
||||
{
|
||||
[JsonProperty("afterFilteringSize")]
|
||||
public int AfterFiliteringSize { get; set; }
|
||||
|
||||
[JsonProperty("afterRelinkingSize")]
|
||||
public int AfterRelinkingSize { get; set; }
|
||||
public string Href { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty("initialPoolSize")]
|
||||
public int InitialPoolSize { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
10
SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs
Normal file
10
SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class RecommendationsResponse
|
||||
{
|
||||
public List<RecommendationSeed> Seeds { get; set; }
|
||||
public List<SimpleTrack> Tracks { get; set; }
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ namespace SpotifyAPI.Web
|
||||
public string MediaType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public string Type { get; set; }
|
||||
public ElementType Type { get; set; }
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
}
|
||||
|
23
SpotifyAPI.Web/Models/Response/SimpleTrack.cs
Normal file
23
SpotifyAPI.Web/Models/Response/SimpleTrack.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class SimpleTrack
|
||||
{
|
||||
public List<SimpleArtist> Artists { get; set; }
|
||||
public List<string> AvailableMarkets { get; set; }
|
||||
public int DiscNumber { get; set; }
|
||||
public int DurationMs { get; set; }
|
||||
public bool Explicit { get; set; }
|
||||
public Dictionary<string, string> ExternalUrls { get; set; }
|
||||
public string Href { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool IsPlayable { get; set; }
|
||||
public LinkedTrack LinkedFrom { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string PreviewUrl { get; set; }
|
||||
public int TrackNumber { get; set; }
|
||||
public ElementType Type { get; set; }
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
}
|
@ -57,7 +57,9 @@ namespace SpotifyAPI.Web
|
||||
public WebProxy CreateWebProxy()
|
||||
{
|
||||
if (!IsValid())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
WebProxy proxy = new WebProxy
|
||||
{
|
||||
@ -67,7 +69,9 @@ namespace SpotifyAPI.Web
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
|
||||
{
|
||||
return proxy;
|
||||
}
|
||||
|
||||
proxy.UseDefaultCredentials = false;
|
||||
proxy.Credentials = new NetworkCredential(Username, Password);
|
||||
@ -79,12 +83,16 @@ namespace SpotifyAPI.Web
|
||||
{
|
||||
HttpClientHandler clientHandler = new HttpClientHandler
|
||||
{
|
||||
PreAuthenticate = false,
|
||||
UseDefaultCredentials = true,
|
||||
UseProxy = false
|
||||
PreAuthenticate = false,
|
||||
UseDefaultCredentials = true,
|
||||
UseProxy = false
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(proxyConfig?.Host)) return clientHandler;
|
||||
if (string.IsNullOrWhiteSpace(proxyConfig?.Host))
|
||||
{
|
||||
return clientHandler;
|
||||
}
|
||||
|
||||
WebProxy proxy = proxyConfig.CreateWebProxy();
|
||||
clientHandler.UseProxy = true;
|
||||
clientHandler.Proxy = proxy;
|
||||
|
@ -3,21 +3,22 @@ namespace SpotifyAPI.Web
|
||||
{
|
||||
public static class SpotifyUrls
|
||||
{
|
||||
static URIParameterFormatProvider _provider = new URIParameterFormatProvider();
|
||||
static private readonly URIParameterFormatProvider _provider = new URIParameterFormatProvider();
|
||||
|
||||
public static Uri API_V1 = new Uri("https://api.spotify.com/v1/");
|
||||
|
||||
public static Uri Me() => _Uri("me");
|
||||
public static Uri Me() => EUri($"me");
|
||||
|
||||
public static Uri User(string userId) => _Uri($"users/{userId}");
|
||||
public static Uri User(string userId) => EUri($"users/{userId}");
|
||||
|
||||
public static Uri Categories() => _Uri("browse/categories");
|
||||
public static Uri Categories() => EUri($"browse/categories");
|
||||
|
||||
public static Uri Category(string categoryId) => _Uri($"browse/categories/{categoryId}");
|
||||
public static Uri Category(string categoryId) => EUri($"browse/categories/{categoryId}");
|
||||
|
||||
public static Uri CategoryPlaylists(string categoryId) => _Uri($"browse/categories/{categoryId}/playlists");
|
||||
public static Uri CategoryPlaylists(string categoryId) => EUri($"browse/categories/{categoryId}/playlists");
|
||||
|
||||
private static Uri _Uri(FormattableString path) => new Uri(path.ToString(_provider), UriKind.Relative);
|
||||
private static Uri _Uri(string path) => new Uri(path, UriKind.Relative);
|
||||
public static Uri Recommendations() => EUri($"recommendations");
|
||||
|
||||
private static Uri EUri(FormattableString path) => new Uri(path.ToString(_provider), UriKind.Relative);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ namespace SpotifyAPI.Web
|
||||
/// <param name = "name">The name of the argument</param>
|
||||
public static void ArgumentNotNull(object value, string name)
|
||||
{
|
||||
if (value != null) return;
|
||||
if (value != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentNullException(name);
|
||||
}
|
||||
@ -26,9 +29,22 @@ namespace SpotifyAPI.Web
|
||||
/// <param name = "name">The name of the argument</param>
|
||||
public static void ArgumentNotNullOrEmptyString(string value, string name)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value)) return;
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentException("String is empty or null", name);
|
||||
}
|
||||
|
||||
public static void PropertyNotNull(object value, string name, string additional = null)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"The property {name} is null{additional}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,10 @@ namespace SpotifyAPI.Web
|
||||
var queryString = String.Join("&", newParameters.Select((parameter) => $"{parameter.Key}={parameter.Value}"));
|
||||
var query = string.IsNullOrEmpty(queryString) ? null : $"?{queryString}";
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Query = query;
|
||||
var uriBuilder = new UriBuilder(uri)
|
||||
{
|
||||
Query = query
|
||||
};
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
@ -13,12 +13,10 @@ namespace SpotifyAPI.Web
|
||||
|
||||
public object GetFormat(Type formatType)
|
||||
{
|
||||
if (formatType == typeof(ICustomFormatter))
|
||||
return _formatter;
|
||||
return null;
|
||||
return formatType == typeof(ICustomFormatter) ? _formatter : null;
|
||||
}
|
||||
|
||||
class URIParameterFormatter : ICustomFormatter
|
||||
public class URIParameterFormatter : ICustomFormatter
|
||||
{
|
||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
||||
{
|
||||
|
8
omnisharp.json
Normal file
8
omnisharp.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"RoslynExtensionsOptions": {
|
||||
"enableAnalyzersSupport": true
|
||||
},
|
||||
"FormattingOptions": {
|
||||
"enableEditorConfigSupport": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user