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")] [Authorize(AuthenticationSchemes = "Spotify")]
public class HomeController : Controller public class HomeController : Controller
{ {
public async Task<IActionResult> Index() public IActionResult Index()
{ {
var accessToken = await HttpContext.GetTokenAsync("Spotify", "access_token"); // var accessToken = await HttpContext.GetTokenAsync("Spotify", "access_token");
SpotifyWebAPI api = new SpotifyWebAPI // SpotifyWebAPI api = new SpotifyWebAPI
{ // {
AccessToken = accessToken, // AccessToken = accessToken,
TokenType = "Bearer" // 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)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

View File

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

View File

@ -6,10 +6,10 @@
<div class="text-center"> <div class="text-center">
You have @Model.SavedTracks.Total saved tracks in your library! Here are 50 of them: You have @Model.SavedTracks.Total saved tracks in your library! Here are 50 of them:
<ul> @* <ul>
@foreach (var item in Model.SavedTracks.Items) @foreach (var item in Model.SavedTracks.Items)
{ {
<li>@item.Track.Name</li> <li>@item.Track.Name</li>
} }
</ul> </ul> *@
</div> </div>

View File

@ -1,75 +1,72 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpotifyAPI.Web.Auth;
using SpotifyAPI.Web.Enums;
using SpotifyAPI.Web.Models;
namespace SpotifyAPI.Web.Examples.CLI namespace SpotifyAPI.Web.Examples.CLI
{ {
internal static class Program internal static class Program
{ {
private static string _clientId = ""; //""; // private static string _clientId = ""; //"";
private static string _secretId = ""; //""; // private static string _secretId = ""; //"";
// ReSharper disable once UnusedParameter.Local // ReSharper disable once UnusedParameter.Local
public static void Main(string[] args) // public static void Main(string[] args)
{ // {
_clientId = string.IsNullOrEmpty(_clientId) ? // _clientId = string.IsNullOrEmpty(_clientId) ?
Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") : // Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") :
_clientId; // _clientId;
_secretId = string.IsNullOrEmpty(_secretId) ? // _secretId = string.IsNullOrEmpty(_secretId) ?
Environment.GetEnvironmentVariable("SPOTIFY_SECRET_ID") : // Environment.GetEnvironmentVariable("SPOTIFY_SECRET_ID") :
_secretId; // _secretId;
Console.WriteLine("####### Spotify API Example #######"); // Console.WriteLine("####### Spotify API Example #######");
Console.WriteLine("This example uses AuthorizationCodeAuth."); // Console.WriteLine("This example uses AuthorizationCodeAuth.");
Console.WriteLine( // Console.WriteLine(
"Tip: If you want to supply your ClientID and SecretId beforehand, use env variables (SPOTIFY_CLIENT_ID and SPOTIFY_SECRET_ID)"); // "Tip: If you want to supply your ClientID and SecretId beforehand, use env variables (SPOTIFY_CLIENT_ID and SPOTIFY_SECRET_ID)");
var auth = // var auth =
new AuthorizationCodeAuth(_clientId, _secretId, "http://localhost:4002", "http://localhost:4002", // new AuthorizationCodeAuth(_clientId, _secretId, "http://localhost:4002", "http://localhost:4002",
Scope.PlaylistReadPrivate | Scope.PlaylistReadCollaborative); // Scope.PlaylistReadPrivate | Scope.PlaylistReadCollaborative);
auth.AuthReceived += AuthOnAuthReceived; // auth.AuthReceived += AuthOnAuthReceived;
auth.Start(); // auth.Start();
auth.OpenBrowser(); // auth.OpenBrowser();
Console.ReadLine(); // Console.ReadLine();
auth.Stop(0); // auth.Stop(0);
} // }
private static async void AuthOnAuthReceived(object sender, AuthorizationCode payload) // private static async void AuthOnAuthReceived(object sender, AuthorizationCode payload)
{ // {
var auth = (AuthorizationCodeAuth) sender; // var auth = (AuthorizationCodeAuth)sender;
auth.Stop(); // auth.Stop();
Token token = await auth.ExchangeCode(payload.Code); // Token token = await auth.ExchangeCode(payload.Code);
var api = new SpotifyWebAPI // var api = new SpotifyWebAPI
{ // {
AccessToken = token.AccessToken, // AccessToken = token.AccessToken,
TokenType = token.TokenType // TokenType = token.TokenType
}; // };
await PrintUsefulData(api); // await PrintUsefulData(api);
} // }
private static async Task PrintAllPlaylistTracks(SpotifyWebAPI api, Paging<SimplePlaylist> playlists) // private static async Task PrintAllPlaylistTracks(SpotifyWebAPI api, Paging<SimplePlaylist> playlists)
{ // {
if (playlists.Items == null) return; // if (playlists.Items == null) return;
playlists.Items.ForEach(playlist => Console.WriteLine($"- {playlist.Name}")); // playlists.Items.ForEach(playlist => Console.WriteLine($"- {playlist.Name}"));
if (playlists.HasNextPage()) // if (playlists.HasNextPage())
await PrintAllPlaylistTracks(api, await api.GetNextPageAsync(playlists)); // await PrintAllPlaylistTracks(api, await api.GetNextPageAsync(playlists));
} // }
private static async Task PrintUsefulData(SpotifyWebAPI api) // private static async Task PrintUsefulData(SpotifyWebAPI api)
{ // {
PrivateProfile profile = await api.GetPrivateProfileAsync(); // PrivateProfile profile = await api.GetPrivateProfileAsync();
string name = string.IsNullOrEmpty(profile.DisplayName) ? profile.Id : profile.DisplayName; // string name = string.IsNullOrEmpty(profile.DisplayName) ? profile.Id : profile.DisplayName;
Console.WriteLine($"Hello there, {name}!"); // Console.WriteLine($"Hello there, {name}!");
Console.WriteLine("Your playlists:"); // Console.WriteLine("Your playlists:");
await PrintAllPlaylistTracks(api, api.GetUserPlaylists(profile.Id)); // 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 System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
namespace SpotifyAPI.Web.Tests namespace SpotifyAPI.Web
{ {
[TestFixture] [TestFixture]
public class Test public class Testing
{ {
[Test] [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 playlists = await spotify.Browse.GetCategoryPlaylists("toplists", new CategoriesPlaylistsRequest() { Offset = 1 });
Console.WriteLine(playlists.Playlists.Items[0].Name);
var categories = await spotify.Browse.GetCategories();
var playlists = await spotify.Browse.GetCategoryPlaylists(categories.Categories.Items[0].Id);
} }
} }
} }

View File

@ -14,7 +14,7 @@ namespace SpotifyAPI.Web.Tests
var user = "wizzler"; var user = "wizzler";
var formatter = new URIParameterFormatProvider(); 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}")); Assert.AreEqual(expected, func($"/users/{user}"));
} }
@ -26,7 +26,7 @@ namespace SpotifyAPI.Web.Tests
var user = " wizzler"; var user = " wizzler";
var formatter = new URIParameterFormatProvider(); 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}")); Assert.AreEqual(expected, func($"/users/{user}"));
} }

View File

@ -51,5 +51,12 @@ namespace SpotifyAPI.Web
return API.Get<CategoryPlaylistsResponse>(URLs.CategoryPlaylists(categoryId), request.BuildQueryParams()); 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);
Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request); Task<CategoryPlaylistsResponse> GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request);
Task<RecommendationsResponse> GetRecommendations(RecommendationsRequest request);
} }
} }

