From 9f6729ad60a9fa2e87e8a6796e192b50c22a1cbf Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Sat, 2 May 2020 13:04:26 +0200 Subject: [PATCH] Started with Unit Tests and added .editorconfig for syntax settings Disabled examples for now --- .editorconfig | 28 ++ SpotifyAPI.Web.Auth/AuthUtil.cs | 39 --- SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs | 132 --------- SpotifyAPI.Web.Auth/CredentialsAuth.cs | 44 --- SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs | 63 ---- SpotifyAPI.Web.Auth/SpotifyAuthServer.cs | 93 ------ SpotifyAPI.Web.Auth/TokenSwapAuth.cs | 218 -------------- SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs | 280 ------------------ .../Controllers/HomeController.cs | 20 +- .../Models/IndexModel.cs | 4 +- .../Views/Home/Index.cshtml | 4 +- SpotifyAPI.Web.Examples.CLI/Program.cs | 105 ++++--- .../Http/NetHTTPClientTest.cs | 20 ++ .../Http/NewtonsoftJSONSerializerTest.cs | 90 ++++++ .../Http/TokenHeaderAuthenticatorTest.cs | 22 ++ SpotifyAPI.Web.Tests/UtilTests/Test.cs | 16 +- .../URIParameterFormatProviderTest.cs | 4 +- SpotifyAPI.Web/Clients/BrowseClient.cs | 7 + .../Clients/Interfaces/IBrowseClient.cs | 2 + .../Clients/Interfaces/ISpotifyClient.cs | 2 +- SpotifyAPI.Web/Clients/SpotifyClient.cs | 14 +- SpotifyAPI.Web/Clients/SpotifyClientConfig.cs | 86 ++++++ SpotifyAPI.Web/Enums/AlbumType.cs | 2 +- SpotifyAPI.Web/Http/APIConnector.cs | 22 +- SpotifyAPI.Web/Http/APIResponse.cs | 2 +- SpotifyAPI.Web/Http/Interfaces/IRequest.cs | 2 - SpotifyAPI.Web/Http/NetHttpClient.cs | 36 +-- .../Http/NewtonsoftJSONSerializer.cs | 2 +- SpotifyAPI.Web/Http/Request.cs | 4 +- .../Http/TokenHeaderAuthenticator.cs | 2 +- .../Models/Request/RecommendationsRequest.cs | 58 ++++ .../Models/Request/RequestParams.cs | 12 +- SpotifyAPI.Web/Models/Response/FullEpisode.cs | 2 +- SpotifyAPI.Web/Models/Response/FullTrack.cs | 2 +- .../Response/Interfaces/IPlaylistElement.cs | 5 +- SpotifyAPI.Web/Models/Response/LinkedTrack.cs | 2 +- .../Models/Response/PlaylistTrack.cs | 3 + .../Models/Response/RecommendationSeed.cs | 18 ++ .../Response/RecommendationsResponse.cs | 10 + SpotifyAPI.Web/Models/Response/SimpleShow.cs | 2 +- SpotifyAPI.Web/Models/Response/SimpleTrack.cs | 23 ++ SpotifyAPI.Web/ProxyConfig.cs | 18 +- SpotifyAPI.Web/SpotifyUrls.cs | 17 +- SpotifyAPI.Web/Util/Ensure.cs | 20 +- SpotifyAPI.Web/Util/URIExtension.cs | 6 +- .../Util/URIParameterFormatProvider.cs | 6 +- omnisharp.json | 8 + 47 files changed, 550 insertions(+), 1027 deletions(-) create mode 100644 .editorconfig delete mode 100644 SpotifyAPI.Web.Auth/AuthUtil.cs delete mode 100644 SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs delete mode 100644 SpotifyAPI.Web.Auth/CredentialsAuth.cs delete mode 100644 SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs delete mode 100644 SpotifyAPI.Web.Auth/SpotifyAuthServer.cs delete mode 100644 SpotifyAPI.Web.Auth/TokenSwapAuth.cs delete mode 100644 SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs create mode 100644 SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs create mode 100644 SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs create mode 100644 SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs create mode 100644 SpotifyAPI.Web/Clients/SpotifyClientConfig.cs create mode 100644 SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs create mode 100644 SpotifyAPI.Web/Models/Response/RecommendationSeed.cs create mode 100644 SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs create mode 100644 SpotifyAPI.Web/Models/Response/SimpleTrack.cs create mode 100644 omnisharp.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c37bcce8 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/SpotifyAPI.Web.Auth/AuthUtil.cs b/SpotifyAPI.Web.Auth/AuthUtil.cs deleted file mode 100644 index cea40708..00000000 --- a/SpotifyAPI.Web.Auth/AuthUtil.cs +++ /dev/null @@ -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; - } - } - } -} diff --git a/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs b/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs deleted file mode 100644 index e5145676..00000000 --- a/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs +++ /dev/null @@ -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 - { - 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().RegisterController(); - } - - private string GetAuthHeader() => $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(ClientId + ":" + SecretId))}"; - - public async Task RefreshToken(string refreshToken) - { - List> args = new List> - { - new KeyValuePair("grant_type", "refresh_token"), - new KeyValuePair("refresh_token", refreshToken) - }; - - return await GetToken(args); - } - - public async Task ExchangeCode(string code) - { - List> args = new List> - { - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("code", code), - new KeyValuePair("redirect_uri", RedirectUri) - }; - - return await GetToken(args); - } - - private async Task GetToken(IEnumerable> 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(msg); - } - } - - public class AuthorizationCode - { - public string Code { get; set; } - - public string Error { get; set; } - } - - internal class AuthorizationCodeAuthController : WebApiController - { - [WebApiHandler(HttpVerbs.Get, "/")] - public Task GetEmpty() - { - string state = Request.QueryString["state"]; - AuthorizationCodeAuth.Instances.TryGetValue(state, out SpotifyAuthServer 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("OK - This window can be closed now"); - } - - [WebApiHandler(HttpVerbs.Post, "/")] - public async Task PostValues() - { - Dictionary formParams = await HttpContext.RequestFormDataDictionaryAsync(); - - string state = (string) formParams["state"]; - AuthorizationCodeAuth.Instances.TryGetValue(state, out SpotifyAuthServer 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) - { } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Auth/CredentialsAuth.cs b/SpotifyAPI.Web.Auth/CredentialsAuth.cs deleted file mode 100644 index 661d987d..00000000 --- a/SpotifyAPI.Web.Auth/CredentialsAuth.cs +++ /dev/null @@ -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 GetToken() - { - string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(ClientId + ":" + ClientSecret)); - - List> args = new List> - {new KeyValuePair("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(msg); - } - } -} diff --git a/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs b/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs deleted file mode 100644 index 2489fdfa..00000000 --- a/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs +++ /dev/null @@ -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 - { - 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().RegisterController(); - } - } - - public class ImplicitGrantAuthController : WebApiController - { - [WebApiHandler(HttpVerbs.Get, "/auth")] - public Task GetAuth() - { - string state = Request.QueryString["state"]; - SpotifyAuthServer 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("OK - This window can be closed now"); - } - - public ImplicitGrantAuthController(IHttpContext context) : base(context) - { } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs b/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs deleted file mode 100644 index 33c6f035..00000000 --- a/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs +++ /dev/null @@ -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 - { - 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> Instances = new Dictionary>(); - - 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 GetByState(string state) - { - return Instances.TryGetValue(state, out SpotifyAuthServer auth) ? auth : null; - } - - protected abstract void AdaptWebServer(WebServer webServer); - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Auth/TokenSwapAuth.cs b/SpotifyAPI.Web.Auth/TokenSwapAuth.cs deleted file mode 100644 index 1514b38d..00000000 --- a/SpotifyAPI.Web.Auth/TokenSwapAuth.cs +++ /dev/null @@ -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 -{ - /// - /// - /// A version of 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. - /// - /// - /// It's recommended that you use if you would like to use the TokenSwap method. - /// - /// - public class TokenSwapAuth : SpotifyAuthServer - { - readonly string _exchangeServerUri; - - /// - /// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival. - /// - public string HtmlResponse { get; set; } = ""; - - /// - /// If true, will time how long it takes for access to expire. On expiry, the event fires. - /// - public bool TimeAccessExpiry { get; set; } - - public ProxyConfig ProxyConfig { get; set; } - - /// The URI to an exchange server that will perform the key exchange. - /// 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) - /// - /// Stating none will randomly generate a state parameter. - /// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival. - 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().RegisterController(); - } - - 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()); - } - - /// - /// The maximum amount of times to retry getting a token. - /// - /// A token get is attempted every time you and . - /// - public int MaxGetTokenRetries { get; set; } = 10; - - /// - /// Creates a HTTP request to obtain a token object. - /// 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. - /// - /// Will re-attempt on error, on null or on no access token times before finally returning null. - /// - /// - /// Can only be "refresh_token" or "authorization_code". - /// This needs to be defined if "grantType" is "authorization_code". - /// This needs to be defined if "grantType" is "refresh_token". - /// Does not need to be defined. Used internally for retry attempt recursion. - /// Attempts to return a full , but after retry attempts, may return a with no , or null. - async Task GetToken(string grantType, string authorizationCode = "", string refreshToken = "", - int currentRetries = 0) - { - FormUrlEncodedContent content = new FormUrlEncodedContent(new Dictionary - { { "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(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; - - /// - /// When Spotify authorization has expired. Will only trigger if is true. - /// - public event EventHandler OnAccessTokenExpired; - - /// - /// If is true, sets a timer for how long access will take to expire. - /// - /// - 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); - } - - /// - /// 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 . - /// - /// - /// - public async Task ExchangeCodeAsync(string authorizationCode) - { - Token token = await GetToken("authorization_code", authorizationCode : authorizationCode); - if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken)) - { - SetAccessExpireTimer(token); - } - - return token; - } - - /// - /// 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). - /// - /// - /// - public async Task 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 GetAuth() - { - string state = Request.QueryString["state"]; - SpotifyAuthServer 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); - } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs b/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs deleted file mode 100644 index 604be245..00000000 --- a/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs +++ /dev/null @@ -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 -{ - /// - /// Returns a using the TokenSwapAuth process. - /// - public class TokenSwapWebAPIFactory - { - /// - /// Access provided by Spotify expires after 1 hour. If true, will time the access tokens, and access will attempt to be silently (without opening a browser) refreshed automatically. This will not make fire, see for that. - /// - public bool AutoRefresh { get; set; } - /// - /// If true when calling , will time how long it takes for access to Spotify to expire. The event fires when the timer elapses. - /// - public bool TimeAccessExpiry { get; set; } - /// - /// 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. - /// - public int Timeout { get; set; } - public Scope Scope { get; set; } - /// - /// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens. - /// - public string ExchangeServerUri { get; set; } - /// - /// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002 - /// - public string HostServerUri { get; set; } - /// - /// 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. - /// - public bool OpenBrowser { get; set; } - /// - /// The HTML to respond with when the callback server has been reached. By default, it is set to close the window on arrival. - /// - public string HtmlResponse { get; set; } - /// - /// 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. - /// - public bool ShowDialog { get; set; } - /// - /// The maximum amount of times to retry getting a token. - /// - /// A token get is attempted every time you and . Increasing this may improve how often these actions succeed - although it won't solve any underlying problems causing a get token failure. - /// - public int MaxGetTokenRetries { get; set; } = 10; - /// - /// Returns a SpotifyWebAPI using the TokenSwapAuth process. - /// - /// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens. - /// - /// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002 - /// 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. - /// Access provided by Spotify expires after 1 hour. If true, access will attempt to be silently (without opening a browser) refreshed automatically. - /// 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. - 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; } - } - /// - /// When the URI to get an authorization code is ready to be used to be visited. Not required if is true as the exchange URI will automatically be visited for you. - /// - public event EventHandler OnExchangeReady; - - /// - /// Refreshes the access for a SpotifyWebAPI returned by this factory. - /// - /// - 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() - { } - } - /// - /// When the authorization from Spotify expires. This will only occur if is true. - /// - public event EventHandler OnAccessTokenExpired; - - public class AuthSuccessEventArgs : EventArgs - { - public static new AuthSuccessEventArgs Empty { get; } = new AuthSuccessEventArgs(); - - public AuthSuccessEventArgs() - { } - } - /// - /// When an authorization attempt succeeds and gains authorization. - /// - public event EventHandler OnAuthSuccess; - - public class AuthFailureEventArgs : EventArgs - { - public static new AuthFailureEventArgs Empty { get; } = new AuthFailureEventArgs(""); - - public string Error { get; } - - public AuthFailureEventArgs(string error) - { - Error = error; - } - } - /// - /// When an authorization attempt fails to gain authorization. - /// - public event EventHandler OnAuthFailure; - - /// - /// Manually triggers the timeout for any ongoing get web API request. - /// - 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; - - /// - /// Gets an authorized and ready to use SpotifyWebAPI by following the SecureAuthorizationCodeAuth process with its current settings. - /// - /// - public async Task GetWebApiAsync() - { - return await Task.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; - }); - } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs b/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs index cce16ad4..d6d9f84d 100644 --- a/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs +++ b/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs @@ -10,18 +10,18 @@ namespace SpotifyAPI.Web.Examples.ASP.Controllers [Authorize(AuthenticationSchemes = "Spotify")] public class HomeController : Controller { - public async Task 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)] @@ -30,4 +30,4 @@ namespace SpotifyAPI.Web.Examples.ASP.Controllers return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs b/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs index 15f04055..f0fbfe8c 100644 --- a/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs +++ b/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs @@ -5,6 +5,6 @@ namespace SpotifyAPI.Web.Examples.ASP.Models { public class IndexModel { - public Paging SavedTracks; + public Paging SavedTracks; } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml index c1b077fc..8190387a 100644 --- a/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml +++ b/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml @@ -6,10 +6,10 @@
You have @Model.SavedTracks.Total saved tracks in your library! Here are 50 of them: -
    + @*
      @foreach (var item in Model.SavedTracks.Items) {
    • @item.Track.Name
    • } -
    +
*@
diff --git a/SpotifyAPI.Web.Examples.CLI/Program.cs b/SpotifyAPI.Web.Examples.CLI/Program.cs index eeb2b607..1d4b3d76 100644 --- a/SpotifyAPI.Web.Examples.CLI/Program.cs +++ b/SpotifyAPI.Web.Examples.CLI/Program.cs @@ -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 playlists) - { - if (playlists.Items == null) return; + // private static async Task PrintAllPlaylistTracks(SpotifyWebAPI api, Paging 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)); + // } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs b/SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs new file mode 100644 index 00000000..f93948cf --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/NetHTTPClientTest.cs @@ -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() + { + + } + } + } +} diff --git a/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs b/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs new file mode 100644 index 00000000..c51fd953 --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs @@ -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 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(); + request.SetupGet(r => r.Body).Returns(input); + + serializer.SerializeRequest(request.Object); + + Assert.AreEqual(input, request.Object.Body); + } + + public static IEnumerable 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(); + 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(); + response.SetupGet(r => r.Body).Returns("hello"); + response.SetupGet(r => r.ContentType).Returns("media/mp4"); + + IAPIResponse apiResonse = serializer.DeserializeResponse(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(); + response.SetupGet(r => r.Body).Returns("{\"hello_world\": false}"); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + Assert.AreEqual(apiResonse.Body?.HelloWorld, false); + Assert.AreEqual(apiResonse.Response, response.Object); + } + + public class TestResponseObject + { + public bool HelloWorld { get; set; } + } + } +} diff --git a/SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs b/SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs new file mode 100644 index 00000000..bcbc58fd --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs @@ -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(); + request.SetupGet(r => r.Headers).Returns(new Dictionary()); + + authenticator.Apply(request.Object); + Assert.AreEqual(request.Object.Headers["Authorization"], "Bearer MyToken"); + } + } +} diff --git a/SpotifyAPI.Web.Tests/UtilTests/Test.cs b/SpotifyAPI.Web.Tests/UtilTests/Test.cs index 657ade42..9fbf38b1 100644 --- a/SpotifyAPI.Web.Tests/UtilTests/Test.cs +++ b/SpotifyAPI.Web.Tests/UtilTests/Test.cs @@ -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); } } } diff --git a/SpotifyAPI.Web.Tests/UtilTests/URIParameterFormatProviderTest.cs b/SpotifyAPI.Web.Tests/UtilTests/URIParameterFormatProviderTest.cs index 2e47a65c..9e6c5221 100644 --- a/SpotifyAPI.Web.Tests/UtilTests/URIParameterFormatProviderTest.cs +++ b/SpotifyAPI.Web.Tests/UtilTests/URIParameterFormatProviderTest.cs @@ -14,7 +14,7 @@ namespace SpotifyAPI.Web.Tests var user = "wizzler"; var formatter = new URIParameterFormatProvider(); - Func 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 func = (FormattableString str) => str.ToString(formatter); + string func(FormattableString str) => str.ToString(formatter); Assert.AreEqual(expected, func($"/users/{user}")); } diff --git a/SpotifyAPI.Web/Clients/BrowseClient.cs b/SpotifyAPI.Web/Clients/BrowseClient.cs index f07217e9..7d88dbe6 100644 --- a/SpotifyAPI.Web/Clients/BrowseClient.cs +++ b/SpotifyAPI.Web/Clients/BrowseClient.cs @@ -51,5 +51,12 @@ namespace SpotifyAPI.Web return API.Get(URLs.CategoryPlaylists(categoryId), request.BuildQueryParams()); } + + public Task GetRecommendations(RecommendationsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Recommendations(), request.BuildQueryParams()); + } } } diff --git a/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs b/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs index 974c1774..d010a252 100644 --- a/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs +++ b/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs @@ -12,5 +12,7 @@ namespace SpotifyAPI.Web Task GetCategoryPlaylists(string categoryId); Task GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request); + + Task GetRecommendations(RecommendationsRequest request); } } diff --git a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs index 4f33b281..9f368c58 100644 --- a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs @@ -1,6 +1,6 @@ namespace SpotifyAPI.Web { - interface ISpotifyClient + public interface ISpotifyClient { IUserProfileClient UserProfile { get; } diff --git a/SpotifyAPI.Web/Clients/SpotifyClient.cs b/SpotifyAPI.Web/Clients/SpotifyClient.cs index 61aab136..46bb45fc 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClient.cs @@ -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); } diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs new file mode 100644 index 00000000..97d5ff3f --- /dev/null +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -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; } + + /// + /// 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 + /// or to specify the auth details. + /// + /// + /// + /// + /// + 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)); + } + + /// + /// Creates a default configuration, which is not useable without calling or + /// + /// + public static SpotifyClientConfig CreateDefault() + { + return CreateDefault(null); + } + + public static SpotifyClientConfig CreateDefault(IAuthenticator authenticator) + { + return new SpotifyClientConfig( + SpotifyUrls.API_V1, + authenticator, + new NewtonsoftJSONSerializer(), + new NetHttpClient() + ); + } + } +} diff --git a/SpotifyAPI.Web/Enums/AlbumType.cs b/SpotifyAPI.Web/Enums/AlbumType.cs index c8750121..71f59b3f 100644 --- a/SpotifyAPI.Web/Enums/AlbumType.cs +++ b/SpotifyAPI.Web/Enums/AlbumType.cs @@ -20,4 +20,4 @@ namespace SpotifyAPI.Web.Enums [String("album,single,compilation,appears_on")] All = 16 } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web/Http/APIConnector.cs b/SpotifyAPI.Web/Http/APIConnector.cs index 3812dff3..1c8a41b1 100644 --- a/SpotifyAPI.Web/Http/APIConnector.cs +++ b/SpotifyAPI.Web/Http/APIConnector.cs @@ -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), + }; } } } diff --git a/SpotifyAPI.Web/Http/APIResponse.cs b/SpotifyAPI.Web/Http/APIResponse.cs index efda8bb9..48e0ba47 100644 --- a/SpotifyAPI.Web/Http/APIResponse.cs +++ b/SpotifyAPI.Web/Http/APIResponse.cs @@ -2,7 +2,7 @@ namespace SpotifyAPI.Web.Http { public class APIResponse : IAPIResponse { - public APIResponse(IResponse response, T body = default(T)) + public APIResponse(IResponse response, T body = default) { Ensure.ArgumentNotNull(response, nameof(response)); diff --git a/SpotifyAPI.Web/Http/Interfaces/IRequest.cs b/SpotifyAPI.Web/Http/Interfaces/IRequest.cs index 72860ece..9a6c0db3 100644 --- a/SpotifyAPI.Web/Http/Interfaces/IRequest.cs +++ b/SpotifyAPI.Web/Http/Interfaces/IRequest.cs @@ -16,8 +16,6 @@ namespace SpotifyAPI.Web.Http HttpMethod Method { get; } - string ContentType { get; } - object Body { get; set; } } } diff --git a/SpotifyAPI.Web/Http/NetHttpClient.cs b/SpotifyAPI.Web/Http/NetHttpClient.cs index 8c31f822..b289dc76 100644 --- a/SpotifyAPI.Web/Http/NetHttpClient.cs +++ b/SpotifyAPI.Web/Http/NetHttpClient.cs @@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http { public class NetHttpClient : IHTTPClient { - private HttpClient _httpClient; + private readonly HttpClient _httpClient; public NetHttpClient() { @@ -19,14 +19,12 @@ namespace SpotifyAPI.Web.Http { Ensure.ArgumentNotNull(request, nameof(request)); - using (HttpRequestMessage requestMsg = BuildRequestMessage(request)) - { - var responseMsg = await _httpClient - .SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead) - .ConfigureAwait(false); + using HttpRequestMessage requestMsg = BuildRequestMessage(request); + var responseMsg = await _httpClient + .SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead) + .ConfigureAwait(false); - return await BuildResponse(responseMsg).ConfigureAwait(false); - } + return await BuildResponse(responseMsg).ConfigureAwait(false); } private async Task BuildResponse(HttpResponseMessage responseMsg) @@ -34,19 +32,17 @@ namespace SpotifyAPI.Web.Http Ensure.ArgumentNotNull(responseMsg, nameof(responseMsg)); // We only support text stuff for now - using (var content = responseMsg.Content) - { - var headers = responseMsg.Headers.ToDictionary(header => header.Key, header => header.Value.First()); - var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false); - var contentType = content.Headers?.ContentType?.MediaType; + using var content = responseMsg.Content; + var headers = responseMsg.Headers.ToDictionary(header => header.Key, header => header.Value.First()); + var body = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false); + var contentType = content.Headers?.ContentType?.MediaType; - return new Response(headers) - { - ContentType = contentType, - StatusCode = responseMsg.StatusCode, - Body = body - }; - } + return new Response(headers) + { + ContentType = contentType, + StatusCode = responseMsg.StatusCode, + Body = body + }; } private HttpRequestMessage BuildRequestMessage(IRequest request) diff --git a/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs b/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs index d182acfb..e19aeb7e 100644 --- a/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs +++ b/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs @@ -9,7 +9,7 @@ namespace SpotifyAPI.Web.Http { public class NewtonsoftJSONSerializer : IJSONSerializer { - JsonSerializerSettings _serializerSettings; + private readonly JsonSerializerSettings _serializerSettings; public NewtonsoftJSONSerializer() { diff --git a/SpotifyAPI.Web/Http/Request.cs b/SpotifyAPI.Web/Http/Request.cs index 7d88789c..c48a47fa 100644 --- a/SpotifyAPI.Web/Http/Request.cs +++ b/SpotifyAPI.Web/Http/Request.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs b/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs index a4ec0cbe..9b2ff4d1 100644 --- a/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs +++ b/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs @@ -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) { diff --git a/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs b/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs new file mode 100644 index 00000000..774ed041 --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class RecommendationsRequest : RequestParams + { + public RecommendationsRequest() + { + Min = new Dictionary(); + Max = new Dictionary(); + Target = new Dictionary(); + } + + [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 Min { get; set; } + public Dictionary Max { get; set; } + public Dictionary 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 queryParams) + { + foreach (KeyValuePair pair in Min) + { + queryParams.Add($"min_{pair.Key}", pair.Value); + } + foreach (KeyValuePair pair in Min) + { + queryParams.Add($"max_{pair.Key}", pair.Value); + } + foreach (KeyValuePair pair in Min) + { + queryParams.Add($"target_{pair.Key}", pair.Value); + } + } + } +} diff --git a/SpotifyAPI.Web/Models/Request/RequestParams.cs b/SpotifyAPI.Web/Models/Request/RequestParams.cs index e47798e5..510db396 100644 --- a/SpotifyAPI.Web/Models/Request/RequestParams.cs +++ b/SpotifyAPI.Web/Models/Request/RequestParams.cs @@ -8,22 +8,30 @@ namespace SpotifyAPI.Web { public Dictionary 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(); 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 queryParams) { } } public class QueryParamAttribute : Attribute diff --git a/SpotifyAPI.Web/Models/Response/FullEpisode.cs b/SpotifyAPI.Web/Models/Response/FullEpisode.cs index 68abd37f..f2e032f1 100644 --- a/SpotifyAPI.Web/Models/Response/FullEpisode.cs +++ b/SpotifyAPI.Web/Models/Response/FullEpisode.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Models/Response/FullTrack.cs b/SpotifyAPI.Web/Models/Response/FullTrack.cs index 4fe09996..4e722a85 100644 --- a/SpotifyAPI.Web/Models/Response/FullTrack.cs +++ b/SpotifyAPI.Web/Models/Response/FullTrack.cs @@ -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; } } diff --git a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs b/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs index 7d5a627b..1534f4f2 100644 --- a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs +++ b/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Models/Response/LinkedTrack.cs b/SpotifyAPI.Web/Models/Response/LinkedTrack.cs index 1915b943..b16a7624 100644 --- a/SpotifyAPI.Web/Models/Response/LinkedTrack.cs +++ b/SpotifyAPI.Web/Models/Response/LinkedTrack.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs b/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs index 48314615..2093222f 100644 --- a/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs +++ b/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs b/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs new file mode 100644 index 00000000..c8ab076b --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs @@ -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; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs b/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs new file mode 100644 index 00000000..c79d45c8 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class RecommendationsResponse + { + public List Seeds { get; set; } + public List Tracks { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/SimpleShow.cs b/SpotifyAPI.Web/Models/Response/SimpleShow.cs index 6635bc29..cde821a5 100644 --- a/SpotifyAPI.Web/Models/Response/SimpleShow.cs +++ b/SpotifyAPI.Web/Models/Response/SimpleShow.cs @@ -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; } } } diff --git a/SpotifyAPI.Web/Models/Response/SimpleTrack.cs b/SpotifyAPI.Web/Models/Response/SimpleTrack.cs new file mode 100644 index 00000000..665ed10d --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/SimpleTrack.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class SimpleTrack + { + public List Artists { get; set; } + public List AvailableMarkets { get; set; } + public int DiscNumber { get; set; } + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary 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; } + } +} diff --git a/SpotifyAPI.Web/ProxyConfig.cs b/SpotifyAPI.Web/ProxyConfig.cs index 4baec0a8..eacf6a7d 100644 --- a/SpotifyAPI.Web/ProxyConfig.cs +++ b/SpotifyAPI.Web/ProxyConfig.cs @@ -57,7 +57,9 @@ namespace SpotifyAPI.Web public WebProxy CreateWebProxy() { if (!IsValid()) + { return null; + } WebProxy proxy = new WebProxy { @@ -67,7 +69,9 @@ namespace SpotifyAPI.Web }; if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) + { return proxy; + } proxy.UseDefaultCredentials = false; proxy.Credentials = new NetworkCredential(Username, Password); @@ -79,12 +83,16 @@ namespace SpotifyAPI.Web { HttpClientHandler clientHandler = new HttpClientHandler { - PreAuthenticate = false, - UseDefaultCredentials = true, - UseProxy = false + PreAuthenticate = false, + UseDefaultCredentials = true, + UseProxy = false }; - if (string.IsNullOrWhiteSpace(proxyConfig?.Host)) return clientHandler; + if (string.IsNullOrWhiteSpace(proxyConfig?.Host)) + { + return clientHandler; + } + WebProxy proxy = proxyConfig.CreateWebProxy(); clientHandler.UseProxy = true; clientHandler.Proxy = proxy; @@ -94,4 +102,4 @@ namespace SpotifyAPI.Web return clientHandler; } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web/SpotifyUrls.cs b/SpotifyAPI.Web/SpotifyUrls.cs index 2e5e2244..889c5296 100644 --- a/SpotifyAPI.Web/SpotifyUrls.cs +++ b/SpotifyAPI.Web/SpotifyUrls.cs @@ -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); } } diff --git a/SpotifyAPI.Web/Util/Ensure.cs b/SpotifyAPI.Web/Util/Ensure.cs index e162b9cb..645279c2 100644 --- a/SpotifyAPI.Web/Util/Ensure.cs +++ b/SpotifyAPI.Web/Util/Ensure.cs @@ -14,7 +14,10 @@ namespace SpotifyAPI.Web /// The name of the argument 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 /// The name of the argument 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}"); + } } } diff --git a/SpotifyAPI.Web/Util/URIExtension.cs b/SpotifyAPI.Web/Util/URIExtension.cs index 1a443a90..57c26366 100644 --- a/SpotifyAPI.Web/Util/URIExtension.cs +++ b/SpotifyAPI.Web/Util/URIExtension.cs @@ -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; } diff --git a/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs b/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs index 1686f3c5..3e20c9ec 100644 --- a/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs +++ b/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs @@ -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) { diff --git a/omnisharp.json b/omnisharp.json new file mode 100644 index 00000000..b2e29694 --- /dev/null +++ b/omnisharp.json @@ -0,0 +1,8 @@ +{ + "RoslynExtensionsOptions": { + "enableAnalyzersSupport": true + }, + "FormattingOptions": { + "enableEditorConfigSupport": true + } +}