Started with Unit Tests and added .editorconfig for syntax settings

Disabled examples for now
This commit is contained in:
Jonas Dellinger 2020-05-02 13:04:26 +02:00
parent 2c4463529b
commit 9f6729ad60
47 changed files with 550 additions and 1027 deletions

28
.editorconfig Normal file
View 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

View File

@ -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;
}
}
}
}

View File

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

View File

@ -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);
}
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
});
}
}
}

View File

@ -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)]

View File

@ -5,6 +5,6 @@ namespace SpotifyAPI.Web.Examples.ASP.Models
{
public class IndexModel
{
public Paging<SavedTrack> SavedTracks;
public Paging<object> SavedTracks;
}
}

View File

@ -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>

View File

@ -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));
// }
}
}

View 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()
{
}
}
}
}

View 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; }
}
}
}

View 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");
}
}
}

View File

@ -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);
}
}
}

View File

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

View File

@ -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());
}
}
}

View File

@ -12,5 +12,7 @@ namespace SpotifyAPI.Web
Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId);
Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request);
Task<RecommendationsResponse> GetRecommendations(RecommendationsRequest request);
}
}

View File

@ -1,6 +1,6 @@
namespace SpotifyAPI.Web
{
interface ISpotifyClient
public interface ISpotifyClient
{
IUserProfileClient UserProfile { get; }

View File

@ -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);
}

View 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()
);
}
}
}

View File

@ -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),
};
}
}
}

View File

@ -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));

View File

@ -16,8 +16,6 @@ namespace SpotifyAPI.Web.Http
HttpMethod Method { get; }
string ContentType { get; }
object Body { get; set; }
}
}

View File

@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http
{
public class NetHttpClient : IHTTPClient
{
private HttpClient _httpClient;
private readonly HttpClient _httpClient;
public NetHttpClient()
{
@ -19,23 +19,20 @@ namespace SpotifyAPI.Web.Http
{
Ensure.ArgumentNotNull(request, nameof(request));
using (HttpRequestMessage requestMsg = BuildRequestMessage(request))
{
using HttpRequestMessage requestMsg = BuildRequestMessage(request);
var responseMsg = await _httpClient
.SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead)
.ConfigureAwait(false);
return await BuildResponse(responseMsg).ConfigureAwait(false);
}
}
private async Task<IResponse> BuildResponse(HttpResponseMessage responseMsg)
{
Ensure.ArgumentNotNull(responseMsg, nameof(responseMsg));
// We only support text stuff for now
using (var content = responseMsg.Content)
{
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;
@ -47,7 +44,6 @@ namespace SpotifyAPI.Web.Http
Body = body
};
}
}
private HttpRequestMessage BuildRequestMessage(IRequest request)
{

View File

@ -9,7 +9,7 @@ namespace SpotifyAPI.Web.Http
{
public class NewtonsoftJSONSerializer : IJSONSerializer
{
JsonSerializerSettings _serializerSettings;
private readonly JsonSerializerSettings _serializerSettings;
public NewtonsoftJSONSerializer()
{

View File

@ -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; }
}
}

View File

@ -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)
{

View 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);
}
}
}
}

View File

@ -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

View File

@ -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; }
}
}

View File

@ -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; }
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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; }
}
}

View 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; }
}
}

View File

@ -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);
@ -84,7 +88,11 @@ namespace SpotifyAPI.Web
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;

View File

@ -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);
}
}

View File

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

View File

@ -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;
}

View File

@ -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
View File

@ -0,0 +1,8 @@
{
"RoslynExtensionsOptions": {
"enableAnalyzersSupport": true
},
"FormattingOptions": {
"enableEditorConfigSupport": true
}
}