View File

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

View File

@ -4,21 +4,17 @@ namespace SpotifyAPI.Web
{ {
public class SpotifyClient : ISpotifyClient public class SpotifyClient : ISpotifyClient
{ {
private IAPIConnector _apiConnector; private readonly IAPIConnector _apiConnector;
public SpotifyClient(string token, string tokenType = "Bearer") : public SpotifyClient(string token, string tokenType = "Bearer") :
this(new TokenHeaderAuthenticator(token, tokenType)) this(SpotifyClientConfig.CreateDefault(token, tokenType))
{ } { }
public SpotifyClient(IAuthenticator authenticator) : public SpotifyClient(SpotifyClientConfig config)
this(new APIConnector(SpotifyUrls.API_V1, authenticator))
{ }
public SpotifyClient(IAPIConnector apiConnector)
{ {
Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); Ensure.ArgumentNotNull(config, nameof(config));
_apiConnector = apiConnector; _apiConnector = config.CreateAPIConnector();
UserProfile = new UserProfileClient(_apiConnector); UserProfile = new UserProfileClient(_apiConnector);
Browse = new BrowseClient(_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 public class APIConnector : IAPIConnector
{ {
private Uri _baseAddress; private readonly Uri _baseAddress;
private IAuthenticator _authenticator; private readonly IAuthenticator _authenticator;
private IJSONSerializer _jsonSerializer; private readonly IJSONSerializer _jsonSerializer;
private IHTTPClient _httpClient; private readonly IHTTPClient _httpClient;
public APIConnector(Uri baseAddress, IAuthenticator authenticator) : public APIConnector(Uri baseAddress, IAuthenticator authenticator) :
this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient()) this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient())
@ -119,10 +119,10 @@ namespace SpotifyAPI.Web.Http
var request = new Request var request = new Request
{ {
BaseAddress = _baseAddress, BaseAddress = _baseAddress,
ContentType = "application/json",
Parameters = parameters, Parameters = parameters,
Endpoint = uri, Endpoint = uri,
Method = method Method = method,
Body = body
}; };
_jsonSerializer.SerializeRequest(request); _jsonSerializer.SerializeRequest(request);
@ -143,13 +143,11 @@ namespace SpotifyAPI.Web.Http
return; return;
} }
switch (response.StatusCode) throw response.StatusCode switch
{ {
case HttpStatusCode.Unauthorized: HttpStatusCode.Unauthorized => new APIUnauthorizedException(response),
throw new APIUnauthorizedException(response); _ => new APIException(response),
default: };
throw new APIException(response);
}
} }
} }
} }

View File

@ -2,7 +2,7 @@ namespace SpotifyAPI.Web.Http
{ {
public class APIResponse<T> : IAPIResponse<T> 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)); Ensure.ArgumentNotNull(response, nameof(response));

View File

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

View File

@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http
{ {
public class NetHttpClient : IHTTPClient public class NetHttpClient : IHTTPClient
{ {
private HttpClient _httpClient; private readonly HttpClient _httpClient;
public NetHttpClient() public NetHttpClient()
{ {
@ -19,14 +19,12 @@ namespace SpotifyAPI.Web.Http
{ {
Ensure.ArgumentNotNull(request, nameof(request)); Ensure.ArgumentNotNull(request, nameof(request));
using (HttpRequestMessage requestMsg = BuildRequestMessage(request)) using HttpRequestMessage requestMsg = BuildRequestMessage(request);
{ var responseMsg = await _httpClient
var responseMsg = await _httpClient .SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead)
.SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead) .ConfigureAwait(false);
.ConfigureAwait(false);
return await BuildResponse(responseMsg).ConfigureAwait(false); return await BuildResponse(responseMsg).ConfigureAwait(false);
}
} }
private async Task<IResponse> BuildResponse(HttpResponseMessage responseMsg) private async Task<IResponse> BuildResponse(HttpResponseMessage responseMsg)
@ -34,19 +32,17 @@ namespace SpotifyAPI.Web.Http
Ensure.ArgumentNotNull(responseMsg, nameof(responseMsg)); Ensure.ArgumentNotNull(responseMsg, nameof(responseMsg));
// We only support text stuff for now // 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 headers = responseMsg.Headers.ToDictionary(header => header.Key, header => header.Value.First()); var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false);
var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false); var contentType = content.Headers?.ContentType?.MediaType;
var contentType = content.Headers?.ContentType?.MediaType;
return new Response(headers) return new Response(headers)
{ {
ContentType = contentType, ContentType = contentType,
StatusCode = responseMsg.StatusCode, StatusCode = responseMsg.StatusCode,
Body = body Body = body
}; };
}
} }
private HttpRequestMessage BuildRequestMessage(IRequest request) private HttpRequestMessage BuildRequestMessage(IRequest request)

View File

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

View File

@ -4,7 +4,7 @@ using System.Net.Http;
namespace SpotifyAPI.Web.Http namespace SpotifyAPI.Web.Http
{ {
class Request : IRequest public class Request : IRequest
{ {
public Request() public Request()
{ {
@ -22,8 +22,6 @@ namespace SpotifyAPI.Web.Http
public HttpMethod Method { get; set; } public HttpMethod Method { get; set; }
public string ContentType { get; set; }
public object Body { get; set; } public object Body { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@ using System.Threading.Tasks;
namespace SpotifyAPI.Web.Http namespace SpotifyAPI.Web.Http
{ {
class TokenHeaderAuthenticator : IAuthenticator public class TokenHeaderAuthenticator : IAuthenticator
{ {
public TokenHeaderAuthenticator(string token, string tokenType) 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() 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); .Where(prop => prop.GetCustomAttributes(typeof(QueryParamAttribute), true).Length > 0);
var queryParams = new Dictionary<string, string>(); var queryParams = new Dictionary<string, string>();
foreach (var prop in queryProps) foreach (var prop in queryProps)
{ {
var attribute = prop.GetCustomAttribute(typeof(QueryParamAttribute)) as QueryParamAttribute; var attribute = prop.GetCustomAttribute(typeof(QueryParamAttribute)) as QueryParamAttribute;
var value = prop.GetValue(this); object value = prop.GetValue(this);
if (value != null) if (value != null)
{ {
queryParams.Add(attribute.Key ?? prop.Name, value.ToString()); queryParams.Add(attribute.Key ?? prop.Name, value.ToString());
} }
} }
AddCustomQueryParams(queryParams);
return queryParams; return queryParams;
} }
protected virtual void Ensure() { }
protected virtual void AddCustomQueryParams(Dictionary<string, string> queryParams) { }
} }
public class QueryParamAttribute : Attribute public class QueryParamAttribute : Attribute

View File

@ -19,7 +19,7 @@ namespace SpotifyAPI.Web
public string ReleaseDatePrecision { get; set; } public string ReleaseDatePrecision { get; set; }
public ResumePoint ResumePoint { get; set; } public ResumePoint ResumePoint { get; set; }
public SimpleShow Show { get; set; } public SimpleShow Show { get; set; }
public PlaylistElementType Type { get; set; } public ElementType Type { get; set; }
public string Uri { get; set; } public string Uri { get; set; }
} }
} }

View File

@ -21,7 +21,7 @@ namespace SpotifyAPI.Web
public int Popularity { get; set; } public int Popularity { get; set; }
public string PreviewUrl { get; set; } public string PreviewUrl { get; set; }
public int TrackNumber { get; set; } public int TrackNumber { get; set; }
public PlaylistElementType Type { get; set; } public ElementType Type { get; set; }
public string Uri { get; set; } public string Uri { get; set; }
public bool IsLocal { get; set; } public bool IsLocal { get; set; }
} }

View File

@ -3,14 +3,15 @@ using Newtonsoft.Json.Converters;
namespace SpotifyAPI.Web namespace SpotifyAPI.Web
{ {
public enum PlaylistElementType public enum ElementType
{ {
Track, Track,
Episode Episode
} }
public interface IPlaylistElement public interface IPlaylistElement
{ {
[JsonConverter(typeof(StringEnumConverter))] [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 Href { get; set; }
public string Id { get; set; } public string Id { get; set; }
public string Type { 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 System;
using Newtonsoft.Json;
namespace SpotifyAPI.Web namespace SpotifyAPI.Web
{ {
public class PlaylistTrack public class PlaylistTrack
@ -6,6 +8,7 @@ namespace SpotifyAPI.Web
public DateTime? AddedAt { get; set; } public DateTime? AddedAt { get; set; }
public PublicUser AddedBy { get; set; } public PublicUser AddedBy { get; set; }
public bool IsLocal { get; set; } public bool IsLocal { get; set; }
[JsonConverter(typeof(PlaylistElementConverter))]
public IPlaylistElement Track { get; set; } 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 MediaType { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Publisher { get; set; } public string Publisher { get; set; }
public string Type { get; set; } public ElementType Type { get; set; }
public string Uri { 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() public WebProxy CreateWebProxy()
{ {
if (!IsValid()) if (!IsValid())
{
return null; return null;
}
WebProxy proxy = new WebProxy WebProxy proxy = new WebProxy
{ {
@ -67,7 +69,9 @@ namespace SpotifyAPI.Web
}; };
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
{
return proxy; return proxy;
}
proxy.UseDefaultCredentials = false; proxy.UseDefaultCredentials = false;
proxy.Credentials = new NetworkCredential(Username, Password); proxy.Credentials = new NetworkCredential(Username, Password);
@ -79,12 +83,16 @@ namespace SpotifyAPI.Web
{ {
HttpClientHandler clientHandler = new HttpClientHandler HttpClientHandler clientHandler = new HttpClientHandler
{ {
PreAuthenticate = false, PreAuthenticate = false,
UseDefaultCredentials = true, UseDefaultCredentials = true,
UseProxy = false UseProxy = false
}; };
if (string.IsNullOrWhiteSpace(proxyConfig?.Host)) return clientHandler; if (string.IsNullOrWhiteSpace(proxyConfig?.Host))
{
return clientHandler;
}
WebProxy proxy = proxyConfig.CreateWebProxy(); WebProxy proxy = proxyConfig.CreateWebProxy();
clientHandler.UseProxy = true; clientHandler.UseProxy = true;
clientHandler.Proxy = proxy; clientHandler.Proxy = proxy;

View File

@ -3,21 +3,22 @@ namespace SpotifyAPI.Web
{ {
public static class SpotifyUrls 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 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); public static Uri Recommendations() => EUri($"recommendations");
private static Uri _Uri(string path) => new Uri(path, UriKind.Relative);
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> /// <param name = "name">The name of the argument</param>
public static void ArgumentNotNull(object value, string name) public static void ArgumentNotNull(object value, string name)
{ {
if (value != null) return; if (value != null)
{
return;
}
throw new ArgumentNullException(name); throw new ArgumentNullException(name);
} }
@ -26,9 +29,22 @@ namespace SpotifyAPI.Web
/// <param name = "name">The name of the argument</param> /// <param name = "name">The name of the argument</param>
public static void ArgumentNotNullOrEmptyString(string value, string name) 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); 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 queryString = String.Join("&", newParameters.Select((parameter) => $"{parameter.Key}={parameter.Value}"));
var query = string.IsNullOrEmpty(queryString) ? null : $"?{queryString}"; var query = string.IsNullOrEmpty(queryString) ? null : $"?{queryString}";
var uriBuilder = new UriBuilder(uri); var uriBuilder = new UriBuilder(uri)
uriBuilder.Query = query; {
Query = query
};
return uriBuilder.Uri; return uriBuilder.Uri;
} }

View File

@ -13,12 +13,10 @@ namespace SpotifyAPI.Web
public object GetFormat(Type formatType) public object GetFormat(Type formatType)
{ {
if (formatType == typeof(ICustomFormatter)) return formatType == typeof(ICustomFormatter) ? _formatter : null;
return _formatter;
return null;
} }
class URIParameterFormatter : ICustomFormatter public class URIParameterFormatter : ICustomFormatter
{ {
public string Format(string format, object arg, IFormatProvider formatProvider) 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
}
}