diff --git a/.gitignore b/.gitignore index 8debf01..0ff7e94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ + # Created by https://www.gitignore.io/api/visualstudio # Edit at https://www.gitignore.io/?templates=visualstudio @@ -8,6 +15,76 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +**/.idea/** + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# JetBrains templates +**___jb_tmp___ + + # User-specific files *.rsuser *.suo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index df7a88f..5d9fabb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,5 @@ CHANGE LOG: -Beta 2.0: Improved performance. Removed 500 album restriction. Added automatic prompt to re-authenticate if computer resumes from sleep. -Beta 2.0.2: Fixed error log spam. \ No newline at end of file +Release 2.0: Improved performance. Removed 500 album restriction. Added automatic prompt to re-authenticate if computer resumes from sleep. +Release 2.0.2: Fixed error log spam. +Beta 3.1: Upgraded all API methods to 6.x.x spec. Implemented PKCE auth method with token persistence and automatic renewal. General speed improvements. \ No newline at end of file diff --git a/Plugins/EmbedIO.dll b/Plugins/EmbedIO.dll new file mode 100644 index 0000000..cfdff5a Binary files /dev/null and b/Plugins/EmbedIO.dll differ diff --git a/Plugins/SpotifyAPI.Web.Auth.dll b/Plugins/SpotifyAPI.Web.Auth.dll index 129f936..63b7470 100644 Binary files a/Plugins/SpotifyAPI.Web.Auth.dll and b/Plugins/SpotifyAPI.Web.Auth.dll differ diff --git a/Plugins/SpotifyAPI.Web.dll b/Plugins/SpotifyAPI.Web.dll index d1830fc..f60cf83 100644 Binary files a/Plugins/SpotifyAPI.Web.dll and b/Plugins/SpotifyAPI.Web.dll differ diff --git a/Plugins/Swan.Lite.dll b/Plugins/Swan.Lite.dll new file mode 100644 index 0000000..e291d8a Binary files /dev/null and b/Plugins/Swan.Lite.dll differ diff --git a/Plugins/Unosquare.Labs.EmbedIO.dll b/Plugins/Unosquare.Labs.EmbedIO.dll deleted file mode 100644 index 89c106e..0000000 Binary files a/Plugins/Unosquare.Labs.EmbedIO.dll and /dev/null differ diff --git a/Plugins/Unosquare.Swan.Lite.dll b/Plugins/Unosquare.Swan.Lite.dll deleted file mode 100644 index 882dafd..0000000 Binary files a/Plugins/Unosquare.Swan.Lite.dll and /dev/null differ diff --git a/Plugins/mb_Spotify-Plugin.dll b/Plugins/mb_Spotify-Plugin.dll index b79f436..6d20595 100644 Binary files a/Plugins/mb_Spotify-Plugin.dll and b/Plugins/mb_Spotify-Plugin.dll differ diff --git a/README.txt b/README.txt index f57a434..fe2b561 100644 --- a/README.txt +++ b/README.txt @@ -12,5 +12,7 @@ Let me know if you encounter any bugs - "ZKHCOHEN" on the MusicBee Forums. KNOWN ISSUES: -Performance issues due to a workaround I implemented because of a bug in the Spotify API. Adding/removing albums is slow. The first track you play after enabling the add-in is slow to load. -You have to re-authenticate hourly due to a limitation with the Spotify API. I plan to do this silently in the future. \ No newline at end of file +The Spotify API is (incorrectly) reporting the following error in the MusicBee ErrorLog.dat file: + +System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. +As a result, the unobserved exception was rethrown by the finalizer thread. ---> SpotifyAPI.Web.APIException: invalid_grant \ No newline at end of file diff --git a/Source Files/Crypt.cs b/Source Files/Crypt.cs new file mode 100644 index 0000000..d272cbf --- /dev/null +++ b/Source Files/Crypt.cs @@ -0,0 +1,89 @@ +using System; +using System.Xml; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; + +namespace MusicBeePlugin +{ + public partial class Plugin + { + public static void Encrypt(XmlDocument Doc, string ElementToEncrypt, string EncryptionElementID, RSA Alg, string KeyName) + { + if (Doc == null) + throw new ArgumentNullException("Doc"); + if (ElementToEncrypt == null) + throw new ArgumentNullException("ElementToEncrypt"); + if (EncryptionElementID == null) + throw new ArgumentNullException("EncryptionElementID"); + if (Alg == null) + throw new ArgumentNullException("Alg"); + if (KeyName == null) + throw new ArgumentNullException("KeyName"); + + XmlElement elementToEncrypt = Doc.GetElementsByTagName(ElementToEncrypt)[0] as XmlElement; + + if (elementToEncrypt == null) + { + throw new XmlException("The specified element was not found"); + } + Aes sessionKey = null; + + try + { + EncryptedXml eXml = new EncryptedXml(); + EncryptedData edElement = new EncryptedData(); + EncryptedKey ek = new EncryptedKey(); + DataReference dRef = new DataReference(); + KeyInfoName kin = new KeyInfoName(); + + sessionKey = Aes.Create(); + + byte[] encryptedElement = eXml.EncryptData(elementToEncrypt, sessionKey, false); + + edElement.Type = EncryptedXml.XmlEncElementUrl; + edElement.Id = EncryptionElementID; + edElement.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url); + + byte[] encryptedKey = EncryptedXml.EncryptKey(sessionKey.Key, Alg, false); + + ek.CipherData = new CipherData(encryptedKey); + ek.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url); + dRef.Uri = "#" + EncryptionElementID; + ek.AddReference(dRef); + edElement.KeyInfo.AddClause(new KeyInfoEncryptedKey(ek)); + kin.Value = KeyName; + ek.KeyInfo.AddClause(kin); + edElement.CipherData.CipherValue = encryptedElement; + + EncryptedXml.ReplaceElement(elementToEncrypt, edElement, false); + } + catch (Exception e) + { + throw e; + } + finally + { + if (sessionKey != null) + { + sessionKey.Clear(); + } + } + } + + public static void Decrypt(XmlDocument Doc, RSA Alg, string KeyName) + { + if (Doc == null) + throw new ArgumentNullException("Doc"); + if (Alg == null) + throw new ArgumentNullException("Alg"); + if (KeyName == null) + throw new ArgumentNullException("KeyName"); + + EncryptedXml exml = new EncryptedXml(Doc); + + exml.AddKeyNameMapping(KeyName, Alg); + exml.DecryptDocument(); + } + + } +} diff --git a/Source Files/PanelInterface.cs b/Source Files/PanelInterface.cs index c30c07c..10c1539 100644 --- a/Source Files/PanelInterface.cs +++ b/Source Files/PanelInterface.cs @@ -3,9 +3,8 @@ using System.Drawing; using System.Windows.Forms; using System.Net; -using System.Threading; using System.Collections.Generic; -using Microsoft.Win32; +using System.Security.Cryptography; namespace MusicBeePlugin { @@ -16,41 +15,45 @@ public partial class Plugin private PluginInfo about = new PluginInfo(); private Control panel; public int panelHeight; - private static string _searchTerm, _savedAlbumsPath; + private static string _searchTerm; + private bool _runOnce = true; Font largeBold, smallRegular, smallBold; - static System.Threading.Timer authTimer; + private RSACryptoServiceProvider _rsaKey; - static void TickTimer(object state) - { - _auth = 0; - } + // Create a new CspParameters object to specify a key container. + CspParameters _cspParams = new CspParameters(); - public PluginInfo Initialise(IntPtr apiInterfacePtr) + public PluginInfo Initialise(IntPtr apiInterfacePtr) { mbApiInterface = new MusicBeeApiInterface(); mbApiInterface.Initialise(apiInterfacePtr); about.PluginInfoVersion = PluginInfoVersion; about.Name = "mb_Spotify_Plugin"; about.Description = "This plugin integrates Spotify with MusicBee."; - about.Author = "Zachary Cohen"; + about.Author = "zkhcohen"; about.TargetApplication = "Spotify Plugin"; about.Type = PluginType.PanelView; - about.VersionMajor = 2; + about.VersionMajor = 3; about.VersionMinor = 0; - about.Revision = 2; + about.Revision = 5; about.MinInterfaceVersion = MinInterfaceVersion; about.MinApiRevision = MinApiRevision; about.ReceiveNotifications = (ReceiveNotificationFlags.PlayerEvents | ReceiveNotificationFlags.TagEvents); about.ConfigurationPanelHeight = 0; - _savedAlbumsPath = mbApiInterface.Setting_GetPersistentStoragePath() + @"spotify.txt"; + _path = mbApiInterface.Setting_GetPersistentStoragePath() + "token.xml"; + + _cspParams.KeyContainerName = "SPOTIFY_XML_ENC_RSA_KEY"; + + // Create a new RSA key and save it in the container. This key will encrypt + // a symmetric key, which will then be encrypted in the XML document. + _rsaKey = new RSACryptoServiceProvider(_cspParams); - SystemEvents.PowerModeChanged += OnPowerChange; + //SystemEvents.PowerModeChanged += OnPowerChange; return about; } - public int OnDockablePanelCreated(Control panel) { @@ -103,6 +106,14 @@ private void DrawPanel(object sender, PaintEventArgs e) e.Graphics.Clear(bg); panel.Cursor = Cursors.Hand; + if(_runOnce) + { + SpotifyWebAuth(); + _trackMissing = 1; + panel.Invalidate(); + _runOnce = false; + } + // re-draws when file is found? if (_auth == 1 && _trackMissing != 1) { @@ -119,35 +130,35 @@ private void DrawPanel(object sender, PaintEventArgs e) e.Graphics.DrawImage(image, new Point(10, 80)); webClient.Dispose(); - - if (CheckTrack(_trackID)) - { - TextRenderer.DrawText(e.Graphics, "Track Saved in Library", smallBold, new Point(80, 85), text1); - } - else - { - TextRenderer.DrawText(e.Graphics, "Track Not in Library", smallRegular, new Point(80, 85), text1); - } - - if (CheckAlbum(_albumID)) - { - TextRenderer.DrawText(e.Graphics, "Album Saved in Library", smallBold, new Point(80, 105), text1); - } - else - { - TextRenderer.DrawText(e.Graphics, "Album Not in Library", smallRegular, new Point(80, 105), text1); - } - - if (CheckArtist(_artistID)) - { - TextRenderer.DrawText(e.Graphics, "Artist Already Followed", smallBold, new Point(80, 125), text1); - } - else - { - TextRenderer.DrawText(e.Graphics, "Artist Not Followed", smallRegular, new Point(80, 125), text1); - } - + if (CheckTrack(_trackID)) + { + TextRenderer.DrawText(e.Graphics, "Track Saved in Library", smallBold, new Point(80, 85), text1); + } + else + { + TextRenderer.DrawText(e.Graphics, "Track Not in Library", smallRegular, new Point(80, 85), text1); + } + + if (CheckAlbum(_albumID)) + { + TextRenderer.DrawText(e.Graphics, "Album Saved in Library", smallBold, new Point(80, 105), text1); + } + else + { + TextRenderer.DrawText(e.Graphics, "Album Not in Library", smallRegular, new Point(80, 105), text1); + } + + if (CheckArtist(_artistID)) + { + TextRenderer.DrawText(e.Graphics, "Artist Already Followed", smallBold, new Point(80, 125), text1); + } + else + { + TextRenderer.DrawText(e.Graphics, "Artist Not Followed", smallRegular, new Point(80, 125), text1); + } + + } else if (_auth == 1 && _trackMissing == 1) { @@ -163,7 +174,6 @@ private void DrawPanel(object sender, PaintEventArgs e) } - } public List GetMenuItems() @@ -178,23 +188,22 @@ public List GetMenuItems() return list; } - - - private void OnPowerChange(object s, PowerModeChangedEventArgs e) - { - switch (e.Mode) - { - case PowerModes.Resume: + //private void OnPowerChange(object s, PowerModeChangedEventArgs e) + //{ + // switch (e.Mode) + // { + // case PowerModes.Resume: - _auth = 0; + // _auth = 0; - break; - } - } + // break; + // } + //} public void reAuthSpotify(object sender, EventArgs e) { - SpotifyWebAuth(false); + File.Delete(_path); + SpotifyWebAuth(); _trackMissing = 1; panel.Invalidate(); } @@ -207,7 +216,7 @@ private void PanelClick(object sender, EventArgs e) if (_auth == 0 && me.Button == System.Windows.Forms.MouseButtons.Left) { - SpotifyWebAuth(false); + SpotifyWebAuth(); _trackMissing = 1; panel.Invalidate(); @@ -237,26 +246,24 @@ private void PanelClick(object sender, EventArgs e) panel.Invalidate(); //panel.Paint += DrawPanel; } - + } else if (point.X > 80 && point.X < this.panel.Width && point.Y < 120 && point.Y > 110) { - + if (_albumLIB) { RemoveAlbum(); - GenerateAlbumList(); panel.Invalidate(); //panel.Paint += DrawPanel; } else { SaveAlbum(); - GenerateAlbumList(); panel.Invalidate(); //panel.Paint += DrawPanel; } - + } else if (point.X > 80 && point.X < this.panel.Width && point.Y < 100 && point.Y > 90) { @@ -273,73 +280,42 @@ private void PanelClick(object sender, EventArgs e) panel.Invalidate(); //panel.Paint += DrawPanel; } - + } - + } } - - public void ReceiveNotification(string sourceFileUrl, NotificationType type) + public async void ReceiveNotification(string sourceFileUrl, NotificationType type) { switch (type) { - // Window stacking doesn't work with custom panel plugins. - - //case NotificationType.PluginStartup: - - // panel.Invalidate(); - // //panel.Paint += DrawPanel; - - - // break; case NotificationType.TrackChanged: - - if(_runOnce == true) - { - authTimer = new System.Threading.Timer( - new TimerCallback(TickTimer), - null, - 3600000, - 3600000); - - GenerateAlbumList(); - _runOnce = false; - } - - _trackMissing = 0; _num = 0; _searchTerm = mbApiInterface.NowPlaying_GetFileTag(MetaDataType.TrackTitle) + " + " + mbApiInterface.NowPlaying_GetFileTag(MetaDataType.Artist); - if (_auth == 1) { mbApiInterface.MB_RefreshPanels(); - TrackSearch(); + await TrackSearch(); } - panel.Invalidate(); - //panel.Paint += DrawPanel; break; } } - - public bool Configure(IntPtr panelHandle) { - - return true; } @@ -350,7 +326,7 @@ public void SaveSettings() public void Close(PluginCloseReason reason) { - SystemEvents.PowerModeChanged -= OnPowerChange; + //SystemEvents.PowerModeChanged -= OnPowerChange; } public void Uninstall() diff --git a/Source Files/Properties/AssemblyInfo.cs b/Source Files/Properties/AssemblyInfo.cs index 9754288..a954099 100644 --- a/Source Files/Properties/AssemblyInfo.cs +++ b/Source Files/Properties/AssemblyInfo.cs @@ -8,9 +8,9 @@ [assembly: AssemblyTitle("mb_Spotify_Plugin")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Zachary Cohen")] +[assembly: AssemblyCompany("zkhcohen")] [assembly: AssemblyProduct("mb_Spotify_Plugin")] -[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.2.0")] -[assembly: AssemblyFileVersion("2.0.2.0")] +[assembly: AssemblyVersion("3.0.5.0")] +[assembly: AssemblyFileVersion("3.0.5.0")] diff --git a/Source Files/SpotifyAPI.Web.Auth/AssemblyInfo.cs b/Source Files/SpotifyAPI.Web.Auth/AssemblyInfo.cs new file mode 100644 index 0000000..661cd81 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SpotifyAPI.Web.Tests")] +[assembly: CLSCompliant(true)] diff --git a/Source Files/SpotifyAPI.Web.Auth/AuthException.cs b/Source Files/SpotifyAPI.Web.Auth/AuthException.cs new file mode 100644 index 0000000..aec059f --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/AuthException.cs @@ -0,0 +1,20 @@ +namespace SpotifyAPI.Web.Auth +{ + [System.Serializable] + public class AuthException : System.Exception + { + public AuthException(string? error, string? state) + { + Error = error; + State = state; + } + public AuthException(string message) : base(message) { } + public AuthException(string message, System.Exception inner) : base(message, inner) { } + protected AuthException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? Error { get; set; } + public string? State { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs b/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs deleted file mode 100644 index 84719e6..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Windows.Forms; -using System.Management.Automation; -using System.Collections.ObjectModel; -using System.Text.RegularExpressions; - -namespace SpotifyAPI.Web.Auth -{ - internal static class AuthUtil - { - public static void OpenBrowser(string url, bool AR) - { -#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 - - - if (AR == true) - { - - - var proc = new Process(); - proc.StartInfo.FileName = "powershell.exe"; - proc.StartInfo.Arguments = $"$ie = new-object -com \"InternetExplorer.Application\"; $ie.navigate(\"\"\" {url} \"\"\"); $ie.visible = $true"; - proc.StartInfo.CreateNoWindow = true; - proc.StartInfo.RedirectStandardError = true; - proc.StartInfo.UseShellExecute = false; - - - if (!proc.Start()) - { - - MessageBox.Show("Powershell didn't start properly."); - return; - - } - - var reader = proc.StandardError; - string line; - while ((line = reader.ReadLine()) != null) System.Console.WriteLine(line); - - proc.Close(); - - - } - else - { - url = url.Replace("&", "^&"); - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); - - } -#endif - } - - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs b/Source Files/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs deleted file mode 100644 index b8687c3..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs +++ /dev/null @@ -1,134 +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 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) - }; - - HttpClient client = new HttpClient(); - 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 async Task ExchangeCode(string code) - { - List> args = new List> - { - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("code", code), - new KeyValuePair("redirect_uri", RedirectUri) - }; - - HttpClient client = new HttpClient(); - 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 this.StringResponseAsync("OK - This window can be closed now"); - } - - [WebApiHandler(HttpVerbs.Post, "/")] - public async Task PostValues() - { - Dictionary formParams = await this.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 this.Redirect(uri, false); - } - - public AuthorizationCodeAuthController(IHttpContext context) : base(context) - { - } - } -} diff --git a/Source Files/SpotifyAPI.Web.Auth/BrowserUtil.cs b/Source Files/SpotifyAPI.Web.Auth/BrowserUtil.cs new file mode 100644 index 0000000..b8cda77 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/BrowserUtil.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System; + +namespace SpotifyAPI.Web.Auth +{ + public static class BrowserUtil + { + public static void Open(Uri uri) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var uriStr = uri.ToString().Replace("&", "^&"); + Process.Start(new ProcessStartInfo($"cmd", $"/c start {uriStr}")); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", uri.ToString()); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", uri.ToString()); + } + } + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/CredentialsAuth.cs b/Source Files/SpotifyAPI.Web.Auth/CredentialsAuth.cs deleted file mode 100644 index 1da062b..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/CredentialsAuth.cs +++ /dev/null @@ -1,42 +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 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") - }; - - HttpClient client = new HttpClient(); - 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/Source Files/SpotifyAPI.Web.Auth/EmbedIOAuthServer.cs b/Source Files/SpotifyAPI.Web.Auth/EmbedIOAuthServer.cs new file mode 100644 index 0000000..e1b7c73 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/EmbedIOAuthServer.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using System.Threading; +using System.Web; +using System.Globalization; +using System.Text; +using System; +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Actions; + +namespace SpotifyAPI.Web.Auth +{ + public class EmbedIOAuthServer : IAuthServer + { + public event Func? AuthorizationCodeReceived; + public event Func? ImplictGrantReceived; + public event Func? PkceReceived; + + private const string AssetsResourcePath = "SpotifyAPI.Web.Auth.Resources.auth_assets"; + private const string DefaultResourcePath = "SpotifyAPI.Web.Auth.Resources.default_site"; + + private CancellationTokenSource? _cancelTokenSource; + private readonly WebServer _webServer; + + public EmbedIOAuthServer(Uri baseUri, int port) + : this(baseUri, port, Assembly.GetExecutingAssembly(), DefaultResourcePath) { } + + public EmbedIOAuthServer(Uri baseUri, int port, Assembly resourceAssembly, string resourcePath) + { + Ensure.ArgumentNotNull(baseUri, nameof(baseUri)); + + BaseUri = baseUri; + Port = port; + + _webServer = new WebServer(port) + .WithModule(new ActionModule("/callback", HttpVerbs.Post, (ctx) => + { + var query = ctx.Request.QueryString; + var error = query["error"]; + if (error != null) + { + throw new AuthException(error, query["state"]); + } + + PkceReceived?.Invoke(this, new PkceResponse(query["code"]!) + { + State = query["state"] + }); + + return ctx.SendStringAsync("OK", "text/plain", Encoding.UTF8); + })) + .WithModule(new ActionModule("/", HttpVerbs.Post, (ctx) => + { + var query = ctx.Request.QueryString; + var error = query["error"]; + if (error != null) + { + throw new AuthException(error, query["state"]); + } + + var requestType = query.Get("request_type"); + if (requestType == "token") + { + ImplictGrantReceived?.Invoke(this, new ImplictGrantResponse( + query["access_token"]!, query["token_type"]!, int.Parse(query["expires_in"]!) + ) + { + State = query["state"] + }); + } + if (requestType == "code") + { + AuthorizationCodeReceived?.Invoke(this, new AuthorizationCodeResponse(query["code"]!) + { + State = query["state"] + }); + } + return ctx.SendStringAsync("OK", "text/plain", Encoding.UTF8); + })) + .WithEmbeddedResources("/auth_assets", Assembly.GetExecutingAssembly(), AssetsResourcePath) + .WithEmbeddedResources(baseUri.AbsolutePath, resourceAssembly, resourcePath); + } + + public Uri BaseUri { get; } + public int Port { get; } + + public Task Start() + { + _cancelTokenSource = new CancellationTokenSource(); + _webServer.Start(_cancelTokenSource.Token); + return Task.CompletedTask; + } + + public Task Stop() + { + _cancelTokenSource?.Cancel(); + return Task.CompletedTask; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _webServer?.Dispose(); + } + } + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/IAuthServer.cs b/Source Files/SpotifyAPI.Web.Auth/IAuthServer.cs new file mode 100644 index 0000000..1347121 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/IAuthServer.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Auth +{ + public interface IAuthServer : IDisposable + { + event Func AuthorizationCodeReceived; + + event Func ImplictGrantReceived; + + event Func PkceReceived; + + Task Start(); + Task Stop(); + + Uri BaseUri { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs b/Source Files/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs deleted file mode 100644 index c99809f..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs +++ /dev/null @@ -1,65 +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 this.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 this.StringResponseAsync("OK - This window can be closed now"); - } - - public ImplicitGrantAuthController(IHttpContext context) : base(context) - { - } - } -} diff --git a/Source Files/SpotifyAPI.Web.Auth/Models/Response/AuthorizationCodeResponse.cs b/Source Files/SpotifyAPI.Web.Auth/Models/Response/AuthorizationCodeResponse.cs new file mode 100644 index 0000000..b83bec2 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Models/Response/AuthorizationCodeResponse.cs @@ -0,0 +1,15 @@ +namespace SpotifyAPI.Web.Auth +{ + public class AuthorizationCodeResponse + { + public AuthorizationCodeResponse(string code) + { + Ensure.ArgumentNotNullOrEmptyString(code, nameof(code)); + + Code = code; + } + + public string Code { get; set; } = default!; + public string? State { get; set; } = default!; + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/Models/Response/ImplicitGrantResponse.cs b/Source Files/SpotifyAPI.Web.Auth/Models/Response/ImplicitGrantResponse.cs new file mode 100644 index 0000000..7f8e256 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Models/Response/ImplicitGrantResponse.cs @@ -0,0 +1,30 @@ +using System; + +namespace SpotifyAPI.Web.Auth +{ + public class ImplictGrantResponse + { + public ImplictGrantResponse(string accessToken, string tokenType, int expiresIn) + { + Ensure.ArgumentNotNullOrEmptyString(accessToken, nameof(accessToken)); + Ensure.ArgumentNotNullOrEmptyString(tokenType, nameof(tokenType)); + + AccessToken = accessToken; + TokenType = tokenType; + ExpiresIn = expiresIn; + } + + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } + public string? State { get; set; } = default!; + + /// + /// Auto-Initalized to UTC Now + /// + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; } + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/Models/Response/PkceResponse.cs b/Source Files/SpotifyAPI.Web.Auth/Models/Response/PkceResponse.cs new file mode 100644 index 0000000..6dcfdbb --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Models/Response/PkceResponse.cs @@ -0,0 +1,15 @@ +namespace SpotifyAPI.Web.Auth +{ + public class PkceResponse + { + public PkceResponse(string code) + { + Ensure.ArgumentNotNullOrEmptyString(code, nameof(code)); + + Code = code; + } + + public string Code { get; set; } = default!; + public string? State { get; set; } = default!; + } +} diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css deleted file mode 100644 index 59825f7..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css +++ /dev/null @@ -1 +0,0 @@ -/*! bulma.io v0.7.1 | MIT License | github.com/jgthms/bulma */@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.breadcrumb,.button,.delete,.file,.is-unselectable,.modal-close,.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous,.tabs{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link::after,.select:not(.is-multiple):not(.is-loading)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:center;transform-origin:center;width:.625em}.block:not(:last-child),.box:not(:last-child),.breadcrumb:not(:last-child),.content:not(:last-child),.highlight:not(:last-child),.level:not(:last-child),.message:not(:last-child),.notification:not(:last-child),.progress:not(:last-child),.subtitle:not(:last-child),.table-container:not(:last-child),.table:not(:last-child),.tabs:not(:last-child),.title:not(:last-child){margin-bottom:1.5rem}.delete,.modal-close{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(10,10,10,.2);border:none;border-radius:290486px;cursor:pointer;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:20px;max-height:20px;max-width:20px;min-height:20px;min-width:20px;outline:0;position:relative;vertical-align:top;width:20px}.delete::after,.delete::before,.modal-close::after,.modal-close::before{background-color:#fff;content:"";display:block;left:50%;position:absolute;top:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);-webkit-transform-origin:center center;transform-origin:center center}.delete::before,.modal-close::before{height:2px;width:50%}.delete::after,.modal-close::after{height:50%;width:2px}.delete:focus,.delete:hover,.modal-close:focus,.modal-close:hover{background-color:rgba(10,10,10,.3)}.delete:active,.modal-close:active{background-color:rgba(10,10,10,.4)}.is-small.delete,.is-small.modal-close{height:16px;max-height:16px;max-width:16px;min-height:16px;min-width:16px;width:16px}.is-medium.delete,.is-medium.modal-close{height:24px;max-height:24px;max-width:24px;min-height:24px;min-width:24px;width:24px}.is-large.delete,.is-large.modal-close{height:32px;max-height:32px;max-width:32px;min-height:32px;min-width:32px;width:32px}.button.is-loading::after,.control.is-loading::after,.loader,.select.is-loading::after{-webkit-animation:spinAround .5s infinite linear;animation:spinAround .5s infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:1em;position:relative;width:1em}.hero-video,.image.is-16by9 img,.image.is-1by1 img,.image.is-1by2 img,.image.is-1by3 img,.image.is-2by1 img,.image.is-2by3 img,.image.is-3by1 img,.image.is-3by2 img,.image.is-3by4 img,.image.is-3by5 img,.image.is-4by3 img,.image.is-4by5 img,.image.is-5by3 img,.image.is-5by4 img,.image.is-9by16 img,.image.is-square img,.is-overlay,.modal,.modal-background{bottom:0;left:0;position:absolute;right:0;top:0}.button,.file-cta,.file-name,.input,.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous,.select select,.textarea{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.25em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.375em - 1px);padding-left:calc(.625em - 1px);padding-right:calc(.625em - 1px);padding-top:calc(.375em - 1px);position:relative;vertical-align:top}.button:active,.button:focus,.file-cta:active,.file-cta:focus,.file-name:active,.file-name:focus,.input:active,.input:focus,.is-active.button,.is-active.file-cta,.is-active.file-name,.is-active.input,.is-active.pagination-ellipsis,.is-active.pagination-link,.is-active.pagination-next,.is-active.pagination-previous,.is-active.textarea,.is-focused.button,.is-focused.file-cta,.is-focused.file-name,.is-focused.input,.is-focused.pagination-ellipsis,.is-focused.pagination-link,.is-focused.pagination-next,.is-focused.pagination-previous,.is-focused.textarea,.pagination-ellipsis:active,.pagination-ellipsis:focus,.pagination-link:active,.pagination-link:focus,.pagination-next:active,.pagination-next:focus,.pagination-previous:active,.pagination-previous:focus,.select select.is-active,.select select.is-focused,.select select:active,.select select:focus,.textarea:active,.textarea:focus{outline:0}.button[disabled],.file-cta[disabled],.file-name[disabled],.input[disabled],.pagination-ellipsis[disabled],.pagination-link[disabled],.pagination-next[disabled],.pagination-previous[disabled],.select select[disabled],.textarea[disabled]{cursor:not-allowed}/*! minireset.css v0.0.3 | MIT License | github.com/jgthms/minireset.css */blockquote,body,dd,dl,dt,fieldset,figure,h1,h2,h3,h4,h5,h6,hr,html,iframe,legend,li,ol,p,pre,textarea,ul{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:400}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,::after,::before{box-sizing:inherit}audio,img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0;text-align:left}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1rem;font-weight:400;line-height:1.5}a{color:#3273dc;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#ff3860;font-size:.875em;font-weight:400;padding:.25em .5em .25em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#4a4a4a;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{text-align:left;vertical-align:top}table th{color:#363636}.is-clearfix::after{clear:both;content:" ";display:table}.is-pulled-left{float:left!important}.is-pulled-right{float:right!important}.is-clipped{overflow:hidden!important}.is-size-1{font-size:3rem!important}.is-size-2{font-size:2.5rem!important}.is-size-3{font-size:2rem!important}.is-size-4{font-size:1.5rem!important}.is-size-5{font-size:1.25rem!important}.is-size-6{font-size:1rem!important}.is-size-7{font-size:.75rem!important}@media screen and (max-width:768px){.is-size-1-mobile{font-size:3rem!important}.is-size-2-mobile{font-size:2.5rem!important}.is-size-3-mobile{font-size:2rem!important}.is-size-4-mobile{font-size:1.5rem!important}.is-size-5-mobile{font-size:1.25rem!important}.is-size-6-mobile{font-size:1rem!important}.is-size-7-mobile{font-size:.75rem!important}}@media screen and (min-width:769px),print{.is-size-1-tablet{font-size:3rem!important}.is-size-2-tablet{font-size:2.5rem!important}.is-size-3-tablet{font-size:2rem!important}.is-size-4-tablet{font-size:1.5rem!important}.is-size-5-tablet{font-size:1.25rem!important}.is-size-6-tablet{font-size:1rem!important}.is-size-7-tablet{font-size:.75rem!important}}@media screen and (max-width:1087px){.is-size-1-touch{font-size:3rem!important}.is-size-2-touch{font-size:2.5rem!important}.is-size-3-touch{font-size:2rem!important}.is-size-4-touch{font-size:1.5rem!important}.is-size-5-touch{font-size:1.25rem!important}.is-size-6-touch{font-size:1rem!important}.is-size-7-touch{font-size:.75rem!important}}@media screen and (min-width:1088px){.is-size-1-desktop{font-size:3rem!important}.is-size-2-desktop{font-size:2.5rem!important}.is-size-3-desktop{font-size:2rem!important}.is-size-4-desktop{font-size:1.5rem!important}.is-size-5-desktop{font-size:1.25rem!important}.is-size-6-desktop{font-size:1rem!important}.is-size-7-desktop{font-size:.75rem!important}}@media screen and (min-width:1280px){.is-size-1-widescreen{font-size:3rem!important}.is-size-2-widescreen{font-size:2.5rem!important}.is-size-3-widescreen{font-size:2rem!important}.is-size-4-widescreen{font-size:1.5rem!important}.is-size-5-widescreen{font-size:1.25rem!important}.is-size-6-widescreen{font-size:1rem!important}.is-size-7-widescreen{font-size:.75rem!important}}@media screen and (min-width:1472px){.is-size-1-fullhd{font-size:3rem!important}.is-size-2-fullhd{font-size:2.5rem!important}.is-size-3-fullhd{font-size:2rem!important}.is-size-4-fullhd{font-size:1.5rem!important}.is-size-5-fullhd{font-size:1.25rem!important}.is-size-6-fullhd{font-size:1rem!important}.is-size-7-fullhd{font-size:.75rem!important}}.has-text-centered{text-align:center!important}.has-text-justified{text-align:justify!important}.has-text-left{text-align:left!important}.has-text-right{text-align:right!important}@media screen and (max-width:768px){.has-text-centered-mobile{text-align:center!important}}@media screen and (min-width:769px),print{.has-text-centered-tablet{text-align:center!important}}@media screen and (min-width:769px) and (max-width:1087px){.has-text-centered-tablet-only{text-align:center!important}}@media screen and (max-width:1087px){.has-text-centered-touch{text-align:center!important}}@media screen and (min-width:1088px){.has-text-centered-desktop{text-align:center!important}}@media screen and (min-width:1088px) and (max-width:1279px){.has-text-centered-desktop-only{text-align:center!important}}@media screen and (min-width:1280px){.has-text-centered-widescreen{text-align:center!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-centered-widescreen-only{text-align:center!important}}@media screen and (min-width:1472px){.has-text-centered-fullhd{text-align:center!important}}@media screen and (max-width:768px){.has-text-justified-mobile{text-align:justify!important}}@media screen and (min-width:769px),print{.has-text-justified-tablet{text-align:justify!important}}@media screen and (min-width:769px) and (max-width:1087px){.has-text-justified-tablet-only{text-align:justify!important}}@media screen and (max-width:1087px){.has-text-justified-touch{text-align:justify!important}}@media screen and (min-width:1088px){.has-text-justified-desktop{text-align:justify!important}}@media screen and (min-width:1088px) and (max-width:1279px){.has-text-justified-desktop-only{text-align:justify!important}}@media screen and (min-width:1280px){.has-text-justified-widescreen{text-align:justify!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-justified-widescreen-only{text-align:justify!important}}@media screen and (min-width:1472px){.has-text-justified-fullhd{text-align:justify!important}}@media screen and (max-width:768px){.has-text-left-mobile{text-align:left!important}}@media screen and (min-width:769px),print{.has-text-left-tablet{text-align:left!important}}@media screen and (min-width:769px) and (max-width:1087px){.has-text-left-tablet-only{text-align:left!important}}@media screen and (max-width:1087px){.has-text-left-touch{text-align:left!important}}@media screen and (min-width:1088px){.has-text-left-desktop{text-align:left!important}}@media screen and (min-width:1088px) and (max-width:1279px){.has-text-left-desktop-only{text-align:left!important}}@media screen and (min-width:1280px){.has-text-left-widescreen{text-align:left!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-left-widescreen-only{text-align:left!important}}@media screen and (min-width:1472px){.has-text-left-fullhd{text-align:left!important}}@media screen and (max-width:768px){.has-text-right-mobile{text-align:right!important}}@media screen and (min-width:769px),print{.has-text-right-tablet{text-align:right!important}}@media screen and (min-width:769px) and (max-width:1087px){.has-text-right-tablet-only{text-align:right!important}}@media screen and (max-width:1087px){.has-text-right-touch{text-align:right!important}}@media screen and (min-width:1088px){.has-text-right-desktop{text-align:right!important}}@media screen and (min-width:1088px) and (max-width:1279px){.has-text-right-desktop-only{text-align:right!important}}@media screen and (min-width:1280px){.has-text-right-widescreen{text-align:right!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-right-widescreen-only{text-align:right!important}}@media screen and (min-width:1472px){.has-text-right-fullhd{text-align:right!important}}.is-capitalized{text-transform:capitalize!important}.is-lowercase{text-transform:lowercase!important}.is-uppercase{text-transform:uppercase!important}.is-italic{font-style:italic!important}.has-text-white{color:#fff!important}a.has-text-white:focus,a.has-text-white:hover{color:#e6e6e6!important}.has-background-white{background-color:#fff!important}.has-text-black{color:#0a0a0a!important}a.has-text-black:focus,a.has-text-black:hover{color:#000!important}.has-background-black{background-color:#0a0a0a!important}.has-text-light{color:#f5f5f5!important}a.has-text-light:focus,a.has-text-light:hover{color:#dbdbdb!important}.has-background-light{background-color:#f5f5f5!important}.has-text-dark{color:#363636!important}a.has-text-dark:focus,a.has-text-dark:hover{color:#1c1c1c!important}.has-background-dark{background-color:#363636!important}.has-text-primary{color:#00d1b2!important}a.has-text-primary:focus,a.has-text-primary:hover{color:#009e86!important}.has-background-primary{background-color:#00d1b2!important}.has-text-link{color:#3273dc!important}a.has-text-link:focus,a.has-text-link:hover{color:#205bbc!important}.has-background-link{background-color:#3273dc!important}.has-text-info{color:#209cee!important}a.has-text-info:focus,a.has-text-info:hover{color:#0f81cc!important}.has-background-info{background-color:#209cee!important}.has-text-success{color:#23d160!important}a.has-text-success:focus,a.has-text-success:hover{color:#1ca64c!important}.has-background-success{background-color:#23d160!important}.has-text-warning{color:#ffdd57!important}a.has-text-warning:focus,a.has-text-warning:hover{color:#ffd324!important}.has-background-warning{background-color:#ffdd57!important}.has-text-danger{color:#ff3860!important}a.has-text-danger:focus,a.has-text-danger:hover{color:#ff0537!important}.has-background-danger{background-color:#ff3860!important}.has-text-black-bis{color:#121212!important}.has-background-black-bis{background-color:#121212!important}.has-text-black-ter{color:#242424!important}.has-background-black-ter{background-color:#242424!important}.has-text-grey-darker{color:#363636!important}.has-background-grey-darker{background-color:#363636!important}.has-text-grey-dark{color:#4a4a4a!important}.has-background-grey-dark{background-color:#4a4a4a!important}.has-text-grey{color:#7a7a7a!important}.has-background-grey{background-color:#7a7a7a!important}.has-text-grey-light{color:#b5b5b5!important}.has-background-grey-light{background-color:#b5b5b5!important}.has-text-grey-lighter{color:#dbdbdb!important}.has-background-grey-lighter{background-color:#dbdbdb!important}.has-text-white-ter{color:#f5f5f5!important}.has-background-white-ter{background-color:#f5f5f5!important}.has-text-white-bis{color:#fafafa!important}.has-background-white-bis{background-color:#fafafa!important}.has-text-weight-light{font-weight:300!important}.has-text-weight-normal{font-weight:400!important}.has-text-weight-semibold{font-weight:600!important}.has-text-weight-bold{font-weight:700!important}.is-block{display:block!important}@media screen and (max-width:768px){.is-block-mobile{display:block!important}}@media screen and (min-width:769px),print{.is-block-tablet{display:block!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-block-tablet-only{display:block!important}}@media screen and (max-width:1087px){.is-block-touch{display:block!important}}@media screen and (min-width:1088px){.is-block-desktop{display:block!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-block-desktop-only{display:block!important}}@media screen and (min-width:1280px){.is-block-widescreen{display:block!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-block-widescreen-only{display:block!important}}@media screen and (min-width:1472px){.is-block-fullhd{display:block!important}}.is-flex{display:flex!important}@media screen and (max-width:768px){.is-flex-mobile{display:flex!important}}@media screen and (min-width:769px),print{.is-flex-tablet{display:flex!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-flex-tablet-only{display:flex!important}}@media screen and (max-width:1087px){.is-flex-touch{display:flex!important}}@media screen and (min-width:1088px){.is-flex-desktop{display:flex!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-flex-desktop-only{display:flex!important}}@media screen and (min-width:1280px){.is-flex-widescreen{display:flex!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-flex-widescreen-only{display:flex!important}}@media screen and (min-width:1472px){.is-flex-fullhd{display:flex!important}}.is-inline{display:inline!important}@media screen and (max-width:768px){.is-inline-mobile{display:inline!important}}@media screen and (min-width:769px),print{.is-inline-tablet{display:inline!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-inline-tablet-only{display:inline!important}}@media screen and (max-width:1087px){.is-inline-touch{display:inline!important}}@media screen and (min-width:1088px){.is-inline-desktop{display:inline!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-inline-desktop-only{display:inline!important}}@media screen and (min-width:1280px){.is-inline-widescreen{display:inline!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-widescreen-only{display:inline!important}}@media screen and (min-width:1472px){.is-inline-fullhd{display:inline!important}}.is-inline-block{display:inline-block!important}@media screen and (max-width:768px){.is-inline-block-mobile{display:inline-block!important}}@media screen and (min-width:769px),print{.is-inline-block-tablet{display:inline-block!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-inline-block-tablet-only{display:inline-block!important}}@media screen and (max-width:1087px){.is-inline-block-touch{display:inline-block!important}}@media screen and (min-width:1088px){.is-inline-block-desktop{display:inline-block!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-inline-block-desktop-only{display:inline-block!important}}@media screen and (min-width:1280px){.is-inline-block-widescreen{display:inline-block!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-block-widescreen-only{display:inline-block!important}}@media screen and (min-width:1472px){.is-inline-block-fullhd{display:inline-block!important}}.is-inline-flex{display:inline-flex!important}@media screen and (max-width:768px){.is-inline-flex-mobile{display:inline-flex!important}}@media screen and (min-width:769px),print{.is-inline-flex-tablet{display:inline-flex!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-inline-flex-tablet-only{display:inline-flex!important}}@media screen and (max-width:1087px){.is-inline-flex-touch{display:inline-flex!important}}@media screen and (min-width:1088px){.is-inline-flex-desktop{display:inline-flex!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-inline-flex-desktop-only{display:inline-flex!important}}@media screen and (min-width:1280px){.is-inline-flex-widescreen{display:inline-flex!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-flex-widescreen-only{display:inline-flex!important}}@media screen and (min-width:1472px){.is-inline-flex-fullhd{display:inline-flex!important}}.is-hidden{display:none!important}@media screen and (max-width:768px){.is-hidden-mobile{display:none!important}}@media screen and (min-width:769px),print{.is-hidden-tablet{display:none!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-hidden-tablet-only{display:none!important}}@media screen and (max-width:1087px){.is-hidden-touch{display:none!important}}@media screen and (min-width:1088px){.is-hidden-desktop{display:none!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-hidden-desktop-only{display:none!important}}@media screen and (min-width:1280px){.is-hidden-widescreen{display:none!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-hidden-widescreen-only{display:none!important}}@media screen and (min-width:1472px){.is-hidden-fullhd{display:none!important}}.is-invisible{visibility:hidden!important}@media screen and (max-width:768px){.is-invisible-mobile{visibility:hidden!important}}@media screen and (min-width:769px),print{.is-invisible-tablet{visibility:hidden!important}}@media screen and (min-width:769px) and (max-width:1087px){.is-invisible-tablet-only{visibility:hidden!important}}@media screen and (max-width:1087px){.is-invisible-touch{visibility:hidden!important}}@media screen and (min-width:1088px){.is-invisible-desktop{visibility:hidden!important}}@media screen and (min-width:1088px) and (max-width:1279px){.is-invisible-desktop-only{visibility:hidden!important}}@media screen and (min-width:1280px){.is-invisible-widescreen{visibility:hidden!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-invisible-widescreen-only{visibility:hidden!important}}@media screen and (min-width:1472px){.is-invisible-fullhd{visibility:hidden!important}}.is-marginless{margin:0!important}.is-paddingless{padding:0!important}.is-radiusless{border-radius:0!important}.is-shadowless{box-shadow:none!important}.box{background-color:#fff;border-radius:6px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;display:block;padding:1.25rem}a.box:focus,a.box:hover{box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px #3273dc}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #3273dc}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.375em - 1px);padding-left:.75em;padding-right:.75em;padding-top:calc(.375em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-large,.button .icon.is-medium,.button .icon.is-small{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-.375em - 1px);margin-right:.1875em}.button .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:calc(-.375em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-.375em - 1px);margin-right:calc(-.375em - 1px)}.button.is-hovered,.button:hover{border-color:#b5b5b5;color:#363636}.button.is-focused,.button:focus{border-color:#3273dc;color:#363636}.button.is-focused:not(:active),.button:focus:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-active,.button:active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#4a4a4a;text-decoration:underline}.button.is-text.is-focused,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text:hover{background-color:#f5f5f5;color:#363636}.button.is-text.is-active,.button.is-text:active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled]{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white.is-hovered,.button.is-white:hover{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white.is-focused,.button.is-white:focus{border-color:transparent;color:#0a0a0a}.button.is-white.is-focused:not(:active),.button.is-white:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white.is-active,.button.is-white:active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled]{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted:hover{background-color:#000}.button.is-white.is-inverted[disabled]{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined:focus,.button.is-white.is-outlined:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined:hover{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black.is-hovered,.button.is-black:hover{background-color:#040404;border-color:transparent;color:#fff}.button.is-black.is-focused,.button.is-black:focus{border-color:transparent;color:#fff}.button.is-black.is-focused:not(:active),.button.is-black:focus:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black.is-active,.button.is-black:active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled]{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted:hover{background-color:#f2f2f2}.button.is-black.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined:focus,.button.is-black.is-outlined:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-outlined[disabled]{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined:hover{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:#363636}.button.is-light.is-hovered,.button.is-light:hover{background-color:#eee;border-color:transparent;color:#363636}.button.is-light.is-focused,.button.is-light:focus{border-color:transparent;color:#363636}.button.is-light.is-focused:not(:active),.button.is-light:focus:not(:active){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light.is-active,.button.is-light:active{background-color:#e8e8e8;border-color:transparent;color:#363636}.button.is-light[disabled]{background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted:hover{background-color:#292929}.button.is-light.is-inverted[disabled]{background-color:#363636;border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined:focus,.button.is-light.is-outlined:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.button.is-light.is-outlined.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-outlined[disabled]{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-light.is-inverted.is-outlined:focus,.button.is-light.is-inverted.is-outlined:hover{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark{background-color:#363636;border-color:transparent;color:#f5f5f5}.button.is-dark.is-hovered,.button.is-dark:hover{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused,.button.is-dark:focus{border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused:not(:active),.button.is-dark:focus:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark.is-active,.button.is-dark:active{background-color:#292929;border-color:transparent;color:#f5f5f5}.button.is-dark[disabled]{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted:hover{background-color:#e8e8e8}.button.is-dark.is-inverted[disabled]{background-color:#f5f5f5;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-outlined[disabled]{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined:hover{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-primary{background-color:#00d1b2;border-color:transparent;color:#fff}.button.is-primary.is-hovered,.button.is-primary:hover{background-color:#00c4a7;border-color:transparent;color:#fff}.button.is-primary.is-focused,.button.is-primary:focus{border-color:transparent;color:#fff}.button.is-primary.is-focused:not(:active),.button.is-primary:focus:not(:active){box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.button.is-primary.is-active,.button.is-primary:active{background-color:#00b89c;border-color:transparent;color:#fff}.button.is-primary[disabled]{background-color:#00d1b2;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted:hover{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#00d1b2}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined{background-color:transparent;border-color:#00d1b2;color:#00d1b2}.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined:hover{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #00d1b2 #00d1b2!important}.button.is-primary.is-outlined[disabled]{background-color:transparent;border-color:#00d1b2;box-shadow:none;color:#00d1b2}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined:hover{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link{background-color:#3273dc;border-color:transparent;color:#fff}.button.is-link.is-hovered,.button.is-link:hover{background-color:#276cda;border-color:transparent;color:#fff}.button.is-link.is-focused,.button.is-link:focus{border-color:transparent;color:#fff}.button.is-link.is-focused:not(:active),.button.is-link:focus:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-link.is-active,.button.is-link:active{background-color:#2366d1;border-color:transparent;color:#fff}.button.is-link[disabled]{background-color:#3273dc;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#3273dc}.button.is-link.is-inverted:hover{background-color:#f2f2f2}.button.is-link.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#3273dc}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;color:#3273dc}.button.is-link.is-outlined:focus,.button.is-link.is-outlined:hover{background-color:#3273dc;border-color:#3273dc;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-outlined[disabled]{background-color:transparent;border-color:#3273dc;box-shadow:none;color:#3273dc}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined:hover{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info{background-color:#209cee;border-color:transparent;color:#fff}.button.is-info.is-hovered,.button.is-info:hover{background-color:#1496ed;border-color:transparent;color:#fff}.button.is-info.is-focused,.button.is-info:focus{border-color:transparent;color:#fff}.button.is-info.is-focused:not(:active),.button.is-info:focus:not(:active){box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.button.is-info.is-active,.button.is-info:active{background-color:#118fe4;border-color:transparent;color:#fff}.button.is-info[disabled]{background-color:#209cee;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#209cee}.button.is-info.is-inverted:hover{background-color:#f2f2f2}.button.is-info.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#209cee}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined{background-color:transparent;border-color:#209cee;color:#209cee}.button.is-info.is-outlined:focus,.button.is-info.is-outlined:hover{background-color:#209cee;border-color:#209cee;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #209cee #209cee!important}.button.is-info.is-outlined[disabled]{background-color:transparent;border-color:#209cee;box-shadow:none;color:#209cee}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined:hover{background-color:#fff;color:#209cee}.button.is-info.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success{background-color:#23d160;border-color:transparent;color:#fff}.button.is-success.is-hovered,.button.is-success:hover{background-color:#22c65b;border-color:transparent;color:#fff}.button.is-success.is-focused,.button.is-success:focus{border-color:transparent;color:#fff}.button.is-success.is-focused:not(:active),.button.is-success:focus:not(:active){box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.button.is-success.is-active,.button.is-success:active{background-color:#20bc56;border-color:transparent;color:#fff}.button.is-success[disabled]{background-color:#23d160;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#23d160}.button.is-success.is-inverted:hover{background-color:#f2f2f2}.button.is-success.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#23d160}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined{background-color:transparent;border-color:#23d160;color:#23d160}.button.is-success.is-outlined:focus,.button.is-success.is-outlined:hover{background-color:#23d160;border-color:#23d160;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #23d160 #23d160!important}.button.is-success.is-outlined[disabled]{background-color:transparent;border-color:#23d160;box-shadow:none;color:#23d160}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined:hover{background-color:#fff;color:#23d160}.button.is-success.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-hovered,.button.is-warning:hover{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused,.button.is-warning:focus{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused:not(:active),.button.is-warning:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning.is-active,.button.is-warning:active{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning[disabled]{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted:hover{background-color:rgba(0,0,0,.7)}.button.is-warning.is-inverted[disabled]{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined:hover{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-outlined[disabled]{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined:hover{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-danger{background-color:#ff3860;border-color:transparent;color:#fff}.button.is-danger.is-hovered,.button.is-danger:hover{background-color:#ff2b56;border-color:transparent;color:#fff}.button.is-danger.is-focused,.button.is-danger:focus{border-color:transparent;color:#fff}.button.is-danger.is-focused:not(:active),.button.is-danger:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.button.is-danger.is-active,.button.is-danger:active{background-color:#ff1f4b;border-color:transparent;color:#fff}.button.is-danger[disabled]{background-color:#ff3860;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted:hover{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled]{background-color:#fff;border-color:transparent;box-shadow:none;color:#ff3860}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined{background-color:transparent;border-color:#ff3860;color:#ff3860}.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined:hover{background-color:#ff3860;border-color:#ff3860;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #ff3860 #ff3860!important}.button.is-danger.is-outlined[disabled]{background-color:transparent;border-color:#ff3860;box-shadow:none;color:#ff3860}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined:hover{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted.is-outlined[disabled]{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled]{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent!important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em / 2));top:calc(50% - (1em / 2));position:absolute!important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child){margin-right:.5rem}.buttons:last-child{margin-bottom:-.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button.is-hovered,.buttons.has-addons .button:hover{z-index:2}.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-focused,.buttons.has-addons .button.is-selected,.buttons.has-addons .button:active,.buttons.has-addons .button:focus{z-index:3}.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button.is-selected:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button:focus:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1}.buttons.is-centered{justify-content:center}.buttons.is-right{justify-content:flex-end}.container{margin:0 auto;position:relative}@media screen and (min-width:1088px){.container{max-width:960px;width:960px}.container.is-fluid{margin-left:64px;margin-right:64px;max-width:none;width:auto}}@media screen and (max-width:1279px){.container.is-widescreen{max-width:1152px;width:auto}}@media screen and (max-width:1471px){.container.is-fullhd{max-width:1344px;width:auto}}@media screen and (min-width:1280px){.container{max-width:1152px;width:1152px}}@media screen and (min-width:1472px){.container{max-width:1344px;width:1344px}}.content li+li{margin-top:.25em}.content blockquote:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content p:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child),.content ul:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style:decimal outside;margin-left:2em;margin-top:1em}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sub,.content sup{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636;text-align:left}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.input,.textarea{background-color:#fff;border-color:#dbdbdb;color:#363636;box-shadow:inset 0 1px 2px rgba(10,10,10,.1);max-width:100%;width:100%}.input::-moz-placeholder,.textarea::-moz-placeholder{color:rgba(54,54,54,.3)}.input::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:rgba(54,54,54,.3)}.input:-moz-placeholder,.textarea:-moz-placeholder{color:rgba(54,54,54,.3)}.input:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:rgba(54,54,54,.3)}.input.is-hovered,.input:hover,.textarea.is-hovered,.textarea:hover{border-color:#b5b5b5}.input.is-active,.input.is-focused,.input:active,.input:focus,.textarea.is-active,.textarea.is-focused,.textarea:active,.textarea:focus{border-color:#3273dc;box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.input[disabled],.textarea[disabled]{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.input[disabled]::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]::-webkit-input-placeholder,.textarea[disabled]::-webkit-input-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-moz-placeholder,.textarea[disabled]:-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-ms-input-placeholder,.textarea[disabled]:-ms-input-placeholder{color:rgba(122,122,122,.3)}.input[readonly],.textarea[readonly]{box-shadow:none}.input.is-white,.textarea.is-white{border-color:#fff}.input.is-white.is-active,.input.is-white.is-focused,.input.is-white:active,.input.is-white:focus,.textarea.is-white.is-active,.textarea.is-white.is-focused,.textarea.is-white:active,.textarea.is-white:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.input.is-black,.textarea.is-black{border-color:#0a0a0a}.input.is-black.is-active,.input.is-black.is-focused,.input.is-black:active,.input.is-black:focus,.textarea.is-black.is-active,.textarea.is-black.is-focused,.textarea.is-black:active,.textarea.is-black:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.input.is-light,.textarea.is-light{border-color:#f5f5f5}.input.is-light.is-active,.input.is-light.is-focused,.input.is-light:active,.input.is-light:focus,.textarea.is-light.is-active,.textarea.is-light.is-focused,.textarea.is-light:active,.textarea.is-light:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.input.is-dark,.textarea.is-dark{border-color:#363636}.input.is-dark.is-active,.input.is-dark.is-focused,.input.is-dark:active,.input.is-dark:focus,.textarea.is-dark.is-active,.textarea.is-dark.is-focused,.textarea.is-dark:active,.textarea.is-dark:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.input.is-primary,.textarea.is-primary{border-color:#00d1b2}.input.is-primary.is-active,.input.is-primary.is-focused,.input.is-primary:active,.input.is-primary:focus,.textarea.is-primary.is-active,.textarea.is-primary.is-focused,.textarea.is-primary:active,.textarea.is-primary:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.input.is-link,.textarea.is-link{border-color:#3273dc}.input.is-link.is-active,.input.is-link.is-focused,.input.is-link:active,.input.is-link:focus,.textarea.is-link.is-active,.textarea.is-link.is-focused,.textarea.is-link:active,.textarea.is-link:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.input.is-info,.textarea.is-info{border-color:#209cee}.input.is-info.is-active,.input.is-info.is-focused,.input.is-info:active,.input.is-info:focus,.textarea.is-info.is-active,.textarea.is-info.is-focused,.textarea.is-info:active,.textarea.is-info:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.input.is-success,.textarea.is-success{border-color:#23d160}.input.is-success.is-active,.input.is-success.is-focused,.input.is-success:active,.input.is-success:focus,.textarea.is-success.is-active,.textarea.is-success.is-focused,.textarea.is-success:active,.textarea.is-success:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.input.is-warning,.textarea.is-warning{border-color:#ffdd57}.input.is-warning.is-active,.input.is-warning.is-focused,.input.is-warning:active,.input.is-warning:focus,.textarea.is-warning.is-active,.textarea.is-warning.is-focused,.textarea.is-warning:active,.textarea.is-warning:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.input.is-danger,.textarea.is-danger{border-color:#ff3860}.input.is-danger.is-active,.input.is-danger.is-focused,.input.is-danger:active,.input.is-danger:focus,.textarea.is-danger.is-active,.textarea.is-danger.is-focused,.textarea.is-danger:active,.textarea.is-danger:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.input.is-small,.textarea.is-small{border-radius:2px;font-size:.75rem}.input.is-medium,.textarea.is-medium{font-size:1.25rem}.input.is-large,.textarea.is-large{font-size:1.5rem}.input.is-fullwidth,.textarea.is-fullwidth{display:block;width:100%}.input.is-inline,.textarea.is-inline{display:inline;width:auto}.input.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.input.is-static{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.textarea{display:block;max-width:100%;min-width:100%;padding:.625em;resize:vertical}.textarea:not([rows]){max-height:600px;min-height:120px}.textarea[rows]{height:initial}.textarea.has-fixed-size{resize:none}.checkbox,.radio{cursor:pointer;display:inline-block;line-height:1.25;position:relative}.checkbox input,.radio input{cursor:pointer}.checkbox:hover,.radio:hover{color:#363636}.checkbox[disabled],.radio[disabled]{color:#7a7a7a;cursor:not-allowed}.radio+.radio{margin-left:.5em}.select{display:inline-block;max-width:100%;position:relative;vertical-align:top}.select:not(.is-multiple){height:2.25em}.select:not(.is-multiple):not(.is-loading)::after{border-color:#3273dc;right:1.125em;z-index:4}.select.is-rounded select{border-radius:290486px;padding-left:1em}.select select{background-color:#fff;border-color:#dbdbdb;color:#363636;cursor:pointer;display:block;font-size:1em;max-width:100%;outline:0}.select select::-moz-placeholder{color:rgba(54,54,54,.3)}.select select::-webkit-input-placeholder{color:rgba(54,54,54,.3)}.select select:-moz-placeholder{color:rgba(54,54,54,.3)}.select select:-ms-input-placeholder{color:rgba(54,54,54,.3)}.select select.is-hovered,.select select:hover{border-color:#b5b5b5}.select select.is-active,.select select.is-focused,.select select:active,.select select:focus{border-color:#3273dc;box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select select[disabled]{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.select select[disabled]::-moz-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]::-webkit-input-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]:-moz-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]:-ms-input-placeholder{color:rgba(122,122,122,.3)}.select select::-ms-expand{display:none}.select select[disabled]:hover{border-color:#f5f5f5}.select select:not([multiple]){padding-right:2.5em}.select select[multiple]{height:initial;padding:0}.select select[multiple] option{padding:.5em 1em}.select:not(.is-multiple):not(.is-loading):hover::after{border-color:#363636}.select.is-white:not(:hover)::after{border-color:#fff}.select.is-white select{border-color:#fff}.select.is-white select.is-hovered,.select.is-white select:hover{border-color:#f2f2f2}.select.is-white select.is-active,.select.is-white select.is-focused,.select.is-white select:active,.select.is-white select:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.select.is-black:not(:hover)::after{border-color:#0a0a0a}.select.is-black select{border-color:#0a0a0a}.select.is-black select.is-hovered,.select.is-black select:hover{border-color:#000}.select.is-black select.is-active,.select.is-black select.is-focused,.select.is-black select:active,.select.is-black select:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.select.is-light:not(:hover)::after{border-color:#f5f5f5}.select.is-light select{border-color:#f5f5f5}.select.is-light select.is-hovered,.select.is-light select:hover{border-color:#e8e8e8}.select.is-light select.is-active,.select.is-light select.is-focused,.select.is-light select:active,.select.is-light select:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.select.is-dark:not(:hover)::after{border-color:#363636}.select.is-dark select{border-color:#363636}.select.is-dark select.is-hovered,.select.is-dark select:hover{border-color:#292929}.select.is-dark select.is-active,.select.is-dark select.is-focused,.select.is-dark select:active,.select.is-dark select:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.select.is-primary:not(:hover)::after{border-color:#00d1b2}.select.is-primary select{border-color:#00d1b2}.select.is-primary select.is-hovered,.select.is-primary select:hover{border-color:#00b89c}.select.is-primary select.is-active,.select.is-primary select.is-focused,.select.is-primary select:active,.select.is-primary select:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.select.is-link:not(:hover)::after{border-color:#3273dc}.select.is-link select{border-color:#3273dc}.select.is-link select.is-hovered,.select.is-link select:hover{border-color:#2366d1}.select.is-link select.is-active,.select.is-link select.is-focused,.select.is-link select:active,.select.is-link select:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select.is-info:not(:hover)::after{border-color:#209cee}.select.is-info select{border-color:#209cee}.select.is-info select.is-hovered,.select.is-info select:hover{border-color:#118fe4}.select.is-info select.is-active,.select.is-info select.is-focused,.select.is-info select:active,.select.is-info select:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.select.is-success:not(:hover)::after{border-color:#23d160}.select.is-success select{border-color:#23d160}.select.is-success select.is-hovered,.select.is-success select:hover{border-color:#20bc56}.select.is-success select.is-active,.select.is-success select.is-focused,.select.is-success select:active,.select.is-success select:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.select.is-warning:not(:hover)::after{border-color:#ffdd57}.select.is-warning select{border-color:#ffdd57}.select.is-warning select.is-hovered,.select.is-warning select:hover{border-color:#ffd83d}.select.is-warning select.is-active,.select.is-warning select.is-focused,.select.is-warning select:active,.select.is-warning select:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.select.is-danger:not(:hover)::after{border-color:#ff3860}.select.is-danger select{border-color:#ff3860}.select.is-danger select.is-hovered,.select.is-danger select:hover{border-color:#ff1f4b}.select.is-danger select.is-active,.select.is-danger select.is-focused,.select.is-danger select:active,.select.is-danger select:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.select.is-small{border-radius:2px;font-size:.75rem}.select.is-medium{font-size:1.25rem}.select.is-large{font-size:1.5rem}.select.is-disabled::after{border-color:#7a7a7a}.select.is-fullwidth{width:100%}.select.is-fullwidth select{width:100%}.select.is-loading::after{margin-top:0;position:absolute;right:.625em;top:.625em;-webkit-transform:none;transform:none}.select.is-loading.is-small:after{font-size:.75rem}.select.is-loading.is-medium:after{font-size:1.25rem}.select.is-loading.is-large:after{font-size:1.5rem}.file{align-items:stretch;display:flex;justify-content:flex-start;position:relative}.file.is-white .file-cta{background-color:#fff;border-color:transparent;color:#0a0a0a}.file.is-white.is-hovered .file-cta,.file.is-white:hover .file-cta{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.file.is-white.is-focused .file-cta,.file.is-white:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,255,255,.25);color:#0a0a0a}.file.is-white.is-active .file-cta,.file.is-white:active .file-cta{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.file.is-black .file-cta{background-color:#0a0a0a;border-color:transparent;color:#fff}.file.is-black.is-hovered .file-cta,.file.is-black:hover .file-cta{background-color:#040404;border-color:transparent;color:#fff}.file.is-black.is-focused .file-cta,.file.is-black:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(10,10,10,.25);color:#fff}.file.is-black.is-active .file-cta,.file.is-black:active .file-cta{background-color:#000;border-color:transparent;color:#fff}.file.is-light .file-cta{background-color:#f5f5f5;border-color:transparent;color:#363636}.file.is-light.is-hovered .file-cta,.file.is-light:hover .file-cta{background-color:#eee;border-color:transparent;color:#363636}.file.is-light.is-focused .file-cta,.file.is-light:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(245,245,245,.25);color:#363636}.file.is-light.is-active .file-cta,.file.is-light:active .file-cta{background-color:#e8e8e8;border-color:transparent;color:#363636}.file.is-dark .file-cta{background-color:#363636;border-color:transparent;color:#f5f5f5}.file.is-dark.is-hovered .file-cta,.file.is-dark:hover .file-cta{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.file.is-dark.is-focused .file-cta,.file.is-dark:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(54,54,54,.25);color:#f5f5f5}.file.is-dark.is-active .file-cta,.file.is-dark:active .file-cta{background-color:#292929;border-color:transparent;color:#f5f5f5}.file.is-primary .file-cta{background-color:#00d1b2;border-color:transparent;color:#fff}.file.is-primary.is-hovered .file-cta,.file.is-primary:hover .file-cta{background-color:#00c4a7;border-color:transparent;color:#fff}.file.is-primary.is-focused .file-cta,.file.is-primary:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(0,209,178,.25);color:#fff}.file.is-primary.is-active .file-cta,.file.is-primary:active .file-cta{background-color:#00b89c;border-color:transparent;color:#fff}.file.is-link .file-cta{background-color:#3273dc;border-color:transparent;color:#fff}.file.is-link.is-hovered .file-cta,.file.is-link:hover .file-cta{background-color:#276cda;border-color:transparent;color:#fff}.file.is-link.is-focused .file-cta,.file.is-link:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(50,115,220,.25);color:#fff}.file.is-link.is-active .file-cta,.file.is-link:active .file-cta{background-color:#2366d1;border-color:transparent;color:#fff}.file.is-info .file-cta{background-color:#209cee;border-color:transparent;color:#fff}.file.is-info.is-hovered .file-cta,.file.is-info:hover .file-cta{background-color:#1496ed;border-color:transparent;color:#fff}.file.is-info.is-focused .file-cta,.file.is-info:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(32,156,238,.25);color:#fff}.file.is-info.is-active .file-cta,.file.is-info:active .file-cta{background-color:#118fe4;border-color:transparent;color:#fff}.file.is-success .file-cta{background-color:#23d160;border-color:transparent;color:#fff}.file.is-success.is-hovered .file-cta,.file.is-success:hover .file-cta{background-color:#22c65b;border-color:transparent;color:#fff}.file.is-success.is-focused .file-cta,.file.is-success:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(35,209,96,.25);color:#fff}.file.is-success.is-active .file-cta,.file.is-success:active .file-cta{background-color:#20bc56;border-color:transparent;color:#fff}.file.is-warning .file-cta{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-hovered .file-cta,.file.is-warning:hover .file-cta{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-focused .file-cta,.file.is-warning:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,221,87,.25);color:rgba(0,0,0,.7)}.file.is-warning.is-active .file-cta,.file.is-warning:active .file-cta{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-danger .file-cta{background-color:#ff3860;border-color:transparent;color:#fff}.file.is-danger.is-hovered .file-cta,.file.is-danger:hover .file-cta{background-color:#ff2b56;border-color:transparent;color:#fff}.file.is-danger.is-focused .file-cta,.file.is-danger:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,56,96,.25);color:#fff}.file.is-danger.is-active .file-cta,.file.is-danger:active .file-cta{background-color:#ff1f4b;border-color:transparent;color:#fff}.file.is-small{font-size:.75rem}.file.is-medium{font-size:1.25rem}.file.is-medium .file-icon .fa{font-size:21px}.file.is-large{font-size:1.5rem}.file.is-large .file-icon .fa{font-size:28px}.file.has-name .file-cta{border-bottom-right-radius:0;border-top-right-radius:0}.file.has-name .file-name{border-bottom-left-radius:0;border-top-left-radius:0}.file.has-name.is-empty .file-cta{border-radius:4px}.file.has-name.is-empty .file-name{display:none}.file.is-boxed .file-label{flex-direction:column}.file.is-boxed .file-cta{flex-direction:column;height:auto;padding:1em 3em}.file.is-boxed .file-name{border-width:0 1px 1px}.file.is-boxed .file-icon{height:1.5em;width:1.5em}.file.is-boxed .file-icon .fa{font-size:21px}.file.is-boxed.is-small .file-icon .fa{font-size:14px}.file.is-boxed.is-medium .file-icon .fa{font-size:28px}.file.is-boxed.is-large .file-icon .fa{font-size:35px}.file.is-boxed.has-name .file-cta{border-radius:4px 4px 0 0}.file.is-boxed.has-name .file-name{border-radius:0 0 4px 4px;border-width:0 1px 1px}.file.is-centered{justify-content:center}.file.is-fullwidth .file-label{width:100%}.file.is-fullwidth .file-name{flex-grow:1;max-width:none}.file.is-right{justify-content:flex-end}.file.is-right .file-cta{border-radius:0 4px 4px 0}.file.is-right .file-name{border-radius:4px 0 0 4px;border-width:1px 0 1px 1px;order:-1}.file-label{align-items:stretch;display:flex;cursor:pointer;justify-content:flex-start;overflow:hidden;position:relative}.file-label:hover .file-cta{background-color:#eee;color:#363636}.file-label:hover .file-name{border-color:#d5d5d5}.file-label:active .file-cta{background-color:#e8e8e8;color:#363636}.file-label:active .file-name{border-color:#cfcfcf}.file-input{height:.01em;left:0;outline:0;position:absolute;top:0;width:.01em}.file-cta,.file-name{border-color:#dbdbdb;border-radius:4px;font-size:1em;padding-left:1em;padding-right:1em;white-space:nowrap}.file-cta{background-color:#f5f5f5;color:#4a4a4a}.file-name{border-color:#dbdbdb;border-style:solid;border-width:1px 1px 1px 0;display:block;max-width:16em;overflow:hidden;text-align:left;text-overflow:ellipsis}.file-icon{align-items:center;display:flex;height:1em;justify-content:center;margin-right:.5em;width:1em}.file-icon .fa{font-size:14px}.label{color:#363636;display:block;font-size:1rem;font-weight:700}.label:not(:last-child){margin-bottom:.5em}.label.is-small{font-size:.75rem}.label.is-medium{font-size:1.25rem}.label.is-large{font-size:1.5rem}.help{display:block;font-size:.75rem;margin-top:.25rem}.help.is-white{color:#fff}.help.is-black{color:#0a0a0a}.help.is-light{color:#f5f5f5}.help.is-dark{color:#363636}.help.is-primary{color:#00d1b2}.help.is-link{color:#3273dc}.help.is-info{color:#209cee}.help.is-success{color:#23d160}.help.is-warning{color:#ffdd57}.help.is-danger{color:#ff3860}.field:not(:last-child){margin-bottom:.75rem}.field.has-addons{display:flex;justify-content:flex-start}.field.has-addons .control:not(:last-child){margin-right:-1px}.field.has-addons .control:not(:first-child):not(:last-child) .button,.field.has-addons .control:not(:first-child):not(:last-child) .input,.field.has-addons .control:not(:first-child):not(:last-child) .select select{border-radius:0}.field.has-addons .control:first-child .button,.field.has-addons .control:first-child .input,.field.has-addons .control:first-child .select select{border-bottom-right-radius:0;border-top-right-radius:0}.field.has-addons .control:last-child .button,.field.has-addons .control:last-child .input,.field.has-addons .control:last-child .select select{border-bottom-left-radius:0;border-top-left-radius:0}.field.has-addons .control .button.is-hovered,.field.has-addons .control .button:hover,.field.has-addons .control .input.is-hovered,.field.has-addons .control .input:hover,.field.has-addons .control .select select.is-hovered,.field.has-addons .control .select select:hover{z-index:2}.field.has-addons .control .button.is-active,.field.has-addons .control .button.is-focused,.field.has-addons .control .button:active,.field.has-addons .control .button:focus,.field.has-addons .control .input.is-active,.field.has-addons .control .input.is-focused,.field.has-addons .control .input:active,.field.has-addons .control .input:focus,.field.has-addons .control .select select.is-active,.field.has-addons .control .select select.is-focused,.field.has-addons .control .select select:active,.field.has-addons .control .select select:focus{z-index:3}.field.has-addons .control .button.is-active:hover,.field.has-addons .control .button.is-focused:hover,.field.has-addons .control .button:active:hover,.field.has-addons .control .button:focus:hover,.field.has-addons .control .input.is-active:hover,.field.has-addons .control .input.is-focused:hover,.field.has-addons .control .input:active:hover,.field.has-addons .control .input:focus:hover,.field.has-addons .control .select select.is-active:hover,.field.has-addons .control .select select.is-focused:hover,.field.has-addons .control .select select:active:hover,.field.has-addons .control .select select:focus:hover{z-index:4}.field.has-addons .control.is-expanded{flex-grow:1}.field.has-addons.has-addons-centered{justify-content:center}.field.has-addons.has-addons-right{justify-content:flex-end}.field.has-addons.has-addons-fullwidth .control{flex-grow:1;flex-shrink:0}.field.is-grouped{display:flex;justify-content:flex-start}.field.is-grouped>.control{flex-shrink:0}.field.is-grouped>.control:not(:last-child){margin-bottom:0;margin-right:.75rem}.field.is-grouped>.control.is-expanded{flex-grow:1;flex-shrink:1}.field.is-grouped.is-grouped-centered{justify-content:center}.field.is-grouped.is-grouped-right{justify-content:flex-end}.field.is-grouped.is-grouped-multiline{flex-wrap:wrap}.field.is-grouped.is-grouped-multiline>.control:last-child,.field.is-grouped.is-grouped-multiline>.control:not(:last-child){margin-bottom:.75rem}.field.is-grouped.is-grouped-multiline:last-child{margin-bottom:-.75rem}.field.is-grouped.is-grouped-multiline:not(:last-child){margin-bottom:0}@media screen and (min-width:769px),print{.field.is-horizontal{display:flex}}.field-label .label{font-size:inherit}@media screen and (max-width:768px){.field-label{margin-bottom:.5rem}}@media screen and (min-width:769px),print{.field-label{flex-basis:0;flex-grow:1;flex-shrink:0;margin-right:1.5rem;text-align:right}.field-label.is-small{font-size:.75rem;padding-top:.375em}.field-label.is-normal{padding-top:.375em}.field-label.is-medium{font-size:1.25rem;padding-top:.375em}.field-label.is-large{font-size:1.5rem;padding-top:.375em}}.field-body .field .field{margin-bottom:0}@media screen and (min-width:769px),print{.field-body{display:flex;flex-basis:0;flex-grow:5;flex-shrink:1}.field-body .field{margin-bottom:0}.field-body>.field{flex-shrink:1}.field-body>.field:not(.is-narrow){flex-grow:1}.field-body>.field:not(:last-child){margin-right:.75rem}}.control{font-size:1rem;position:relative;text-align:left}.control.has-icon .icon{color:#dbdbdb;height:2.25em;pointer-events:none;position:absolute;top:0;width:2.25em;z-index:4}.control.has-icon .input:focus+.icon{color:#7a7a7a}.control.has-icon .input.is-small+.icon{font-size:.75rem}.control.has-icon .input.is-medium+.icon{font-size:1.25rem}.control.has-icon .input.is-large+.icon{font-size:1.5rem}.control.has-icon:not(.has-icon-right) .icon{left:0}.control.has-icon:not(.has-icon-right) .input{padding-left:2.25em}.control.has-icon.has-icon-right .icon{right:0}.control.has-icon.has-icon-right .input{padding-right:2.25em}.control.has-icons-left .input:focus~.icon,.control.has-icons-left .select:focus~.icon,.control.has-icons-right .input:focus~.icon,.control.has-icons-right .select:focus~.icon{color:#7a7a7a}.control.has-icons-left .input.is-small~.icon,.control.has-icons-left .select.is-small~.icon,.control.has-icons-right .input.is-small~.icon,.control.has-icons-right .select.is-small~.icon{font-size:.75rem}.control.has-icons-left .input.is-medium~.icon,.control.has-icons-left .select.is-medium~.icon,.control.has-icons-right .input.is-medium~.icon,.control.has-icons-right .select.is-medium~.icon{font-size:1.25rem}.control.has-icons-left .input.is-large~.icon,.control.has-icons-left .select.is-large~.icon,.control.has-icons-right .input.is-large~.icon,.control.has-icons-right .select.is-large~.icon{font-size:1.5rem}.control.has-icons-left .icon,.control.has-icons-right .icon{color:#dbdbdb;height:2.25em;pointer-events:none;position:absolute;top:0;width:2.25em;z-index:4}.control.has-icons-left .input,.control.has-icons-left .select select{padding-left:2.25em}.control.has-icons-left .icon.is-left{left:0}.control.has-icons-right .input,.control.has-icons-right .select select{padding-right:2.25em}.control.has-icons-right .icon.is-right{right:0}.control.is-loading::after{position:absolute!important;right:.625em;top:.625em;z-index:4}.control.is-loading.is-small:after{font-size:.75rem}.control.is-loading.is-medium:after{font-size:1.25rem}.control.is-loading.is-large:after{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.image{display:block;position:relative}.image img{display:block;height:auto;width:100%}.image img.is-rounded{border-radius:290486px}.image.is-16by9 img,.image.is-1by1 img,.image.is-1by2 img,.image.is-1by3 img,.image.is-2by1 img,.image.is-2by3 img,.image.is-3by1 img,.image.is-3by2 img,.image.is-3by4 img,.image.is-3by5 img,.image.is-4by3 img,.image.is-4by5 img,.image.is-5by3 img,.image.is-5by4 img,.image.is-9by16 img,.image.is-square img{height:100%;width:100%}.image.is-1by1,.image.is-square{padding-top:100%}.image.is-5by4{padding-top:80%}.image.is-4by3{padding-top:75%}.image.is-3by2{padding-top:66.6666%}.image.is-5by3{padding-top:60%}.image.is-16by9{padding-top:56.25%}.image.is-2by1{padding-top:50%}.image.is-3by1{padding-top:33.3333%}.image.is-4by5{padding-top:125%}.image.is-3by4{padding-top:133.3333%}.image.is-2by3{padding-top:150%}.image.is-3by5{padding-top:166.6666%}.image.is-9by16{padding-top:177.7777%}.image.is-1by2{padding-top:200%}.image.is-1by3{padding-top:300%}.image.is-16x16{height:16px;width:16px}.image.is-24x24{height:24px;width:24px}.image.is-32x32{height:32px;width:32px}.image.is-48x48{height:48px;width:48px}.image.is-64x64{height:64px;width:64px}.image.is-96x96{height:96px;width:96px}.image.is-128x128{height:128px;width:128px}.notification{background-color:#f5f5f5;border-radius:4px;padding:1.25rem 2.5rem 1.25rem 1.5rem;position:relative}.notification a:not(.button){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code,.notification pre{background:#fff}.notification pre code{background:0 0}.notification>.delete{position:absolute;right:.5rem;top:.5rem}.notification .content,.notification .subtitle,.notification .title{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light{background-color:#f5f5f5;color:#363636}.notification.is-dark{background-color:#363636;color:#f5f5f5}.notification.is-primary{background-color:#00d1b2;color:#fff}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-info{background-color:#209cee;color:#fff}.notification.is-success{background-color:#23d160;color:#fff}.notification.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.notification.is-danger{background-color:#ff3860;color:#fff}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#dbdbdb}.progress::-webkit-progress-value{background-color:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-light::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill{background-color:#f5f5f5}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-primary::-webkit-progress-value{background-color:#00d1b2}.progress.is-primary::-moz-progress-bar{background-color:#00d1b2}.progress.is-primary::-ms-fill{background-color:#00d1b2}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-info::-webkit-progress-value{background-color:#209cee}.progress.is-info::-moz-progress-bar{background-color:#209cee}.progress.is-info::-ms-fill{background-color:#209cee}.progress.is-success::-webkit-progress-value{background-color:#23d160}.progress.is-success::-moz-progress-bar{background-color:#23d160}.progress.is-success::-ms-fill{background-color:#23d160}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-danger::-webkit-progress-value{background-color:#ff3860}.progress.is-danger::-moz-progress-bar{background-color:#ff3860}.progress.is-danger::-ms-fill{background-color:#ff3860}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#f5f5f5}.table td.is-primary,.table th.is-primary{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#209cee;border-color:#209cee;color:#fff}.table td.is-success,.table th.is-success{background-color:#23d160;border-color:#23d160;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.table td.is-danger,.table th.is-danger{background-color:#ff3860;border-color:#ff3860;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#00d1b2;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table th{color:#363636;text-align:left}.table tr.is-selected{background-color:#00d1b2;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.tags.has-addons .tag:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#4a4a4a;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light{background-color:#f5f5f5;color:#363636}.tag:not(body).is-dark{background-color:#363636;color:#f5f5f5}.tag:not(body).is-primary{background-color:#00d1b2;color:#fff}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-info{background-color:#209cee;color:#fff}.tag:not(body).is-success{background-color:#23d160;color:#fff}.tag:not(body).is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.tag:not(body).is-danger{background-color:#ff3860;color:#fff}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-.375em;margin-right:-.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::after,.tag:not(body).is-delete::before{background-color:currentColor;content:"";display:block;left:50%;position:absolute;top:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);-webkit-transform-origin:center center;transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:focus,.tag:not(body).is-delete:hover{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.subtitle,.title{word-break:break-word}.subtitle em,.subtitle span,.title em,.title span{font-weight:inherit}.subtitle sub,.title sub{font-size:.75em}.subtitle sup,.title sup{font-size:.75em}.subtitle .tag,.title .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:"\0002f"}.breadcrumb ol,.breadcrumb ul{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:"\02192"}.breadcrumb.has-bullet-separator li+li::before{content:"\02022"}.breadcrumb.has-dot-separator li+li::before{content:"\000b7"}.breadcrumb.has-succeeds-separator li+li::before{content:"\0227B"}.card{background-color:#fff;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;max-width:100%;position:relative}.card-header{background-color:none;align-items:stretch;box-shadow:0 1px 2px rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem}.card-image{display:block;position:relative}.card-content{background-color:none;padding:1.5rem}.card-footer{background-color:none;border-top:1px solid #dbdbdb;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #dbdbdb}.card .media:not(:last-child){margin-bottom:.75rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item{padding-right:3rem;white-space:nowrap}a.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#dbdbdb;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item{margin-right:.75rem}.level.is-mobile .level-item:not(:last-child){margin-bottom:0}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width:769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .subtitle,.level-item .title{margin-bottom:0}@media screen and (max-width:768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width:769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width:768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width:769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width:769px),print{.level-right{display:flex}}.media{align-items:flex-start;display:flex;text-align:left}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:left}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#4a4a4a;display:block;padding:.5em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:1em}.menu-label:not(:last-child){margin-bottom:1em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff;color:#4d4d4d}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a;color:#090909}.message.is-light{background-color:#fafafa}.message.is-light .message-header{background-color:#f5f5f5;color:#363636}.message.is-light .message-body{border-color:#f5f5f5;color:#505050}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#f5f5f5}.message.is-dark .message-body{border-color:#363636;color:#2a2a2a}.message.is-primary{background-color:#f5fffd}.message.is-primary .message-header{background-color:#00d1b2;color:#fff}.message.is-primary .message-body{border-color:#00d1b2;color:#021310}.message.is-link{background-color:#f6f9fe}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#22509a}.message.is-info{background-color:#f6fbfe}.message.is-info .message-header{background-color:#209cee;color:#fff}.message.is-info .message-body{border-color:#209cee;color:#12537e}.message.is-success{background-color:#f6fef9}.message.is-success .message-header{background-color:#23d160;color:#fff}.message.is-success .message-body{border-color:#23d160;color:#0e301a}.message.is-warning{background-color:#fffdf5}.message.is-warning .message-header{background-color:#ffdd57;color:rgba(0,0,0,.7)}.message.is-warning .message-body{border-color:#ffdd57;color:#3b3108}.message.is-danger{background-color:#fff5f7}.message.is-danger .message-header{background-color:#ff3860;color:#fff}.message.is-danger .message-body{border-color:#ff3860;color:#cd0930}.message-header{align-items:center;background-color:#4a4a4a;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#4a4a4a;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-card,.modal-content{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width:769px),print{.modal-card,.modal-content{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:0 0;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden}.modal-card-foot,.modal-card-head{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:10px}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link,.navbar.is-white .navbar-brand>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link.is-active,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}@media screen and (min-width:1088px){.navbar.is-white .navbar-end .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-start>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link.is-active,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link::after,.navbar.is-white .navbar-start .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand .navbar-link,.navbar.is-black .navbar-brand>.navbar-item{color:#fff}.navbar.is-black .navbar-brand .navbar-link.is-active,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-black .navbar-end .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-start>.navbar-item{color:#fff}.navbar.is-black .navbar-end .navbar-link.is-active,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-end .navbar-link::after,.navbar.is-black .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:#363636}.navbar.is-light .navbar-brand .navbar-link,.navbar.is-light .navbar-brand>.navbar-item{color:#363636}.navbar.is-light .navbar-brand .navbar-link.is-active,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-brand .navbar-link::after{border-color:#363636}@media screen and (min-width:1088px){.navbar.is-light .navbar-end .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-start>.navbar-item{color:#363636}.navbar.is-light .navbar-end .navbar-link.is-active,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-end .navbar-link::after,.navbar.is-light .navbar-start .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#363636}}.navbar.is-dark{background-color:#363636;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link,.navbar.is-dark .navbar-brand>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link.is-active,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#f5f5f5}@media screen and (min-width:1088px){.navbar.is-dark .navbar-end .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-start>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link.is-active,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link::after,.navbar.is-dark .navbar-start .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#f5f5f5}}.navbar.is-primary{background-color:#00d1b2;color:#fff}.navbar.is-primary .navbar-brand .navbar-link,.navbar.is-primary .navbar-brand>.navbar-item{color:#fff}.navbar.is-primary .navbar-brand .navbar-link.is-active,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-primary .navbar-end .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-start>.navbar-item{color:#fff}.navbar.is-primary .navbar-end .navbar-link.is-active,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-end .navbar-link::after,.navbar.is-primary .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#00d1b2;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand .navbar-link,.navbar.is-link .navbar-brand>.navbar-item{color:#fff}.navbar.is-link .navbar-brand .navbar-link.is-active,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-link .navbar-end .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-start>.navbar-item{color:#fff}.navbar.is-link .navbar-end .navbar-link.is-active,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-end .navbar-link::after,.navbar.is-link .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#209cee;color:#fff}.navbar.is-info .navbar-brand .navbar-link,.navbar.is-info .navbar-brand>.navbar-item{color:#fff}.navbar.is-info .navbar-brand .navbar-link.is-active,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-info .navbar-end .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-start>.navbar-item{color:#fff}.navbar.is-info .navbar-end .navbar-link.is-active,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-end .navbar-link::after,.navbar.is-info .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#209cee;color:#fff}}.navbar.is-success{background-color:#23d160;color:#fff}.navbar.is-success .navbar-brand .navbar-link,.navbar.is-success .navbar-brand>.navbar-item{color:#fff}.navbar.is-success .navbar-brand .navbar-link.is-active,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-success .navbar-end .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-start>.navbar-item{color:#fff}.navbar.is-success .navbar-end .navbar-link.is-active,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-end .navbar-link::after,.navbar.is-success .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#23d160;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link,.navbar.is-warning .navbar-brand>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link.is-active,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}@media screen and (min-width:1088px){.navbar.is-warning .navbar-end .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-start>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link.is-active,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link::after,.navbar.is-warning .navbar-start .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#ff3860;color:#fff}.navbar.is-danger .navbar-brand .navbar-link,.navbar.is-danger .navbar-brand>.navbar-item{color:#fff}.navbar.is-danger .navbar-brand .navbar-link.is-active,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}@media screen and (min-width:1088px){.navbar.is-danger .navbar-end .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-start>.navbar-item{color:#fff}.navbar.is-danger .navbar-end .navbar-link.is-active,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-end .navbar-link::after,.navbar.is-danger .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#ff3860;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px 0 0 #f5f5f5}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px 0 0 #f5f5f5}.navbar.is-fixed-top{top:0}body.has-navbar-fixed-top,html.has-navbar-fixed-top{padding-top:3.25rem}body.has-navbar-fixed-bottom,html.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;-webkit-transform-origin:center;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,-webkit-transform;transition-property:background-color,opacity,transform;transition-property:background-color,opacity,transform,-webkit-transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,.05)}.navbar-burger.is-active span:nth-child(1){-webkit-transform:translateY(5px) rotate(45deg);transform:translateY(5px) rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){-webkit-transform:translateY(-5px) rotate(-45deg);transform:translateY(-5px) rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}.navbar-link,a.navbar-item{cursor:pointer}.navbar-link.is-active,.navbar-link:hover,a.navbar-item.is-active,a.navbar-item:hover{background-color:#fafafa;color:#3273dc}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link{padding-right:2.5em}.navbar-link::after{border-color:#3273dc;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:1087px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}body.has-navbar-fixed-top-touch,html.has-navbar-fixed-top-touch{padding-top:3.25rem}body.has-navbar-fixed-bottom-touch,html.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:1088px){.navbar,.navbar-end,.navbar-menu,.navbar-start{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-end,.navbar.is-spaced .navbar-start{align-items:center}.navbar.is-spaced .navbar-link,.navbar.is-spaced a.navbar-item{border-radius:4px}.navbar.is-transparent .navbar-link.is-active,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent a.navbar-item:hover{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{-webkit-transform:rotate(135deg) translate(.25em,-.25em);transform:rotate(135deg) translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown{opacity:1;pointer-events:auto;-webkit-transform:translateY(0);transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-dropdown{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));-webkit-transform:translateY(-5px);transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.container>.navbar .navbar-brand,.navbar>.container .navbar-brand{margin-left:-1rem}.container>.navbar .navbar-menu,.navbar>.container .navbar-menu{margin-right:-1rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}body.has-navbar-fixed-top-desktop,html.has-navbar-fixed-top-desktop{padding-top:3.25rem}body.has-navbar-fixed-bottom-desktop,html.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}body.has-spaced-navbar-fixed-top,html.has-spaced-navbar-fixed-top{padding-top:5.25rem}body.has-spaced-navbar-fixed-bottom,html.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}.navbar-link.is-active,a.navbar-item.is-active{color:#0a0a0a}.navbar-link.is-active:not(:hover),a.navbar-item.is-active:not(:hover){background-color:transparent}.navbar-item.has-dropdown.is-active .navbar-link,.navbar-item.has-dropdown:hover .navbar-link{background-color:#fafafa}}.pagination{font-size:1rem;margin:-.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-next,.pagination.is-rounded .pagination-previous{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous{font-size:1em;padding-left:.5em;padding-right:.5em;justify-content:center;margin:.25rem;text-align:center}.pagination-link,.pagination-next,.pagination-previous{border-color:#dbdbdb;color:#363636;min-width:2.25em}.pagination-link:hover,.pagination-next:hover,.pagination-previous:hover{border-color:#b5b5b5;color:#363636}.pagination-link:focus,.pagination-next:focus,.pagination-previous:focus{border-color:#3273dc}.pagination-link:active,.pagination-next:active,.pagination-previous:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-link[disabled],.pagination-next[disabled],.pagination-previous[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-next,.pagination-previous{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width:768px){.pagination{flex-wrap:wrap}.pagination-next,.pagination-previous{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width:769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel-block,.panel-heading,.panel-tabs{border-bottom:1px solid #dbdbdb;border-left:1px solid #dbdbdb;border-right:1px solid #dbdbdb}.panel-block:first-child,.panel-heading:first-child,.panel-tabs:first-child{border-top:1px solid #dbdbdb}.panel-heading{background-color:#f5f5f5;border-radius:4px 4px 0 0;color:#363636;font-size:1.25em;font-weight:300;line-height:1.25;padding:.5em .75em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#4a4a4a}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#4a4a4a;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent!important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-radius:4px 0 0 4px}.tabs.is-toggle li:last-child a{border-radius:0 4px 4px 0}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-1{flex:none;width:8.33333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-1-mobile{flex:none;width:8.33333%}.column.is-offset-1-mobile{margin-left:8.33333%}.column.is-2-mobile{flex:none;width:16.66667%}.column.is-offset-2-mobile{margin-left:16.66667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333%}.column.is-offset-4-mobile{margin-left:33.33333%}.column.is-5-mobile{flex:none;width:41.66667%}.column.is-offset-5-mobile{margin-left:41.66667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333%}.column.is-offset-7-mobile{margin-left:58.33333%}.column.is-8-mobile{flex:none;width:66.66667%}.column.is-offset-8-mobile{margin-left:66.66667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333%}.column.is-offset-10-mobile{margin-left:83.33333%}.column.is-11-mobile{flex:none;width:91.66667%}.column.is-offset-11-mobile{margin-left:91.66667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:1087px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-1-touch{flex:none;width:8.33333%}.column.is-offset-1-touch{margin-left:8.33333%}.column.is-2-touch{flex:none;width:16.66667%}.column.is-offset-2-touch{margin-left:16.66667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333%}.column.is-offset-4-touch{margin-left:33.33333%}.column.is-5-touch{flex:none;width:41.66667%}.column.is-offset-5-touch{margin-left:41.66667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333%}.column.is-offset-7-touch{margin-left:58.33333%}.column.is-8-touch{flex:none;width:66.66667%}.column.is-offset-8-touch{margin-left:66.66667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333%}.column.is-offset-10-touch{margin-left:83.33333%}.column.is-11-touch{flex:none;width:91.66667%}.column.is-offset-11-touch{margin-left:91.66667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:1088px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-1-desktop{flex:none;width:8.33333%}.column.is-offset-1-desktop{margin-left:8.33333%}.column.is-2-desktop{flex:none;width:16.66667%}.column.is-offset-2-desktop{margin-left:16.66667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333%}.column.is-offset-4-desktop{margin-left:33.33333%}.column.is-5-desktop{flex:none;width:41.66667%}.column.is-offset-5-desktop{margin-left:41.66667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333%}.column.is-offset-7-desktop{margin-left:58.33333%}.column.is-8-desktop{flex:none;width:66.66667%}.column.is-offset-8-desktop{margin-left:66.66667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333%}.column.is-offset-10-desktop{margin-left:83.33333%}.column.is-11-desktop{flex:none;width:91.66667%}.column.is-offset-11-desktop{margin-left:91.66667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1280px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-1-widescreen{flex:none;width:8.33333%}.column.is-offset-1-widescreen{margin-left:8.33333%}.column.is-2-widescreen{flex:none;width:16.66667%}.column.is-offset-2-widescreen{margin-left:16.66667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333%}.column.is-offset-4-widescreen{margin-left:33.33333%}.column.is-5-widescreen{flex:none;width:41.66667%}.column.is-offset-5-widescreen{margin-left:41.66667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333%}.column.is-offset-7-widescreen{margin-left:58.33333%}.column.is-8-widescreen{flex:none;width:66.66667%}.column.is-offset-8-widescreen{margin-left:66.66667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333%}.column.is-offset-10-widescreen{margin-left:83.33333%}.column.is-11-widescreen{flex:none;width:91.66667%}.column.is-offset-11-widescreen{margin-left:91.66667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1472px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-1-fullhd{flex:none;width:8.33333%}.column.is-offset-1-fullhd{margin-left:8.33333%}.column.is-2-fullhd{flex:none;width:16.66667%}.column.is-offset-2-fullhd{margin-left:16.66667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333%}.column.is-offset-4-fullhd{margin-left:33.33333%}.column.is-5-fullhd{flex:none;width:41.66667%}.column.is-offset-5-fullhd{margin-left:41.66667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333%}.column.is-offset-7-fullhd{margin-left:58.33333%}.column.is-8-fullhd{flex:none;width:66.66667%}.column.is-offset-8-fullhd{margin-left:66.66667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333%}.column.is-offset-10-fullhd{margin-left:83.33333%}.column.is-11-fullhd{flex:none;width:91.66667%}.column.is-offset-11-fullhd{margin-left:91.66667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:1088px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}.columns.is-variable.is-1{--columnGap:0.25rem}.columns.is-variable.is-2{--columnGap:0.5rem}.columns.is-variable.is-3{--columnGap:0.75rem}.columns.is-variable.is-4{--columnGap:1rem}.columns.is-variable.is-5{--columnGap:1.25rem}.columns.is-variable.is-6{--columnGap:1.5rem}.columns.is-variable.is-7{--columnGap:1.75rem}.columns.is-variable.is-8{--columnGap:2rem}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:-webkit-min-content;min-height:-moz-min-content;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333%}.tile.is-2{flex:none;width:16.66667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333%}.tile.is-5{flex:none;width:41.66667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333%}.tile.is-8{flex:none;width:66.66667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333%}.tile.is-11{flex:none;width:91.66667%}.tile.is-12{flex:none;width:100%}}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:0 0}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:1087px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white .navbar-link.is-active,.hero.is-white .navbar-link:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black .navbar-link.is-active,.hero.is-black .navbar-link:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black a.navbar-item:hover{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:#363636}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag),.hero.is-light strong{color:inherit}.hero.is-light .title{color:#363636}.hero.is-light .subtitle{color:rgba(54,54,54,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:#363636}@media screen and (max-width:1087px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(54,54,54,.7)}.hero.is-light .navbar-link.is-active,.hero.is-light .navbar-link:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.hero.is-light .tabs a{color:#363636;opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:#363636}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}}.hero.is-dark{background-color:#363636;color:#f5f5f5}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#f5f5f5}.hero.is-dark .subtitle{color:rgba(245,245,245,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#f5f5f5}@media screen and (max-width:1087px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(245,245,245,.7)}.hero.is-dark .navbar-link.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.hero.is-dark .tabs a{color:#f5f5f5;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#f5f5f5}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}}.hero.is-primary{background-color:#00d1b2;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-primary .navbar-menu{background-color:#00d1b2}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary .navbar-link.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary a.navbar-item:hover{background-color:#00b89c;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#00d1b2}.hero.is-primary.is-bold{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}}.hero.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link .navbar-link.is-active,.hero.is-link .navbar-link:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link a.navbar-item:hover{background-color:#2366d1;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}}.hero.is-info{background-color:#209cee;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-info .navbar-menu{background-color:#209cee}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info .navbar-link.is-active,.hero.is-info .navbar-link:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info a.navbar-item:hover{background-color:#118fe4;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#209cee}.hero.is-info.is-bold{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}}.hero.is-success{background-color:#23d160;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-success .navbar-menu{background-color:#23d160}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success .navbar-link.is-active,.hero.is-success .navbar-link:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success a.navbar-item:hover{background-color:#20bc56;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#23d160}.hero.is-success.is-bold{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}}.hero.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1087px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning .navbar-link.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger{background-color:#ff3860;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width:1087px){.hero.is-danger .navbar-menu{background-color:#ff3860}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger .navbar-link.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#ff3860}.hero.is-danger.is-bold{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}}.hero.is-small .hero-body{padding-bottom:1.5rem;padding-top:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body{padding-bottom:9rem;padding-top:9rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body{padding-bottom:18rem;padding-top:18rem}}.hero.is-fullheight .hero-body,.hero.is-halfheight .hero-body{align-items:center;display:flex}.hero.is-fullheight .hero-body>.container,.hero.is-halfheight .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-foot,.hero-head{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:1088px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/1.png b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/1.png deleted file mode 100644 index 4bb428a..0000000 Binary files a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/1.png and /dev/null differ diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/2.png b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/2.png deleted file mode 100644 index a01a53a..0000000 Binary files a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/2.png and /dev/null differ diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html deleted file mode 100644 index 9b883d4..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - -
-
-

Spotify Authentication

-
-
-

Introduction

- -
-
- -
- -
-
-
- -
- -
-
- -
-
- -
-
-
-
-
- - - - - \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/ImplicitGrantAuth/index.html b/Source Files/SpotifyAPI.Web.Auth/Resources/ImplicitGrantAuth/index.html deleted file mode 100644 index 6197f88..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/Resources/ImplicitGrantAuth/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/logo.svg b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/logo.svg new file mode 100644 index 0000000..611a8f9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/logo.svg @@ -0,0 +1,9 @@ + + +# \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.css b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.css new file mode 100644 index 0000000..56fcf5d --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.css @@ -0,0 +1,22 @@ +html, +body { + width : 100%; + height: 100%; +} + +body { + color : #f5f6fa; + background-color : #353b48; + width : 100%; + height : 100%; + background-attachment: fixed; +} + +main { + text-align: center; + margin-top: 100px; +} + +.logo { + margin-bottom: 50px; +} diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.js b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.js new file mode 100644 index 0000000..e03f01e --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/auth_assets/main.js @@ -0,0 +1,57 @@ +function getUrlParams(hash, start) { + const hashes = hash.slice(hash.indexOf(start) + 1).split('&') + + if (!hashes || hashes.length === 0 || hashes[0] === "") { + return undefined; + } + + const params = {} + hashes.map(hash => { + const [key, val] = hash.split('=') + params[key] = decodeURIComponent(val) + }) + return params +} + +function handleImplicitGrant() { + const params = getUrlParams(window.location.hash, '#'); + if (!params) { + return; + } + params.request_type = "token"; + + console.log("Sent request_type token to server", params); + fetch('?' + new URLSearchParams(params).toString(), { + method: 'POST', + }); +} +handleImplicitGrant(); + +function handleAuthenticationCode() { + const params = getUrlParams(window.location.search, '?'); + if (!params) { + return; + } + params.request_type = "code"; + + console.log("Sent request_type code to server", params); + fetch('?' + new URLSearchParams(params).toString(), { + method: 'POST', + }); +} +handleAuthenticationCode(); + +function handlePkceAuth() { + const params = getUrlParams(window.location.search, '?'); + if (!params) { + return; + } + params.request_type = "pkce"; + + console.log("Sent request_type code to server", params); + fetch('?' + new URLSearchParams(params).toString(), { + method: 'POST', + }); +} +handlePkceAuth(); + diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/favicon.ico b/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/favicon.ico new file mode 100644 index 0000000..5ecba01 Binary files /dev/null and b/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/favicon.ico differ diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/index.html b/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/index.html new file mode 100644 index 0000000..6c8c710 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/default_site/index.html @@ -0,0 +1,38 @@ + + + + + + + Spotify Authorization + + + + + + + +
+ +

Success!

+

+ Spotify Authorization was successful. You can close this tab and go back to your app. +

+
+ +
+
+ + + diff --git a/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj index 6f40b62..8f47dbe 100644 --- a/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj +++ b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj @@ -1,25 +1,47 @@  - - net46;netstandard2.0 - - - - bin\Debug\netstandard2.0\SpotifyAPI.Web.Auth.xml - 1701;1702;1705;1591 + net5.0;netstandard2.1;netstandard2.0 + 9.0 + enable + SpotifyAPI.Web.Auth + SpotifyAPI.Web.Auth + Jonas Dellinger + MIT + https://github.com/JohnnyCrazy/SpotifyAPI-NET/ + False + + An embedded Web Server, based on EmbeddedIO, for Spotify Web API Authorization flows + + For more infos, visit https://github.com/JohnnyCrazy/SpotifyAPI-NET + + + spotify api music .net c# spotify-client + + true + true + snupkg + true + true + 1591 - - bin\Release\netstandard2.0\SpotifyAPI.Web.Auth.xml - 1701;1702;1705;1591 + + true - + + + None + + + False + None + - + @@ -30,22 +52,4 @@ - - - - - - - - ..\..\..\..\..\..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll - - - ..\..\..\..\..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Windows.Forms.dll - - - - - - - diff --git a/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.nuspec b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.nuspec deleted file mode 100644 index 6675c4d..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.nuspec +++ /dev/null @@ -1,31 +0,0 @@ - - - - - SpotifyAPI.Web.Auth - 3.0.0 - SpotifyAPI.Web.Auth - JohnnyCrazy - JohnnyCrazy - https://github.com/JohnnyCrazy/SpotifyAPI-NET/blob/master/LICENSE - https://github.com/JohnnyCrazy/SpotifyAPI-NET/ - false - - Authorization Flows for the Spotify's Web API, written in .NET - - For more infos, visit https://github.com/JohnnyCrazy/SpotifyAPI-NET - - - spotify api music .net c# spotify-client - - - - - - - - - - - - diff --git a/Source Files/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs b/Source Files/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs deleted file mode 100644 index e4f668b..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs +++ /dev/null @@ -1,101 +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=false"); - return Uri.EscapeUriString(builder.ToString()); - } - - public void Stop(int delay = 2000) - { - if (_serverSource == null) return; - _serverSource.CancelAfter(delay); - Instances.Remove(State); - } - - public void OpenBrowser(bool AR) - { - string uri = GetUri(); - AuthUtil.OpenBrowser(uri, AR); - } - - // Attempt to silently automate renewal of tokens. Broken due to Javascript redirect, most likely. - /* - public void TryCURL() - { - string uri = GetUri(); - AuthUtil.TryCURL(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/Source Files/SpotifyAPI.Web.Auth/TokenSwapAuth.cs b/Source Files/SpotifyAPI.Web.Auth/TokenSwapAuth.cs deleted file mode 100644 index 73e7b0d..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/TokenSwapAuth.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using SpotifyAPI.Web.Enums; -using Unosquare.Labs.EmbedIO; -using Unosquare.Labs.EmbedIO.Constants; -using Unosquare.Labs.EmbedIO.Modules; -using SpotifyAPI.Web.Models; -using Newtonsoft.Json; -#if NETSTANDARD2_0 -using System.Net.Http; -#endif -#if NET46 -using System.Net.Http; -using HttpListenerContext = Unosquare.Net.HttpListenerContext; -#endif - -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 - { - 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; } - - /// 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; - } - - this.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()); - } - - static readonly HttpClient httpClient = new HttpClient(); - - /// - /// 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) - { - var content = new FormUrlEncodedContent(new Dictionary - { - { "grant_type", grantType }, - { "code", authorizationCode }, - { "refresh_token", refreshToken } - }); - - try - { - var siteResponse = await httpClient.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 this.StringResponseAsync(((TokenSwapAuth)auth).HtmlResponse); - } - } -} diff --git a/Source Files/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs b/Source Files/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs deleted file mode 100644 index 1cc9128..0000000 --- a/Source Files/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs +++ /dev/null @@ -1,282 +0,0 @@ -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Models; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -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(false); - } - - 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; - }); - } - } -} diff --git a/Source Files/SpotifyAPI.Web/Assembly.cs b/Source Files/SpotifyAPI.Web/Assembly.cs new file mode 100644 index 0000000..661cd81 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Assembly.cs @@ -0,0 +1,5 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SpotifyAPI.Web.Tests")] +[assembly: CLSCompliant(true)] diff --git a/Source Files/SpotifyAPI.Web/Authenticators/AuthorizationCodeAuthenticator.cs b/Source Files/SpotifyAPI.Web/Authenticators/AuthorizationCodeAuthenticator.cs new file mode 100644 index 0000000..c116900 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Authenticators/AuthorizationCodeAuthenticator.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + /// + /// This Authenticator requests new credentials token on demand and stores them into memory. + /// It is unable to query user specifc details. + /// + public class AuthorizationCodeAuthenticator : IAuthenticator + { + /// + /// Initiate a new instance. The token will be refreshed once it expires. + /// The initialToken will be updated with the new values on refresh! + /// + public AuthorizationCodeAuthenticator(string clientId, string clientSecret, AuthorizationCodeTokenResponse initialToken) + { + Ensure.ArgumentNotNull(clientId, nameof(clientId)); + Ensure.ArgumentNotNull(clientSecret, nameof(clientSecret)); + Ensure.ArgumentNotNull(initialToken, nameof(initialToken)); + + InitialToken = initialToken; + ClientId = clientId; + ClientSecret = clientSecret; + } + + /// + /// This event is called once a new refreshed token was aquired + /// + public event EventHandler? TokenRefreshed; + + + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard + /// + public string ClientId { get; } + + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard + /// + public string ClientSecret { get; } + + /// + /// The inital token passed to the authenticator. Fields will be updated on refresh. + /// + /// + public AuthorizationCodeTokenResponse InitialToken { get; } + + public async Task Apply(IRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + if (InitialToken.IsExpired) + { + var tokenRequest = new AuthorizationCodeRefreshRequest(ClientId, ClientSecret, InitialToken.RefreshToken); + var refreshedToken = await OAuthClient.RequestToken(tokenRequest, apiConnector).ConfigureAwait(false); + + InitialToken.AccessToken = refreshedToken.AccessToken; + InitialToken.CreatedAt = refreshedToken.CreatedAt; + InitialToken.ExpiresIn = refreshedToken.ExpiresIn; + InitialToken.Scope = refreshedToken.Scope; + InitialToken.TokenType = refreshedToken.TokenType; + + TokenRefreshed?.Invoke(this, InitialToken); + } + + request.Headers["Authorization"] = $"{InitialToken.TokenType} {InitialToken.AccessToken}"; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Authenticators/ClientCredentialsAuthenticator.cs b/Source Files/SpotifyAPI.Web/Authenticators/ClientCredentialsAuthenticator.cs new file mode 100644 index 0000000..00e42ed --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Authenticators/ClientCredentialsAuthenticator.cs @@ -0,0 +1,70 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + /// + /// This Authenticator requests new credentials token on demand and stores them into memory. + /// It is unable to query user specifc details. + /// + public class ClientCredentialsAuthenticator : IAuthenticator + { + /// + /// Initiate a new instance. The initial token will be fetched when the first API call occurs. + /// + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard. + /// + /// + /// The ClientSecret, defined in a spotify application in your Spotify Developer Dashboard. + /// + public ClientCredentialsAuthenticator(string clientId, string clientSecret) : this(clientId, clientSecret, null) { } + + /// + /// Initiate a new instance. The initial token is provided and will be used if not expired + /// + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard. + /// + /// + /// The ClientSecret, defined in a spotify application in your Spotify Developer Dashboard. + /// + /// + /// An optional inital token received earlier. + /// + public ClientCredentialsAuthenticator(string clientId, string clientSecret, ClientCredentialsTokenResponse? token) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret)); + + ClientId = clientId; + ClientSecret = clientSecret; + Token = token; + } + + public ClientCredentialsTokenResponse? Token { get; private set; } + + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard + /// + public string ClientId { get; } + + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard + /// + public string ClientSecret { get; } + + public async Task Apply(IRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + if (Token == null || Token.IsExpired) + { + var tokenRequest = new ClientCredentialsRequest(ClientId, ClientSecret); + Token = await OAuthClient.RequestToken(tokenRequest, apiConnector).ConfigureAwait(false); + } + + request.Headers["Authorization"] = $"{Token.TokenType} {Token.AccessToken}"; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Authenticators/IAuthenticator.cs b/Source Files/SpotifyAPI.Web/Authenticators/IAuthenticator.cs new file mode 100644 index 0000000..ccb14b9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Authenticators/IAuthenticator.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public interface IAuthenticator + { + Task Apply(IRequest request, IAPIConnector apiConnector); + } +} diff --git a/Source Files/SpotifyAPI.Web/Authenticators/PKCEAuthenticator.cs b/Source Files/SpotifyAPI.Web/Authenticators/PKCEAuthenticator.cs new file mode 100644 index 0000000..a2f562e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Authenticators/PKCEAuthenticator.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Xml.Serialization; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + /// + /// This Authenticator requests new credentials token on demand and stores them into memory. + /// It is unable to query user specifc details. + /// + public class PKCEAuthenticator : IAuthenticator + { + /// + /// Initiate a new instance. The token will be refreshed once it expires. + /// The initialToken will be updated with the new values on refresh! + /// + public PKCEAuthenticator(string clientId, PKCETokenResponse initialToken, string path) + { + Ensure.ArgumentNotNull(clientId, nameof(clientId)); + Ensure.ArgumentNotNull(initialToken, nameof(initialToken)); + + InitialToken = initialToken; + ClientId = clientId; + Path = path; + } + + /// + /// This event is called once a new refreshed token was aquired + /// + public event EventHandler? TokenRefreshed; + + + /// + /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard + /// + public string ClientId { get; } + + public string Path { get; } + + /// + /// The inital token passed to the authenticator. Fields will be updated on refresh. + /// + /// + public PKCETokenResponse InitialToken { get; } + + public void SerializeConfig(PKCETokenResponse data) + { + + using (StreamWriter file = new StreamWriter(Path, false)) + { + XmlSerializer controlsDefaultsSerializer = new XmlSerializer(typeof(PKCETokenResponse)); + controlsDefaultsSerializer.Serialize(file, data); + file.Close(); + } + } + + public async Task Apply(IRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + if (InitialToken.IsExpired) + { + var tokenRequest = new PKCETokenRefreshRequest(ClientId, InitialToken.RefreshToken); + var refreshedToken = await OAuthClient.RequestToken(tokenRequest, apiConnector).ConfigureAwait(false); + + InitialToken.AccessToken = refreshedToken.AccessToken; + InitialToken.CreatedAt = refreshedToken.CreatedAt; + InitialToken.ExpiresIn = refreshedToken.ExpiresIn; + InitialToken.Scope = refreshedToken.Scope; + InitialToken.TokenType = refreshedToken.TokenType; + InitialToken.RefreshToken = refreshedToken.RefreshToken; + + SerializeConfig(InitialToken); + + TokenRefreshed?.Invoke(this, InitialToken); + } + + request.Headers["Authorization"] = $"{InitialToken.TokenType} {InitialToken.AccessToken}"; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Authenticators/TokenAuthenticator.cs b/Source Files/SpotifyAPI.Web/Authenticators/TokenAuthenticator.cs new file mode 100644 index 0000000..186a972 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Authenticators/TokenAuthenticator.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class TokenAuthenticator : IAuthenticator + { + public TokenAuthenticator(string token, string tokenType) + { + Token = token; + TokenType = tokenType; + } + + public string Token { get; set; } + + public string TokenType { get; set; } + + public Task Apply(IRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + request.Headers["Authorization"] = $"{TokenType} {Token}"; + return Task.CompletedTask; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/APIClient.cs b/Source Files/SpotifyAPI.Web/Clients/APIClient.cs new file mode 100644 index 0000000..c12c983 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/APIClient.cs @@ -0,0 +1,16 @@ +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public abstract class APIClient + { + protected APIClient(IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + API = apiConnector; + } + + protected IAPIConnector API { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/AlbumsClient.cs b/Source Files/SpotifyAPI.Web/Clients/AlbumsClient.cs new file mode 100644 index 0000000..f70c307 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/AlbumsClient.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class AlbumsClient : APIClient, IAlbumsClient + { + public AlbumsClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Get(string albumId) + { + Ensure.ArgumentNotNullOrEmptyString(albumId, nameof(albumId)); + + return API.Get(URLs.Album(albumId)); + } + + public Task Get(string albumId, AlbumRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(albumId, nameof(albumId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Album(albumId), request.BuildQueryParams()); + } + + public Task GetSeveral(AlbumsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Albums(), request.BuildQueryParams()); + } + + public Task> GetTracks(string albumId) + { + Ensure.ArgumentNotNullOrEmptyString(albumId, nameof(albumId)); + + return API.Get>(URLs.AlbumTracks(albumId)); + } + + public Task> GetTracks(string albumId, AlbumTracksRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(albumId, nameof(albumId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.AlbumTracks(albumId), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/ArtistsClient.cs b/Source Files/SpotifyAPI.Web/Clients/ArtistsClient.cs new file mode 100644 index 0000000..42bea3c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/ArtistsClient.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class ArtistsClient : APIClient, IArtistsClient + { + public ArtistsClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Get(string artistId) + { + Ensure.ArgumentNotNullOrEmptyString(artistId, nameof(artistId)); + + return API.Get(URLs.Artist(artistId)); + } + + public Task> GetAlbums(string artistId) + { + Ensure.ArgumentNotNullOrEmptyString(artistId, nameof(artistId)); + + return API.Get>(URLs.ArtistAlbums(artistId)); + } + + public Task> GetAlbums(string artistId, ArtistsAlbumsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(artistId, nameof(artistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.ArtistAlbums(artistId), request.BuildQueryParams()); + } + + public Task GetRelatedArtists(string artistId) + { + Ensure.ArgumentNotNullOrEmptyString(artistId, nameof(artistId)); + + return API.Get(URLs.ArtistRelatedArtists(artistId)); + } + + public Task GetSeveral(ArtistsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Artists(), request.BuildQueryParams()); + } + + public Task GetTopTracks(string artistId, ArtistsTopTracksRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(artistId, nameof(artistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.ArtistTopTracks(artistId), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/BrowseClient.cs b/Source Files/SpotifyAPI.Web/Clients/BrowseClient.cs new file mode 100644 index 0000000..0cf183f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/BrowseClient.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class BrowseClient : APIClient, IBrowseClient + { + public BrowseClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task GetCategories() + { + return API.Get(URLs.Categories()); + } + + public Task GetCategories(CategoriesRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Categories(), request.BuildQueryParams()); + } + + public Task GetCategory(string categoryId) + { + Ensure.ArgumentNotNullOrEmptyString(categoryId, nameof(categoryId)); + + return API.Get(URLs.Category(categoryId)); + } + + public Task GetCategory(string categoryId, CategoryRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(categoryId, nameof(categoryId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Category(categoryId), request.BuildQueryParams()); + } + + public Task GetCategoryPlaylists(string categoryId) + { + Ensure.ArgumentNotNullOrEmptyString(categoryId, nameof(categoryId)); + + return API.Get(URLs.CategoryPlaylists(categoryId)); + } + + public Task GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(categoryId, nameof(categoryId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + 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()); + } + + public Task GetRecommendationGenres() + { + return API.Get(URLs.RecommendationGenres()); + } + + public Task GetNewReleases() + { + return API.Get(URLs.NewReleases()); + } + + public Task GetNewReleases(NewReleasesRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.NewReleases(), request.BuildQueryParams()); + } + + public Task GetFeaturedPlaylists() + { + return API.Get(URLs.FeaturedPlaylists()); + } + + public Task GetFeaturedPlaylists(FeaturedPlaylistsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.FeaturedPlaylists(), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/EpisodesClient.cs b/Source Files/SpotifyAPI.Web/Clients/EpisodesClient.cs new file mode 100644 index 0000000..f9bd117 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/EpisodesClient.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class EpisodesClient : APIClient, IEpisodesClient + { + public EpisodesClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Get(string episodeId) + { + Ensure.ArgumentNotNullOrEmptyString(episodeId, nameof(episodeId)); + + return API.Get(URLs.Episode(episodeId)); + } + + public Task Get(string episodeId, EpisodeRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(episodeId, nameof(episodeId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Episode(episodeId), request.BuildQueryParams()); + } + + public Task GetSeveral(EpisodesRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Episodes(), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/FollowClient.cs b/Source Files/SpotifyAPI.Web/Clients/FollowClient.cs new file mode 100644 index 0000000..a1ad28c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/FollowClient.cs @@ -0,0 +1,93 @@ +using System.Net; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class FollowClient : APIClient, IFollowClient + { + public FollowClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task> CheckCurrentUser(FollowCheckCurrentUserRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.CurrentUserFollowerContains(), request.BuildQueryParams()); + } + + public Task> CheckPlaylist(string playlistId, FollowCheckPlaylistRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.PlaylistFollowersContains(playlistId), request.BuildQueryParams()); + } + + public async Task Follow(FollowRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API + .Put(URLs.CurrentUserFollower(), request.BuildQueryParams(), request.BuildBodyParams()) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task FollowPlaylist(string playlistId) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + + var statusCode = await API + .Put(URLs.PlaylistFollowers(playlistId), null, null) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task FollowPlaylist(string playlistId, FollowPlaylistRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API + .Put(URLs.PlaylistFollowers(playlistId), null, request.BuildBodyParams()) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public Task OfCurrentUser() + { + var request = new FollowOfCurrentUserRequest(); + + return OfCurrentUser(request); + } + + public Task OfCurrentUser(FollowOfCurrentUserRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.CurrentUserFollower(), request.BuildQueryParams()); + } + + public async Task Unfollow(UnfollowRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API + .Delete(URLs.CurrentUserFollower(), request.BuildQueryParams(), request.BuildBodyParams()) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task UnfollowPlaylist(string playlistId) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + + var statusCode = await API + .Delete(URLs.PlaylistFollowers(playlistId), null, null) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IAlbumsClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IAlbumsClient.cs new file mode 100644 index 0000000..0c2f9c6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IAlbumsClient.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about one or more albums from the Spotify catalog. + /// + public interface IAlbumsClient + { + /// + /// Get Spotify catalog information for multiple albums identified by their Spotify IDs. + /// + /// The request-model which contains required and optional parameters + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-multiple-albums + /// + /// + Task GetSeveral(AlbumsRequest request); + + /// + /// Get Spotify catalog information for a single album. + /// + /// The Spotify ID of the album. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-album + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string albumId); + + /// + /// Get Spotify catalog information for a single album. + /// + /// The Spotify ID of the album. + /// The request-model which contains required and optional parameters + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-album + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string albumId, AlbumRequest request); + + /// + /// Get Spotify catalog information about an album’s tracks. + /// Optional parameters can be used to limit the number of tracks returned. + /// + /// The Spotify ID of the album. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-albums-tracks + /// + /// + Task> GetTracks(string albumId); + + /// + /// Get Spotify catalog information about an album’s tracks. + /// Optional parameters can be used to limit the number of tracks returned. + /// + /// The Spotify ID of the album. + /// The request-model which contains required and optional parameters + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-albums-tracks + /// + /// + Task> GetTracks(string albumId, AlbumTracksRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IArtistsClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IArtistsClient.cs new file mode 100644 index 0000000..b74675d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IArtistsClient.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about one or more artists from the Spotify catalog. + /// + public interface IArtistsClient + { + /// + /// Get Spotify catalog information for several artists based on their Spotify IDs. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-multiple-artists + /// + /// The request-model which contains required and optional parameters + /// + Task GetSeveral(ArtistsRequest request); + + /// + /// Get Spotify catalog information for a single artist identified by their unique Spotify ID. + /// + /// The Spotify ID of the artist. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-artist + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string artistId); + + /// + /// Get Spotify catalog information about an artist’s albums. + /// Optional parameters can be specified in the query string to filter and sort the response. + /// + /// The Spotify ID for the artist. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-artists-albums + /// + /// + Task> GetAlbums(string artistId); + + /// + /// Get Spotify catalog information about an artist’s albums. + /// Optional parameters can be specified in the query string to filter and sort the response. + /// + /// The Spotify ID for the artist. + /// The request-model which contains required and optional parameters + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-artists-albums + /// + /// + Task> GetAlbums(string artistId, ArtistsAlbumsRequest request); + + + /// + /// Get Spotify catalog information about an artist’s top tracks by country. + /// + /// The Spotify ID for the artist + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-artists-top-tracks + /// + /// + Task GetTopTracks(string artistId, ArtistsTopTracksRequest request); + + + /// + /// Get Spotify catalog information about artists similar to a given artist. + /// Similarity is based on analysis of the Spotify community’s listening history. + /// + /// The Spotify ID for the artist + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-artists-related-artists + /// + /// + Task GetRelatedArtists(string artistId); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs new file mode 100644 index 0000000..7ee3636 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IBrowseClient.cs @@ -0,0 +1,130 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for getting playlists and new album releases featured on Spotify’s Browse tab. + /// + public interface IBrowseClient + { + /// + /// Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-categories + /// + /// + Task GetCategories(); + + /// + /// Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-categories + /// + /// + Task GetCategories(CategoriesRequest request); + + /// + /// Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). + /// + /// The Spotify category ID for the category. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-category + /// + /// + Task GetCategory(string categoryId); + + /// + /// Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). + /// + /// The Spotify category ID for the category. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-category + /// + /// + Task GetCategory(string categoryId, CategoryRequest request); + + /// + /// Get a list of Spotify playlists tagged with a particular category. + /// + /// The Spotify category ID for the category. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-categories-playlists + /// + /// + Task GetCategoryPlaylists(string categoryId); + + /// + /// Get a list of Spotify playlists tagged with a particular category. + /// + /// The Spotify category ID for the category. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-categories-playlists + /// + /// + Task GetCategoryPlaylists(string categoryId, CategoriesPlaylistsRequest request); + + /// + /// Recommendations are generated based on the available information for a given seed entity and matched against + /// similar artists and tracks. If there is sufficient information about the provided seeds, + /// a list of tracks will be returned together with pool size details. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-recommendations + /// + /// + Task GetRecommendations(RecommendationsRequest request); + + /// + /// Retrieve a list of available genres seed parameter values for recommendations. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-recommendation-genres + /// + /// + Task GetRecommendationGenres(); + + /// + /// Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-new-releases + /// + /// + Task GetNewReleases(); + + /// + /// Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-new-releases + /// + /// + Task GetNewReleases(NewReleasesRequest request); + + /// + /// Get a list of Spotify featured playlists (shown, for example, on a Spotify player’s ‘Browse’ tab). + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-featured-playlists + /// + /// + Task GetFeaturedPlaylists(); + + /// + /// Get a list of Spotify featured playlists (shown, for example, on a Spotify player’s ‘Browse’ tab). + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-featured-playlists + /// + /// + Task GetFeaturedPlaylists(FeaturedPlaylistsRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IEpisodesClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IEpisodesClient.cs new file mode 100644 index 0000000..0c18eba --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IEpisodesClient.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about one or more episodes from the Spotify catalog. + /// + public interface IEpisodesClient + { + /// + /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. + /// + /// The Spotify ID for the episode. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-episode + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string episodeId); + + /// + /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. + /// + /// The Spotify ID for the episode. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-an-episode + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string episodeId, EpisodeRequest request); + + /// + /// Get Spotify catalog information for several episodes based on their Spotify IDs. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-multiple-episodes + /// + /// + Task GetSeveral(EpisodesRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IFollowClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IFollowClient.cs new file mode 100644 index 0000000..0874c6a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IFollowClient.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for managing the artists, users, and playlists that a Spotify user follows. + /// + public interface IFollowClient + { + /// + /// Check to see if the current user is following one or more artists or other Spotify users. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-check-current-user-follows + /// + /// + Task> CheckCurrentUser(FollowCheckCurrentUserRequest request); + + /// + /// Check to see if one or more Spotify users are following a specified playlist. + /// + /// The Spotify ID of the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-check-if-user-follows-playlist + /// + /// + Task> CheckPlaylist(string playlistId, FollowCheckPlaylistRequest request); + + /// + /// Add the current user as a follower of one or more artists or other Spotify users. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-follow-artists-users + /// + /// + Task Follow(FollowRequest request); + + /// + /// Add the current user as a follower of a playlist. + /// + /// + /// The Spotify ID of the playlist. + /// Any playlist can be followed, regardless of its public/private status, + /// as long as you know its playlist ID. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-follow-playlist + /// + /// + Task FollowPlaylist(string playlistId); + + /// + /// Add the current user as a follower of a playlist. + /// + /// + /// The Spotify ID of the playlist. + /// Any playlist can be followed, regardless of its public/private status, + /// as long as you know its playlist ID. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-follow-playlist + /// + /// + Task FollowPlaylist(string playlistId, FollowPlaylistRequest request); + + /// + /// Get the current user’s followed artists. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-followed + /// + /// + Task OfCurrentUser(); + + /// + /// Get the current user’s followed artists. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-followed + /// + /// + Task OfCurrentUser(FollowOfCurrentUserRequest request); + + /// + /// Remove the current user as a follower of one or more artists or other Spotify users. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-unfollow-artists-users + /// + /// + Task Unfollow(UnfollowRequest request); + + /// + /// Remove the current user as a follower of a playlist. + /// + /// The Spotify ID of the playlist that is to be no longer followed. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-unfollow-playlist + /// + Task UnfollowPlaylist(string playlistId); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/ILibraryClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ILibraryClient.cs new file mode 100644 index 0000000..5e84d35 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ILibraryClient.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about, + /// and managing, tracks that the current user has saved in their “Your Music” library. + /// + public interface ILibraryClient + { + /// + /// Remove one or more albums from the current user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-remove-albums-user + /// + /// + Task RemoveAlbums(LibraryRemoveAlbumsRequest request); + + /// + /// Remove one or more tracks from the current user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-remove-tracks-user + /// + /// + Task RemoveTracks(LibraryRemoveTracksRequest request); + + /// + /// Delete one or more shows from current Spotify user’s library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-remove-shows-user + /// + /// + Task RemoveShows(LibraryRemoveShowsRequest request); + + /// + /// Save one or more tracks to the current user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-save-tracks-user + /// + /// + Task SaveTracks(LibrarySaveTracksRequest request); + + /// + /// Save one or more albums to the current user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-save-albums-user + /// + /// + Task SaveAlbums(LibrarySaveAlbumsRequest request); + + /// + /// Save one or more shows to current Spotify user’s library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-save-shows-user + /// + /// + Task SaveShows(LibrarySaveShowsRequest request); + + /// + /// Check if one or more tracks is already saved in the current Spotify user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-check-users-saved-tracks + /// + /// + Task> CheckTracks(LibraryCheckTracksRequest request); + + /// + /// Check if one or more albums is already saved in the current Spotify user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-check-users-saved-albums + /// + /// + Task> CheckAlbums(LibraryCheckAlbumsRequest request); + + /// + /// Check if one or more shows is already saved in the current Spotify user’s library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-check-users-saved-shows + /// + /// + Task> CheckShows(LibraryCheckShowsRequest request); + + /// + /// Get a list of the songs saved in the current Spotify user’s ‘Your Music’ library. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-tracks + /// + /// + Task> GetTracks(); + + /// + /// Get a list of the songs saved in the current Spotify user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-tracks + /// + /// + Task> GetTracks(LibraryTracksRequest request); + + /// + /// Get a list of the albums saved in the current Spotify user’s ‘Your Music’ library. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-albums + /// + /// + Task> GetAlbums(); + + /// + /// Get a list of the albums saved in the current Spotify user’s ‘Your Music’ library. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-albums + /// + /// + Task> GetAlbums(LibraryAlbumsRequest request); + + /// + /// Get a list of shows saved in the current Spotify user’s library. + /// Optional parameters can be used to limit the number of shows returned. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-shows + /// + /// + Task> GetShows(); + + /// + /// Get a list of shows saved in the current Spotify user’s library. + /// Optional parameters can be used to limit the number of shows returned. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-saved-shows + /// + /// + Task> GetShows(LibraryShowsRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs new file mode 100644 index 0000000..45060fc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// An OAuth Client, which allows to get inital and updated tokens from the Spotify Service + /// + public interface IOAuthClient + { + /// + /// Requests a new token using client_ids and client_secrets. + /// If the token is expired, simply call the funtion again to get a new token + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow + /// + /// + Task RequestToken(ClientCredentialsRequest request); + + /// + /// Refresh an already received token via Authorization Code Auth + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow + /// + /// + Task RequestToken(AuthorizationCodeRefreshRequest request); + + /// + /// Reequest an initial token via Authorization Code Auth + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow + /// + /// + Task RequestToken(AuthorizationCodeTokenRequest request); + + /// + /// Swaps out a received code with a access token using a remote server + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/ + /// + /// + Task RequestToken(TokenSwapTokenRequest request); + + /// + /// Gets a refreshed access token using an already received refresh token using a remote server + /// + /// + /// + /// https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/ + /// + /// + Task RequestToken(TokenSwapRefreshRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPaginator.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPaginator.cs new file mode 100644 index 0000000..7ca0fa1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPaginator.cs @@ -0,0 +1,74 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using SpotifyAPI.Web; + +namespace SpotifyAPI.Web +{ + /// + /// A paginator allows to cycle through all resources of the spotify API + /// + public interface IPaginator + { + /// + /// Fetches all pages and returns them grouped in a list + /// + /// The first page. Will be included in the result list! + /// An API Connector to make requests to spotify + /// Paging Type + /// A list containing all pages, including the firstPage + Task> PaginateAll(IPaginatable firstPage, IAPIConnector connector); + + /// + /// Fetches all pages and returns them grouped in a list. + /// Supports a mapping method which takes care of JSON mapping problems. + /// To give an example, the Search method always returns the paging objects nested in a key. The mapper functions + /// tells the paginate function where to find the actual paging object in the response. + /// + /// The first page. Will be included in the result list! + /// A function which returns the actual paging object in another response object + /// An API Connector to make requests to spotify + /// Paging Type + /// Outer response Type + /// A list containing all pages, including the firstPage + Task> PaginateAll( + IPaginatable firstPage, + Func> mapper, + IAPIConnector connector + ); + +#if !NETSTANDARD2_0 + /// + /// Fetches all pages and returns one by one using IAsyncEnumerable + /// + /// The first page. Will be included in the result list! + /// An API Connector to make requests to spotify + /// A CancellationToken + /// Paging Type + /// + IAsyncEnumerable Paginate(IPaginatable firstPage, IAPIConnector connector, CancellationToken cancel = default); + + /// + /// Fetches all pages and returns them grouped in a list. + /// Supports a mapping method which takes care of JSON mapping problems. + /// To give an example, the Search method always returns the paging objects nested in a key. The mapper functions + /// tells the paginate function where to find the actual paging object in the response. + /// + /// The first page. Will be included in the result list! + /// A function which returns the actual paging object in another response object + /// An API Connector to make requests to spotify + /// A CancellationToken + /// Paging Type + /// Outer response Type + /// + IAsyncEnumerable Paginate( + IPaginatable firstPage, + Func> mapper, + IAPIConnector connector, + CancellationToken cancel = default + ); +#endif + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPersonalizationClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPersonalizationClient.cs new file mode 100644 index 0000000..e80dcce --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPersonalizationClient.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about the user’s listening habits. + /// + public interface IPersonalizationClient + { + /// + /// Get the current user’s top tracks based on calculated affinity. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-top-artists-and-tracks + /// + /// + Task> GetTopTracks(); + + /// + /// Get the current user’s top tracks based on calculated affinity. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-top-artists-and-tracks + /// + /// + Task> GetTopTracks(PersonalizationTopRequest request); + + /// + /// Get the current user’s top artists based on calculated affinity. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-top-artists-and-tracks + /// + /// + Task> GetTopArtists(); + + /// + /// Get the current user’s top artists based on calculated affinity. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-top-artists-and-tracks + /// + /// + Task> GetTopArtists(PersonalizationTopRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlayerClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlayerClient.cs new file mode 100644 index 0000000..de7d9ca --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlayerClient.cs @@ -0,0 +1,208 @@ +using System.Net; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Player Endpoints. + /// These endpoints are in beta. + /// While we encourage you to build with them, a situation may arise + /// where we need to disable some or all of the functionality and/or change how + /// they work without prior notice. Please report any issues via our developer community forum. + /// + public interface IPlayerClient + { + /// + /// Skips to next track in the user’s queue. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-skip-users-playback-to-next-track + /// + /// + Task SkipNext(); + + /// + /// Skips to next track in the user’s queue. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-skip-users-playback-to-next-track + /// + /// + Task SkipNext(PlayerSkipNextRequest request); + + /// + /// Set the repeat mode for the user’s playback. Options are repeat-track, repeat-context, and off. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-set-repeat-mode-on-users-playback + /// + /// + Task SetRepeat(PlayerSetRepeatRequest request); + + /// + /// Transfer playback to a new device and determine if it should start playing. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-transfer-a-users-playback + /// + /// + Task TransferPlayback(PlayerTransferPlaybackRequest request); + + /// + /// Get the object currently being played on the user’s Spotify account. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-the-users-currently-playing-track + /// + /// + Task GetCurrentlyPlaying(PlayerCurrentlyPlayingRequest request); + + /// + /// Get information about the user’s current playback state, including track or episode, progress, and active device. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-information-about-the-users-current-playback + /// + /// + Task GetCurrentPlayback(); + + /// + /// Get information about the user’s current playback state, including track or episode, progress, and active device. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-information-about-the-users-current-playback + /// + /// + Task GetCurrentPlayback(PlayerCurrentPlaybackRequest request); + + /// + /// Seeks to the given position in the user’s currently playing track. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-seek-to-position-in-currently-playing-track + /// + /// + Task SeekTo(PlayerSeekToRequest request); + + /// + /// Skips to previous track in the user’s queue. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-skip-users-playback-to-previous-track + /// + /// + Task SkipPrevious(); + + /// + /// Skips to previous track in the user’s queue. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-skip-users-playback-to-previous-track + /// + /// + Task SkipPrevious(PlayerSkipPreviousRequest request); + + /// + /// Start a new context or resume current playback on the user’s active device. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-start-a-users-playback + /// + /// + Task ResumePlayback(); + + /// + /// Start a new context or resume current playback on the user’s active device. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-start-a-users-playback + /// + /// + Task ResumePlayback(PlayerResumePlaybackRequest request); + + /// + /// Pause playback on the user’s account. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-pause-a-users-playback + /// + /// + Task PausePlayback(); + + /// + /// Pause playback on the user’s account. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-pause-a-users-playback + /// + /// + Task PausePlayback(PlayerPausePlaybackRequest request); + + /// + /// Set the volume for the user’s current playback device. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-set-volume-for-users-playback + /// + /// + Task SetVolume(PlayerVolumeRequest request); + + /// + /// Get tracks from the current user’s recently played tracks. Note: Currently doesn’t support podcast episodes. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-recently-played + /// + /// + Task> GetRecentlyPlayed(); + + /// + /// Get tracks from the current user’s recently played tracks. Note: Currently doesn’t support podcast episodes. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-recently-played + /// + /// + Task> GetRecentlyPlayed(PlayerRecentlyPlayedRequest request); + + /// + /// Get information about a user’s available devices. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-users-available-devices + /// + /// + Task GetAvailableDevices(); + + /// + /// Toggle shuffle on or off for user’s playback. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-toggle-shuffle-for-users-playback + /// + /// + Task SetShuffle(PlayerShuffleRequest request); + + /// + /// Add an item to the end of the user’s current playback queue. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-add-to-queue + /// + /// + Task AddToQueue(PlayerAddToQueueRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs new file mode 100644 index 0000000..ce6c078 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about a user’s playlists and for managing a user’s playlists. + /// + public interface IPlaylistsClient + { + /// + /// Remove one or more items from a user’s playlist. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-remove-tracks-playlist + /// + /// + Task RemoveItems(string playlistId, PlaylistRemoveItemsRequest request); + + /// + /// Add one or more items to a user’s playlist. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-add-tracks-to-playlist + /// + /// + Task AddItems(string playlistId, PlaylistAddItemsRequest request); + + /// + /// Get full details of the items of a playlist owned by a Spotify user. + /// + /// The Spotify ID for the playlist. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks + /// + /// + Task>> GetItems(string playlistId); + + /// + /// Get full details of the items of a playlist owned by a Spotify user. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks + /// + /// + Task>> GetItems(string playlistId, PlaylistGetItemsRequest request); + + /// + /// Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) + /// + /// The user’s Spotify user ID. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-create-playlist + /// + /// + Task Create(string userId, PlaylistCreateRequest request); + + /// + /// Replace the image used to represent a specific playlist. + /// + /// The Spotify ID for the playlist. + /// Base64 encoded JPEG image data, maximum payload size is 256 KB. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-upload-custom-playlist-cover + /// + /// + Task UploadCover(string playlistId, string base64JpgData); + + /// + /// Get the current image associated with a specific playlist. + /// + /// The Spotify ID for the playlist. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlist-cover + /// + /// + Task> GetCovers(string playlistId); + + /// + /// Get a list of the playlists owned or followed by a Spotify user. + /// + /// The user’s Spotify user ID. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-list-users-playlists + /// + /// + Task> GetUsers(string userId); + + /// + /// Get a list of the playlists owned or followed by a Spotify user. + /// + /// The user’s Spotify user ID. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-list-users-playlists + /// + /// + Task> GetUsers(string userId, PlaylistGetUsersRequest request); + + /// + /// Get a playlist owned by a Spotify user. + /// + /// The Spotify ID for the playlist. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlist + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string playlistId); + + /// + /// Get a playlist owned by a Spotify user. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlist + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string playlistId, PlaylistGetRequest request); + + /// + /// Replace all the items in a playlist, overwriting its existing items. + /// This powerful request can be useful for replacing items, re-ordering existing items, or clearing the playlist. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-replace-playlists-tracks + /// + /// + Task ReplaceItems(string playlistId, PlaylistReplaceItemsRequest request); + + /// + /// Get a list of the playlists owned or followed by the current Spotify user. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-list-of-current-users-playlists + /// + /// + Task> CurrentUsers(); + + /// + /// Get a list of the playlists owned or followed by the current Spotify user. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-list-of-current-users-playlists + /// + /// + Task> CurrentUsers(PlaylistCurrentUsersRequest request); + + /// + /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-change-playlist-details + /// + /// + Task ChangeDetails(string playlistId, PlaylistChangeDetailsRequest request); + + /// + /// Reorder an item or a group of items in a playlist. + /// + /// The Spotify ID for the playlist. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-reorder-playlists-tracks + /// + /// + Task ReorderItems(string playlistId, PlaylistReorderItemsRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISearchClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISearchClient.cs new file mode 100644 index 0000000..ddb5be6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISearchClient.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Search Endpoints + /// + public interface ISearchClient + { + /// + /// Get Spotify Catalog information about albums, artists, playlists, + /// tracks, shows or episodes that match a keyword string. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-search + /// + /// + Task Item(SearchRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs new file mode 100644 index 0000000..06a671d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about one or more shows from the Spotify catalog. + /// + public interface IShowsClient + { + /// + /// Get Spotify catalog information for a single show identified by its unique Spotify ID. + /// + /// The Spotify ID for the show. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-show + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string showId); + + /// + /// Get Spotify catalog information for a single show identified by its unique Spotify ID. + /// + /// The Spotify ID for the show. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-show + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string showId, ShowRequest request); + + /// + /// Get Spotify catalog information for several shows based on their Spotify IDs. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-multiple-shows + /// + /// + Task GetSeveral(ShowsRequest request); + + /// + /// Get Spotify catalog information about an show’s episodes. + /// Optional parameters can be used to limit the number of episodes returned. + /// + /// The Spotify ID for the show. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-shows-episodes + /// + /// + Task> GetEpisodes(string showId); + + /// + /// Get Spotify catalog information about an show’s episodes. + /// Optional parameters can be used to limit the number of episodes returned. + /// + /// The Spotify ID for the show. + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-a-shows-episodes + /// + /// + Task> GetEpisodes(string showId, ShowEpisodesRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs new file mode 100644 index 0000000..dfa7227 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs @@ -0,0 +1,181 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public interface ISpotifyClient + { + /// + /// The default paginator used by the Paginator methods + /// + /// + IPaginator DefaultPaginator { get; } + + /// + /// Operations related to Spotify User Profiles + /// + /// + IUserProfileClient UserProfile { get; } + + /// + /// Operations related to Spotify Browse Endpoints + /// + /// + IBrowseClient Browse { get; } + + /// + /// Operations related to Spotify Shows + /// + /// + IShowsClient Shows { get; } + + /// + /// Operations related to Spotify Playlists + /// + /// + IPlaylistsClient Playlists { get; } + + /// + /// Operations related to Spotify Search + /// + /// + ISearchClient Search { get; } + + /// + /// Operations related to Spotify Follows + /// + /// + IFollowClient Follow { get; } + + /// + /// Operations related to Spotify Tracks + /// + /// + ITracksClient Tracks { get; } + + /// + /// Operations related to Spotify Player Endpoints + /// + /// + IPlayerClient Player { get; } + + /// + /// Operations related to Spotify Albums + /// + /// + IAlbumsClient Albums { get; } + + /// + /// Operations related to Spotify Artists + /// + /// + IArtistsClient Artists { get; } + + /// + /// Operations related to Spotify Personalization Endpoints + /// + /// + IPersonalizationClient Personalization { get; } + + /// + /// Operations related to Spotify Podcast Episodes + /// + /// + IEpisodesClient Episodes { get; } + + /// + /// Operations related to Spotify User Library + /// + /// + ILibraryClient Library { get; } + + /// + /// Returns the last response received by an API call. + /// + /// + IResponse? LastResponse { get; } + + /// + /// Fetches all pages and returns them grouped in a list. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// The first page, will be included in the output list! + /// Optional. If not supplied, DefaultPaginator will be used + /// The Paging-Type + /// A list containing all fetched pages + Task> PaginateAll(IPaginatable firstPage, IPaginator? paginator = default!); + + /// + /// Fetches all pages and returns them grouped in a list. + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// The Paging-Type + /// The Response-Type + /// A list containing all fetched pages + Task> PaginateAll( + IPaginatable firstPage, + Func> mapper, + IPaginator? paginator = default! + ); + +#if !NETSTANDARD2_0 + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// An iterable IAsyncEnumerable + IAsyncEnumerable Paginate( + IPaginatable firstPage, + IPaginator? paginator = default!, + CancellationToken cancellationToken = default! + ); + + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// The Response-Type + /// + IAsyncEnumerable Paginate( + IPaginatable firstPage, + Func> mapper, + IPaginator? paginator = default!, + CancellationToken cancellationToken = default! + ); + +#endif + + public Task> NextPage(Paging paging); + + public Task> NextPage(CursorPaging cursorPaging); + + public Task NextPage(IPaginatable paginatable); + + public Task> PreviousPage(Paging paging); + + public Task PreviousPage(Paging paging); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/ITracksClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ITracksClient.cs new file mode 100644 index 0000000..06f2f20 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/ITracksClient.cs @@ -0,0 +1,70 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about one or more tracks from the Spotify catalog. + /// + public interface ITracksClient + { + /// + /// Get Spotify catalog information for multiple tracks based on their Spotify IDs. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-several-tracks + /// + /// + Task GetSeveral(TracksRequest request); + + /// + /// Get a detailed audio analysis for a single track identified by its unique Spotify ID. + /// + /// The Spotify ID for the track. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-audio-analysis + /// + /// + Task GetAudioAnalysis(string trackId); + + /// + /// Get audio feature information for a single track identified by its unique Spotify ID. + /// + /// The Spotify ID for the track. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-audio-features + /// + /// + Task GetAudioFeatures(string trackId); + + /// + /// Get Spotify catalog information for a single track identified by its unique Spotify ID. + /// + /// The Spotify ID for the track. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-track + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string trackId); + + /// + /// Get Spotify catalog information for a single track identified by its unique Spotify ID. + /// + /// The Spotify ID for the track. + /// The request-model which contains required and optional parameters. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string trackId, TrackRequest request); + + /// + /// Get audio features for multiple tracks based on their Spotify IDs. + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-several-audio-features + /// + /// + Task GetSeveralAudioFeatures(TracksAudioFeaturesRequest request); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/Interfaces/IUserProfileClient.cs b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IUserProfileClient.cs new file mode 100644 index 0000000..ae71699 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/Interfaces/IUserProfileClient.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + /// + /// Endpoints for retrieving information about a user’s profile. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#category-user-profile + public interface IUserProfileClient + { + /// + /// Get detailed profile information about the current user (including the current user’s username). + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-current-users-profile + /// + /// Thrown if the client is not authenticated. + Task Current(); + + /// + /// Get public profile information about a Spotify user. + /// + /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-users-profile + /// Thrown if the client is not authenticated. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(string userId); + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/LibraryClient.cs b/Source Files/SpotifyAPI.Web/Clients/LibraryClient.cs new file mode 100644 index 0000000..7c8639f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/LibraryClient.cs @@ -0,0 +1,117 @@ +using System.Net; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class LibraryClient : APIClient, ILibraryClient + { + public LibraryClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task> CheckAlbums(LibraryCheckAlbumsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryAlbumsContains(), request.BuildQueryParams()); + } + + public Task> CheckShows(LibraryCheckShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryShowsContains(), request.BuildQueryParams()); + } + + public Task> CheckTracks(LibraryCheckTracksRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryTracksContains(), request.BuildQueryParams()); + } + + public Task> GetAlbums() + { + return API.Get>(SpotifyUrls.LibraryAlbums()); + } + + public Task> GetAlbums(LibraryAlbumsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryAlbums(), request.BuildQueryParams()); + } + + public Task> GetShows() + { + return API.Get>(SpotifyUrls.LibraryShows()); + } + + public Task> GetShows(LibraryShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryShows(), request.BuildQueryParams()); + } + + public Task> GetTracks() + { + return API.Get>(SpotifyUrls.LibraryTracks()); + } + + public Task> GetTracks(LibraryTracksRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(SpotifyUrls.LibraryTracks(), request.BuildQueryParams()); + } + + public async Task RemoveAlbums(LibraryRemoveAlbumsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Delete(SpotifyUrls.LibraryAlbums(), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task RemoveShows(LibraryRemoveShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Delete(SpotifyUrls.LibraryShows(), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task RemoveTracks(LibraryRemoveTracksRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Delete(SpotifyUrls.LibraryTracks(), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task SaveAlbums(LibrarySaveAlbumsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(SpotifyUrls.LibraryAlbums(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task SaveShows(LibrarySaveShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(SpotifyUrls.LibraryShows(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public async Task SaveTracks(LibrarySaveTracksRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(SpotifyUrls.LibraryTracks(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/OAuthClient.cs b/Source Files/SpotifyAPI.Web/Clients/OAuthClient.cs new file mode 100644 index 0000000..06bfb7c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/OAuthClient.cs @@ -0,0 +1,267 @@ +using System.Text; +using System; +using System.Net.Http; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class OAuthClient : APIClient, IOAuthClient + { + public OAuthClient() : this(SpotifyClientConfig.CreateDefault()) { } + public OAuthClient(IAPIConnector apiConnector) : base(apiConnector) { } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")] + public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { } + + /// + /// Requests a new token using pkce flow + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce + /// + /// 1 + public Task RequestToken(PKCETokenRequest request) + { + return RequestToken(request, API); + } + + /// + /// Refreshes a token using pkce flow + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce + /// + /// 1 + public Task RequestToken(PKCETokenRefreshRequest request) + { + return RequestToken(request, API); + } + + /// + /// Requests a new token using client_ids and client_secrets. + /// If the token is expired, simply call the funtion again to get a new token + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow + /// + /// 1 + public Task RequestToken(ClientCredentialsRequest request) + { + return RequestToken(request, API); + } + + /// + /// Refresh an already received token via Authorization Code Auth + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow + /// + /// + public Task RequestToken(AuthorizationCodeRefreshRequest request) + { + return RequestToken(request, API); + } + + /// + /// Reequest an initial token via Authorization Code Auth + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow + /// + /// + public Task RequestToken(AuthorizationCodeTokenRequest request) + { + return RequestToken(request, API); + } + + /// + /// Swaps out a received code with a access token using a remote server + /// + /// The request-model which contains required and optional parameters. + /// + /// https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/ + /// + /// + public Task RequestToken(TokenSwapTokenRequest request) + { + return RequestToken(request, API); + } + + /// + /// Gets a refreshed access token using an already received refresh token using a remote server + /// + /// + /// + /// https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/ + /// + /// + public Task RequestToken(TokenSwapRefreshRequest request) + { + return RequestToken(request, API); + } + + public static Task RequestToken(PKCETokenRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("client_id", request.ClientId), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", request.Code), + new KeyValuePair("redirect_uri", request.RedirectUri.ToString()), + new KeyValuePair("code_verifier", request.CodeVerifier), + }; + + return SendOAuthRequest(apiConnector, form, null, null); + } + + public static Task RequestToken(PKCETokenRefreshRequest request, IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("client_id", request.ClientId), + new KeyValuePair("grant_type", "refresh_token"), + new KeyValuePair("refresh_token", request.RefreshToken), + }; + + return SendOAuthRequest(apiConnector, form, null, null); + } + + public static Task RequestToken( + TokenSwapRefreshRequest request, IAPIConnector apiConnector + ) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("refresh_token", request.RefreshToken) + }; +#pragma warning disable CA2000 + return apiConnector.Post( + request.RefreshUri, null, new FormUrlEncodedContent(form) + ); +#pragma warning restore CA2000 + } + + public static Task RequestToken( + TokenSwapTokenRequest request, IAPIConnector apiConnector + ) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("code", request.Code) + }; + +#pragma warning disable CA2000 + return apiConnector.Post( + request.TokenUri, null, new FormUrlEncodedContent(form) + ); +#pragma warning restore CA2000 + } + + public static Task RequestToken( + ClientCredentialsRequest request, IAPIConnector apiConnector + ) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("grant_type", "client_credentials") + }; + + return SendOAuthRequest(apiConnector, form, request.ClientId, request.ClientSecret); + } + + public static Task RequestToken( + AuthorizationCodeRefreshRequest request, IAPIConnector apiConnector + ) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("grant_type", "refresh_token"), + new KeyValuePair("refresh_token", request.RefreshToken) + }; + + return SendOAuthRequest(apiConnector, form, request.ClientId, request.ClientSecret); + } + + public static Task RequestToken( + AuthorizationCodeTokenRequest request, IAPIConnector apiConnector + ) + { + Ensure.ArgumentNotNull(request, nameof(request)); + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + var form = new List> + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", request.Code), + new KeyValuePair("redirect_uri", request.RedirectUri.ToString()) + }; + + return SendOAuthRequest(apiConnector, form, request.ClientId, request.ClientSecret); + } + + private static Task SendOAuthRequest( + IAPIConnector apiConnector, + List> form, + string? clientId, + string? clientSecret) + { + var headers = BuildAuthHeader(clientId, clientSecret); +#pragma warning disable CA2000 + return apiConnector.Post(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers); +#pragma warning restore CA2000 + } + + private static Dictionary BuildAuthHeader(string? clientId, string? clientSecret) + { + if (clientId == null || clientSecret == null) + { + return new Dictionary(); + } + + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}")); + return new Dictionary + { + { "Authorization", $"Basic {base64}"} + }; + } + + private static APIConnector ValidateConfig(SpotifyClientConfig config) + { + Ensure.ArgumentNotNull(config, nameof(config)); + + return new APIConnector( + config.BaseAddress, + config.Authenticator, + config.JSONSerializer, + config.HTTPClient, + config.RetryHandler, + config.HTTPLogger + ); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/PersonalizationClient.cs b/Source Files/SpotifyAPI.Web/Clients/PersonalizationClient.cs new file mode 100644 index 0000000..e9af2c6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/PersonalizationClient.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class PersonalizationClient : APIClient, IPersonalizationClient + { + public PersonalizationClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task> GetTopArtists() + { + return API.Get>(URLs.PersonalizationTop("artists")); + } + + public Task> GetTopArtists(PersonalizationTopRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.PersonalizationTop("artists"), request.BuildQueryParams()); + } + + public Task> GetTopTracks() + { + return API.Get>(URLs.PersonalizationTop("tracks")); + } + + public Task> GetTopTracks(PersonalizationTopRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.PersonalizationTop("tracks"), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/PlayerClient.cs b/Source Files/SpotifyAPI.Web/Clients/PlayerClient.cs new file mode 100644 index 0000000..87b3318 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/PlayerClient.cs @@ -0,0 +1,154 @@ +using System.Net; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class PlayerClient : APIClient, IPlayerClient + { + public PlayerClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public async Task AddToQueue(PlayerAddToQueueRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Post(URLs.PlayerQueue(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public Task GetAvailableDevices() + { + return API.Get(URLs.PlayerDevices()); + } + + public Task GetCurrentlyPlaying(PlayerCurrentlyPlayingRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.PlayerCurrentlyPlaying(), request.BuildQueryParams()); + } + + public Task GetCurrentPlayback() + { + return API.Get(URLs.Player()); + } + + public Task GetCurrentPlayback(PlayerCurrentPlaybackRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Player(), request.BuildQueryParams()); + } + + public Task> GetRecentlyPlayed() + { + return API.Get>(URLs.PlayerRecentlyPlayed()); + } + + public Task> GetRecentlyPlayed(PlayerRecentlyPlayedRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.PlayerRecentlyPlayed(), request.BuildQueryParams()); + } + + public async Task PausePlayback() + { + var statusCode = await API.Put(URLs.PlayerPause(), null, null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task PausePlayback(PlayerPausePlaybackRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlayerPause(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task ResumePlayback() + { + var statusCode = await API.Put(URLs.PlayerResume(), null, null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task ResumePlayback(PlayerResumePlaybackRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API + .Put(URLs.PlayerResume(), request.BuildQueryParams(), request.BuildBodyParams()) + .ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SeekTo(PlayerSeekToRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlayerSeek(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SetRepeat(PlayerSetRepeatRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlayerRepeat(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SetShuffle(PlayerShuffleRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlayerShuffle(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SetVolume(PlayerVolumeRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlayerVolume(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SkipNext() + { + var statusCode = await API.Post(URLs.PlayerNext(), null, null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SkipNext(PlayerSkipNextRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Post(URLs.PlayerNext(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SkipPrevious() + { + var statusCode = await API.Post(URLs.PlayerPrevious(), null, null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task SkipPrevious(PlayerSkipPreviousRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Post(URLs.PlayerPrevious(), request.BuildQueryParams(), null).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + + public async Task TransferPlayback(PlayerTransferPlaybackRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.Player(), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.NoContent; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/PlaylistsClient.cs b/Source Files/SpotifyAPI.Web/Clients/PlaylistsClient.cs new file mode 100644 index 0000000..6a598d0 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/PlaylistsClient.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class PlaylistsClient : APIClient, IPlaylistsClient + { + public PlaylistsClient(IAPIConnector connector) : base(connector) { } + + public Task RemoveItems(string playlistId, PlaylistRemoveItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Delete(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()); + } + + public Task AddItems(string playlistId, PlaylistAddItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Post(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()); + } + + public Task>> GetItems(string playlistId) + { + var request = new PlaylistGetItemsRequest(); + + return GetItems(playlistId, request); + } + + public Task>> GetItems(string playlistId, PlaylistGetItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>>(URLs.PlaylistTracks(playlistId), request.BuildQueryParams()); + } + + public Task Create(string userId, PlaylistCreateRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(userId, nameof(userId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Post(URLs.UserPlaylists(userId), null, request.BuildBodyParams()); + } + + public async Task UploadCover(string playlistId, string base64JpgData) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNullOrEmptyString(base64JpgData, nameof(base64JpgData)); + + var statusCode = await API.PutRaw(URLs.PlaylistImages(playlistId), null, base64JpgData).ConfigureAwait(false); + return statusCode == HttpStatusCode.Accepted; + } + + public Task> GetCovers(string playlistId) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + + return API.Get>(URLs.PlaylistImages(playlistId)); + } + + public Task> GetUsers(string userId) + { + Ensure.ArgumentNotNullOrEmptyString(userId, nameof(userId)); + + return API.Get>(URLs.UserPlaylists(userId)); + } + + public Task> GetUsers(string userId, PlaylistGetUsersRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(userId, nameof(userId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.UserPlaylists(userId), request.BuildQueryParams()); + } + + public Task Get(string playlistId) + { + var request = new PlaylistGetRequest(); // We need some defaults + + return Get(playlistId, request); + } + + public Task Get(string playlistId, PlaylistGetRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Playlist(playlistId), request.BuildQueryParams()); + } + + public async Task ReplaceItems(string playlistId, PlaylistReplaceItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.Created; + } + + public Task> CurrentUsers() + { + return API.Get>(URLs.CurrentUserPlaylists()); + } + + public Task> CurrentUsers(PlaylistCurrentUsersRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.CurrentUserPlaylists(), request.BuildQueryParams()); + } + + public async Task ChangeDetails(string playlistId, PlaylistChangeDetailsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + var statusCode = await API.Put(URLs.Playlist(playlistId), null, request.BuildBodyParams()).ConfigureAwait(false); + return statusCode == HttpStatusCode.OK; + } + + public Task ReorderItems(string playlistId, PlaylistReorderItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Put(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/SearchClient.cs b/Source Files/SpotifyAPI.Web/Clients/SearchClient.cs new file mode 100644 index 0000000..b7e9b1b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/SearchClient.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class SearchClient : APIClient, ISearchClient + { + public SearchClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Item(SearchRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Search(), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/ShowsClient.cs b/Source Files/SpotifyAPI.Web/Clients/ShowsClient.cs new file mode 100644 index 0000000..c90fcf3 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/ShowsClient.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class ShowsClient : APIClient, IShowsClient + { + public ShowsClient(IAPIConnector connector) : base(connector) { } + + public Task Get(string showId) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + + return API.Get(URLs.Show(showId)); + } + + public Task Get(string showId, ShowRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Show(showId), request.BuildQueryParams()); + } + + public Task GetSeveral(ShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Shows(), request.BuildQueryParams()); + } + + public Task> GetEpisodes(string showId) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + + return API.Get>(URLs.ShowEpisodes(showId)); + } + + public Task> GetEpisodes(string showId, ShowEpisodesRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.ShowEpisodes(showId), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/SimplePaginator.cs b/Source Files/SpotifyAPI.Web/Clients/SimplePaginator.cs new file mode 100644 index 0000000..76edf8d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/SimplePaginator.cs @@ -0,0 +1,130 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using System.Runtime.CompilerServices; + +namespace SpotifyAPI.Web +{ + public class SimplePaginator : IPaginator + { + protected virtual Task ShouldContinue(List results, IPaginatable page) + { + return Task.FromResult(true); + } + protected virtual Task ShouldContinue(List results, IPaginatable page) + { + return Task.FromResult(true); + } + + public async Task> PaginateAll(IPaginatable firstPage, IAPIConnector connector) + { + Ensure.ArgumentNotNull(firstPage, nameof(firstPage)); + Ensure.ArgumentNotNull(connector, nameof(connector)); + + var page = firstPage; + var results = new List(); + if (page.Items != null) + { + results.AddRange(page.Items); + } + while (page.Next != null && await ShouldContinue(results, page).ConfigureAwait(false)) + { + page = await connector.Get>(new Uri(page.Next, UriKind.Absolute)).ConfigureAwait(false); + if (page.Items != null) + { + results.AddRange(page.Items); + } + } + + return results; + } + + public async Task> PaginateAll( + IPaginatable firstPage, Func> mapper, IAPIConnector connector + ) + { + Ensure.ArgumentNotNull(firstPage, nameof(firstPage)); + Ensure.ArgumentNotNull(mapper, nameof(mapper)); + Ensure.ArgumentNotNull(connector, nameof(connector)); + + var page = firstPage; + var results = new List(); + if (page.Items != null) + { + results.AddRange(page.Items); + } + while (page.Next != null && await ShouldContinue(results, page).ConfigureAwait(false)) + { + var next = await connector.Get(new Uri(page.Next, UriKind.Absolute)).ConfigureAwait(false); + page = mapper(next); + if (page.Items != null) + { + results.AddRange(page.Items); + } + } + + return results; + } + +#if !NETSTANDARD2_0 + public async IAsyncEnumerable Paginate( + IPaginatable firstPage, + IAPIConnector connector, + [EnumeratorCancellation] CancellationToken cancel = default) + { + Ensure.ArgumentNotNull(firstPage, nameof(firstPage)); + Ensure.ArgumentNotNull(connector, nameof(connector)); + if (firstPage.Items == null) + { + throw new ArgumentException("The first page has to contain an Items list!", nameof(firstPage)); + } + + var page = firstPage; + foreach (var item in page.Items) + { + yield return item; + } + while (page.Next != null) + { + page = await connector.Get>(new Uri(page.Next, UriKind.Absolute)).ConfigureAwait(false); + foreach (var item in page.Items!) + { + yield return item; + } + } + } + + public async IAsyncEnumerable Paginate( + IPaginatable firstPage, + Func> mapper, + IAPIConnector connector, + [EnumeratorCancellation] CancellationToken cancel = default) + { + Ensure.ArgumentNotNull(firstPage, nameof(firstPage)); + Ensure.ArgumentNotNull(mapper, nameof(mapper)); + Ensure.ArgumentNotNull(connector, nameof(connector)); + if (firstPage.Items == null) + { + throw new ArgumentException("The first page has to contain an Items list!", nameof(firstPage)); + } + + var page = firstPage; + foreach (var item in page.Items) + { + yield return item; + } + while (page.Next != null) + { + var next = await connector.Get(new Uri(page.Next, UriKind.Absolute)).ConfigureAwait(false); + page = mapper(next); + foreach (var item in page.Items!) + { + yield return item; + } + } + } +#endif + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/SpotifyClient.cs b/Source Files/SpotifyAPI.Web/Clients/SpotifyClient.cs new file mode 100644 index 0000000..92f7710 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/SpotifyClient.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace SpotifyAPI.Web +{ + public class SpotifyClient : ISpotifyClient + { + private readonly IAPIConnector _apiConnector; + + public SpotifyClient(IToken token) : + this(SpotifyClientConfig.CreateDefault(token?.AccessToken ?? throw new ArgumentNullException(nameof(token)), token.TokenType)) + { } + + public SpotifyClient(string token, string tokenType = "Bearer") : + this(SpotifyClientConfig.CreateDefault(token, tokenType)) + { } + + public SpotifyClient(SpotifyClientConfig config) + { + Ensure.ArgumentNotNull(config, nameof(config)); + if (config.Authenticator == null) + { +#pragma warning disable CA2208 + throw new ArgumentNullException("Authenticator in config is null. Please supply it via `WithAuthenticator` or `WithToken`"); +#pragma warning restore CA2208 + } + + _apiConnector = config.BuildAPIConnector(); + _apiConnector.ResponseReceived += (sender, response) => + { + LastResponse = response; + }; + + DefaultPaginator = config.DefaultPaginator; + UserProfile = new UserProfileClient(_apiConnector); + Browse = new BrowseClient(_apiConnector); + Shows = new ShowsClient(_apiConnector); + Playlists = new PlaylistsClient(_apiConnector); + Search = new SearchClient(_apiConnector); + Follow = new FollowClient(_apiConnector); + Tracks = new TracksClient(_apiConnector); + Player = new PlayerClient(_apiConnector); + Albums = new AlbumsClient(_apiConnector); + Artists = new ArtistsClient(_apiConnector); + Personalization = new PersonalizationClient(_apiConnector); + Episodes = new EpisodesClient(_apiConnector); + Library = new LibraryClient(_apiConnector); + } + + public IPaginator DefaultPaginator { get; } + + public IUserProfileClient UserProfile { get; } + + public IBrowseClient Browse { get; } + + public IShowsClient Shows { get; } + + public IPlaylistsClient Playlists { get; } + + public ISearchClient Search { get; } + + public IFollowClient Follow { get; } + + public ITracksClient Tracks { get; } + + public IPlayerClient Player { get; } + + public IAlbumsClient Albums { get; } + + public IArtistsClient Artists { get; } + + public IPersonalizationClient Personalization { get; } + + public IEpisodesClient Episodes { get; } + + public ILibraryClient Library { get; } + + public IResponse? LastResponse { get; private set; } + + /// + /// Fetches all pages and returns them grouped in a list. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// The first page, will be included in the output list! + /// Optional. If not supplied, DefaultPaginator will be used + /// The Paging-Type + /// A list containing all fetched pages + public Task> PaginateAll(IPaginatable firstPage, IPaginator? paginator = null) + { + return (paginator ?? DefaultPaginator).PaginateAll(firstPage, _apiConnector); + } + + /// + /// Fetches all pages and returns them grouped in a list. + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// The Paging-Type + /// The Response-Type + /// A list containing all fetched pages + public Task> PaginateAll( + IPaginatable firstPage, + Func> mapper, + IPaginator? paginator = null + ) + { + return (paginator ?? DefaultPaginator).PaginateAll(firstPage, mapper, _apiConnector); + } + + private Task FetchPage(string? nextUrl) + { + if (nextUrl == null) + { + throw new APIPagingException("The paging object has no next page"); + } + + return _apiConnector.Get(new Uri(nextUrl, UriKind.Absolute)); + } + + /// + /// Fetches the next page of the paging object + /// + /// A paging object which has a next page + /// + /// + public Task> NextPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage>(paging.Next); + } + + /// + /// Fetches the next page of the cursor paging object + /// + /// A cursor paging object which has a next page + /// + /// + public Task> NextPage(CursorPaging cursorPaging) + { + Ensure.ArgumentNotNull(cursorPaging, nameof(cursorPaging)); + return FetchPage>(cursorPaging.Next); + } + + /// + /// Fetches the next page of the complex IPaginatable object. + /// + /// A complex IPaginatable object with a next page + /// + /// The type of the next page + /// + public Task NextPage(IPaginatable paginatable) + { + Ensure.ArgumentNotNull(paginatable, nameof(paginatable)); + return FetchPage(paginatable.Next); + } + + /// + /// Fetches the previous page of the paging object. + /// + /// A paging object with a previous page + /// + /// + public Task> PreviousPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage>(paging.Previous); + } + + + /// + /// Fetches the previous page of the complex paging object. + /// + /// A complex paging object with a previous page + /// + /// The type of the next page + /// + public Task PreviousPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage(paging.Previous); + } + +#if !NETSTANDARD2_0 + + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// An iterable IAsyncEnumerable + public IAsyncEnumerable Paginate( + IPaginatable firstPage, + IPaginator? paginator = null, + CancellationToken cancellationToken = default + ) + { + return (paginator ?? DefaultPaginator).Paginate(firstPage, _apiConnector, cancellationToken); + } + + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// The Response-Type + /// + public IAsyncEnumerable Paginate( + IPaginatable firstPage, + Func> mapper, + IPaginator? paginator = null, + CancellationToken cancellationToken = default + ) + { + return (paginator ?? DefaultPaginator).Paginate(firstPage, mapper, _apiConnector, cancellationToken); + } + + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A Function to retrive the first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// The Response-Type + /// + public async IAsyncEnumerable Paginate( + Func>> getFirstPage, + Func> mapper, + IPaginator? paginator = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Ensure.ArgumentNotNull(getFirstPage, nameof(getFirstPage)); + + var firstPage = await getFirstPage().ConfigureAwait(false); + await foreach (var item in (paginator ?? DefaultPaginator) + .Paginate(firstPage, mapper, _apiConnector, cancellationToken) + .WithCancellation(cancellationToken) + ) + { + yield return item; + } + } + + /// + /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 + /// Some responses (e.g search response) have the pagination nested in a JSON Property. + /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. + /// The default paginator will fetch all available resources without a delay between requests. + /// This can drain your request limit quite fast, so consider using a custom paginator with delays. + /// + /// A Task to retrive the first page, will be included in the output list! + /// A function which maps response objects to the next paging object + /// Optional. If not supplied, DefaultPaginator will be used + /// An optional Cancellation Token + /// The Paging-Type + /// The Response-Type + /// + public async IAsyncEnumerable Paginate( + Task> firstPageTask, + Func> mapper, + IPaginator? paginator = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Ensure.ArgumentNotNull(firstPageTask, nameof(firstPageTask)); + + var firstPage = await firstPageTask.ConfigureAwait(false); + await foreach (var item in (paginator ?? DefaultPaginator) + .Paginate(firstPage, mapper, _apiConnector, cancellationToken) + .WithCancellation(cancellationToken) + ) + { + yield return item; + } + } +#endif + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/Source Files/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs new file mode 100644 index 0000000..56a419c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -0,0 +1,198 @@ +using System.Net.Http; +using System; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class SpotifyClientConfig + { + public Uri BaseAddress { get; private set; } + public IAuthenticator? Authenticator { get; private set; } + public IJSONSerializer JSONSerializer { get; private set; } + public IHTTPClient HTTPClient { get; private set; } + public IHTTPLogger? HTTPLogger { get; private set; } + public IRetryHandler? RetryHandler { get; private set; } + public IPaginator DefaultPaginator { get; private set; } + public IAPIConnector? APIConnector { get; private set; } + + /// + /// This config spefies the internal parts of the SpotifyClient. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SpotifyClientConfig( + Uri baseAddress, + IAuthenticator? authenticator, + IJSONSerializer jsonSerializer, + IHTTPClient httpClient, + IRetryHandler? retryHandler, + IHTTPLogger? httpLogger, + IPaginator defaultPaginator, + IAPIConnector? apiConnector = null + ) + { + BaseAddress = baseAddress; + Authenticator = authenticator; + JSONSerializer = jsonSerializer; + HTTPClient = httpClient; + RetryHandler = retryHandler; + HTTPLogger = httpLogger; + DefaultPaginator = defaultPaginator; + APIConnector = apiConnector; + } + + public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer") + { + Ensure.ArgumentNotNull(token, nameof(token)); + + return new SpotifyClientConfig( + BaseAddress, + new TokenAuthenticator(token, tokenType), + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + DefaultPaginator + ); + } + + public SpotifyClientConfig WithRetryHandler(IRetryHandler retryHandler) + { + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + retryHandler, + HTTPLogger, + DefaultPaginator + ); + } + + public SpotifyClientConfig WithAuthenticator(IAuthenticator authenticator) + { + Ensure.ArgumentNotNull(authenticator, nameof(authenticator)); + + return new SpotifyClientConfig( + BaseAddress, + authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + DefaultPaginator + ); + } + + public SpotifyClientConfig WithHTTPLogger(IHTTPLogger httpLogger) + { + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + httpLogger, + DefaultPaginator + ); + } + + public SpotifyClientConfig WithHTTPClient(IHTTPClient httpClient) + { + Ensure.ArgumentNotNull(httpClient, nameof(httpClient)); + + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + httpClient, + RetryHandler, + HTTPLogger, + DefaultPaginator + ); + } + + public SpotifyClientConfig WithJSONSerializer(IJSONSerializer jsonSerializer) + { + Ensure.ArgumentNotNull(jsonSerializer, nameof(jsonSerializer)); + + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + jsonSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + DefaultPaginator + ); + } + + + public SpotifyClientConfig WithDefaultPaginator(IPaginator defaultPaginator) + { + Ensure.ArgumentNotNull(defaultPaginator, nameof(defaultPaginator)); + + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + defaultPaginator + ); + } + + public SpotifyClientConfig WithAPIConnector(IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + DefaultPaginator, + apiConnector + ); + } + + public IAPIConnector BuildAPIConnector() + { + return APIConnector ?? new APIConnector( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger + ); + } + + public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") + { + return CreateDefault().WithAuthenticator(new TokenAuthenticator(token, tokenType)); + } + + public static SpotifyClientConfig CreateDefault() + { + return new SpotifyClientConfig( + SpotifyUrls.APIV1, + null, + new NewtonsoftJSONSerializer(), + new NetHttpClient(), + null, + null, + new SimplePaginator() + ); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/TracksClient.cs b/Source Files/SpotifyAPI.Web/Clients/TracksClient.cs new file mode 100644 index 0000000..4e51511 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/TracksClient.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class TracksClient : APIClient, ITracksClient + { + public TracksClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Get(string trackId) + { + Ensure.ArgumentNotNullOrEmptyString(trackId, nameof(trackId)); + + return API.Get(URLs.Track(trackId)); + } + + public Task Get(string trackId, TrackRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(trackId, nameof(trackId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Track(trackId), request.BuildQueryParams()); + } + + public Task GetAudioAnalysis(string trackId) + { + Ensure.ArgumentNotNullOrEmptyString(trackId, nameof(trackId)); + + return API.Get(URLs.AudioAnalysis(trackId)); + } + + public Task GetAudioFeatures(string trackId) + { + Ensure.ArgumentNotNullOrEmptyString(trackId, nameof(trackId)); + + return API.Get(URLs.AudioFeatures(trackId)); + } + + public Task GetSeveral(TracksRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Tracks(), request.BuildQueryParams()); + } + + public Task GetSeveralAudioFeatures(TracksAudioFeaturesRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.AudioFeatures(), request.BuildQueryParams()); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Clients/UserProfileClient.cs b/Source Files/SpotifyAPI.Web/Clients/UserProfileClient.cs new file mode 100644 index 0000000..514776d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Clients/UserProfileClient.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class UserProfileClient : APIClient, IUserProfileClient + { + public UserProfileClient(IAPIConnector apiConnector) : base(apiConnector) { } + + public Task Current() + { + return API.Get(SpotifyUrls.Me()); + } + + public Task Get(string userId) + { + Ensure.ArgumentNotNullOrEmptyString(userId, nameof(userId)); + + return API.Get(SpotifyUrls.User(userId)); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Enums/AlbumType.cs b/Source Files/SpotifyAPI.Web/Enums/AlbumType.cs deleted file mode 100644 index c05df33..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/AlbumType.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum AlbumType - { - [String("album")] - Album = 1, - - [String("single")] - Single = 2, - - [String("compilation")] - Compilation = 4, - - [String("appears_on")] - AppearsOn = 8, - - [String("album,single,compilation,appears_on")] - All = 16 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Enums/FollowType.cs b/Source Files/SpotifyAPI.Web/Enums/FollowType.cs deleted file mode 100644 index 6cd802b..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/FollowType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum FollowType - { - [String("artist")] - Artist = 1, - - [String("user")] - User = 2 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Enums/RepeatState.cs b/Source Files/SpotifyAPI.Web/Enums/RepeatState.cs deleted file mode 100644 index d4abcf6..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/RepeatState.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum RepeatState - { - [String("track")] - Track = 1, - - [String("context")] - Context = 2, - - [String("off")] - Off = 4 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Enums/Scope.cs b/Source Files/SpotifyAPI.Web/Enums/Scope.cs deleted file mode 100644 index 5b500b9..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/Scope.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum Scope - { - [String("")] - None = 1, - - [String("playlist-modify-public")] - PlaylistModifyPublic = 2, - - [String("playlist-modify-private")] - PlaylistModifyPrivate = 4, - - [String("playlist-read-private")] - PlaylistReadPrivate = 8, - - [String("streaming")] - Streaming = 16, - - [String("user-read-private")] - UserReadPrivate = 32, - - [String("user-read-email")] - UserReadEmail = 64, - - [String("user-library-read")] - UserLibraryRead = 128, - - [String("user-library-modify")] - UserLibraryModify = 256, - - [String("user-follow-modify")] - UserFollowModify = 512, - - [String("user-follow-read")] - UserFollowRead = 1024, - - [String("user-read-birthdate")] - UserReadBirthdate = 2048, - - [String("user-top-read")] - UserTopRead = 4096, - - [String("playlist-read-collaborative")] - PlaylistReadCollaborative = 8192, - - [String("user-read-recently-played")] - UserReadRecentlyPlayed = 16384, - - [String("user-read-playback-state")] - UserReadPlaybackState = 32768, - - [String("user-modify-playback-state")] - UserModifyPlaybackState = 65536, - - [String("user-read-currently-playing")] - UserReadCurrentlyPlaying = 131072, - - [String("app-remote-control")] - AppRemoteControl = 262144, - - [String("ugc-image-upload")] - UgcImageUpload = 524288 - } -} diff --git a/Source Files/SpotifyAPI.Web/Enums/SearchType.cs b/Source Files/SpotifyAPI.Web/Enums/SearchType.cs deleted file mode 100644 index 652161c..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/SearchType.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum SearchType - { - [String("artist")] - Artist = 1, - - [String("album")] - Album = 2, - - [String("track")] - Track = 4, - - [String("playlist")] - Playlist = 8, - - [String("track,album,artist,playlist")] - All = 16 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Enums/TimeRangeType.cs b/Source Files/SpotifyAPI.Web/Enums/TimeRangeType.cs deleted file mode 100644 index 468a808..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/TimeRangeType.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - /// - /// Only one value allowed - /// - [Flags] - public enum TimeRangeType - { - [String("long_term")] - LongTerm = 1, - - [String("medium_term")] - MediumTerm = 2, - - [String("short_term")] - ShortTerm = 4 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Enums/TrackType.cs b/Source Files/SpotifyAPI.Web/Enums/TrackType.cs deleted file mode 100644 index 63c4341..0000000 --- a/Source Files/SpotifyAPI.Web/Enums/TrackType.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum TrackType - { - [String("track")] - Track = 1, - - [String("episode")] - Episode = 2, - - [String("ad")] - Ad = 4, - - [String("unknown")] - Unknown = 8 - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Exceptions/APIException.cs b/Source Files/SpotifyAPI.Web/Exceptions/APIException.cs new file mode 100644 index 0000000..b7c36e5 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Exceptions/APIException.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SpotifyAPI.Web.Http; +using System; +using System.Runtime.Serialization; + +namespace SpotifyAPI.Web +{ + [Serializable] + public class APIException : Exception + { + public IResponse? Response { get; set; } + + public APIException(IResponse response) : base(ParseAPIErrorMessage(response)) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + Response = response; + } + + public APIException() + { + } + + public APIException(string message) : base(message) + { + } + + public APIException(string message, Exception innerException) : base(message, innerException) + { + } + + protected APIException(SerializationInfo info, StreamingContext context) : base(info, context) + { + Response = info.GetValue("APIException.Response", typeof(IResponse)) as IResponse; + } + + private static string? ParseAPIErrorMessage(IResponse response) + { + var body = response.Body as string; + if (string.IsNullOrEmpty(body)) + { + return null; + } + try + { + JObject bodyObject = JObject.Parse(body!); + + + var error = bodyObject.Value("error"); + if (error.Type == JTokenType.String) + { + return error.ToString(); + } + else if (error.Type == JTokenType.Object) + { + return error.Value("message"); + } + } + catch (JsonReaderException) + { + return null; + } + return null; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("APIException.Response", Response); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Exceptions/APIPagingException.cs b/Source Files/SpotifyAPI.Web/Exceptions/APIPagingException.cs new file mode 100644 index 0000000..b5fd5c4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Exceptions/APIPagingException.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Runtime.Serialization; +using System; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + [Serializable] + public class APIPagingException : APIException + { + public TimeSpan RetryAfter { get; } + + public APIPagingException(IResponse response) : base(response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if (response.Headers.TryGetValue("Retry-After", out string? retryAfter)) + { + RetryAfter = TimeSpan.FromSeconds(int.Parse(retryAfter, CultureInfo.InvariantCulture)); + } + } + + public APIPagingException() { } + + public APIPagingException(string message) : base(message) { } + + public APIPagingException(string message, Exception innerException) : base(message, innerException) { } + + protected APIPagingException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/Source Files/SpotifyAPI.Web/Exceptions/APITooManyRequestsException.cs b/Source Files/SpotifyAPI.Web/Exceptions/APITooManyRequestsException.cs new file mode 100644 index 0000000..2ea206d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Exceptions/APITooManyRequestsException.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Runtime.Serialization; +using System; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + [Serializable] + public class APITooManyRequestsException : APIException + { + public TimeSpan RetryAfter { get; } + + public APITooManyRequestsException(IResponse response) : base(response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if (response.Headers.TryGetValue("Retry-After", out string? retryAfter)) + { + RetryAfter = TimeSpan.FromSeconds(int.Parse(retryAfter, CultureInfo.InvariantCulture)); + } + } + + public APITooManyRequestsException() { } + + public APITooManyRequestsException(string message) : base(message) { } + + public APITooManyRequestsException(string message, Exception innerException) : base(message, innerException) { } + + protected APITooManyRequestsException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/Source Files/SpotifyAPI.Web/Exceptions/APIUnauthorizedException.cs b/Source Files/SpotifyAPI.Web/Exceptions/APIUnauthorizedException.cs new file mode 100644 index 0000000..0bd0b40 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Exceptions/APIUnauthorizedException.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; +using System; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + [Serializable] + public class APIUnauthorizedException : APIException + { + public APIUnauthorizedException(IResponse response) : base(response) { } + + public APIUnauthorizedException() { } + + public APIUnauthorizedException(string message) : base(message) { } + + public APIUnauthorizedException(string message, Exception innerException) : base(message, innerException) { } + + protected APIUnauthorizedException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/APIConnector.cs b/Source Files/SpotifyAPI.Web/Http/APIConnector.cs new file mode 100644 index 0000000..2196181 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/APIConnector.cs @@ -0,0 +1,291 @@ +using System.Net.Http; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Net; + +namespace SpotifyAPI.Web.Http +{ + public class APIConnector : IAPIConnector + { + private readonly Uri _baseAddress; + private readonly IAuthenticator? _authenticator; + private readonly IJSONSerializer _jsonSerializer; + private readonly IHTTPClient _httpClient; + private readonly IRetryHandler? _retryHandler; + private readonly IHTTPLogger? _httpLogger; + + public event EventHandler? ResponseReceived; + + public APIConnector(Uri baseAddress, IAuthenticator authenticator) : + this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient(), null, null) + { } + public APIConnector( + Uri baseAddress, + IAuthenticator? authenticator, + IJSONSerializer jsonSerializer, + IHTTPClient httpClient, + IRetryHandler? retryHandler, + IHTTPLogger? httpLogger) + { + _baseAddress = baseAddress; + _authenticator = authenticator; + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + _retryHandler = retryHandler; + _httpLogger = httpLogger; + } + + public Task Delete(Uri uri) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Delete); + } + + public Task Delete(Uri uri, IDictionary? parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Delete, parameters); + } + + public Task Delete(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Delete, parameters, body); + } + + public async Task Delete(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await SendAPIRequestDetailed(uri, HttpMethod.Delete, parameters, body).ConfigureAwait(false); + return response.StatusCode; + } + + public Task Get(Uri uri) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Get); + } + + public Task Get(Uri uri, IDictionary? parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Get, parameters); + } + + public async Task Get(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await SendAPIRequestDetailed(uri, HttpMethod.Get, parameters, body).ConfigureAwait(false); + return response.StatusCode; + } + + public Task Post(Uri uri) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Post); + } + + public Task Post(Uri uri, IDictionary? parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Post, parameters); + } + + public Task Post(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Post, parameters, body); + } + + public Task Post(Uri uri, IDictionary? parameters, object? body, Dictionary? headers) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Post, parameters, body, headers); + } + + public async Task Post(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await SendAPIRequestDetailed(uri, HttpMethod.Post, parameters, body).ConfigureAwait(false); + return response.StatusCode; + } + + public Task Put(Uri uri) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Put); + } + + public Task Put(Uri uri, IDictionary? parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Put, parameters); + } + + public Task Put(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Put, parameters, body); + } + + public async Task Put(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await SendAPIRequestDetailed(uri, HttpMethod.Put, parameters, body).ConfigureAwait(false); + return response.StatusCode; + } + + public async Task PutRaw(Uri uri, IDictionary? parameters, object? body) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await SendRawRequest(uri, HttpMethod.Put, parameters, body).ConfigureAwait(false); + return response.StatusCode; + } + + public void SetRequestTimeout(TimeSpan timeout) + { + _httpClient.SetRequestTimeout(timeout); + } + + private IRequest CreateRequest( + Uri uri, + HttpMethod method, + IDictionary? parameters, + object? body, + IDictionary? headers + ) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + Ensure.ArgumentNotNull(method, nameof(method)); + + return new Request( + _baseAddress, + uri, + method, + headers ?? new Dictionary(), + parameters ?? new Dictionary()) + { + Body = body + }; + } + + private async Task> DoSerializedRequest(IRequest request) + { + _jsonSerializer.SerializeRequest(request); + var response = await DoRequest(request).ConfigureAwait(false); + return _jsonSerializer.DeserializeResponse(response); + } + + private async Task DoRequest(IRequest request) + { + await ApplyAuthenticator(request).ConfigureAwait(false); + _httpLogger?.OnRequest(request); + IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); + _httpLogger?.OnResponse(response); + ResponseReceived?.Invoke(this, response); + if (_retryHandler != null) + { + response = await _retryHandler.HandleRetry(request, response, async (newRequest) => + { + await ApplyAuthenticator(request).ConfigureAwait(false); + var newResponse = await _httpClient.DoRequest(request).ConfigureAwait(false); + _httpLogger?.OnResponse(newResponse); + ResponseReceived?.Invoke(this, response); + return newResponse; + }).ConfigureAwait(false); + } + ProcessErrors(response); + return response; + } + + private async Task ApplyAuthenticator(IRequest request) + { +#if NETSTANDARD2_0 + if (_authenticator != null + && !request.Endpoint.IsAbsoluteUri + || request.Endpoint.AbsoluteUri.Contains("https://api.spotify.com")) +#else + if (_authenticator != null + && !request.Endpoint.IsAbsoluteUri + || request.Endpoint.AbsoluteUri.Contains("https://api.spotify.com", StringComparison.InvariantCulture)) +#endif + { + await _authenticator!.Apply(request, this).ConfigureAwait(false); + } + } + + public Task SendRawRequest( + Uri uri, + HttpMethod method, + IDictionary? parameters = null, + object? body = null, + IDictionary? headers = null + ) + { + var request = CreateRequest(uri, method, parameters, body, headers); + return DoRequest(request); + } + + public async Task SendAPIRequest( + Uri uri, + HttpMethod method, + IDictionary? parameters = null, + object? body = null, + IDictionary? headers = null + ) + { + var request = CreateRequest(uri, method, parameters, body, headers); + IAPIResponse apiResponse = await DoSerializedRequest(request).ConfigureAwait(false); + return apiResponse.Body!; + } + + public async Task SendAPIRequestDetailed( + Uri uri, + HttpMethod method, + IDictionary? parameters = null, + object? body = null, + IDictionary? headers = null + ) + { + var request = CreateRequest(uri, method, parameters, body, headers); + var response = await DoSerializedRequest(request).ConfigureAwait(false); + return response.Response; + } + + private static void ProcessErrors(IResponse response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if ((int)response.StatusCode >= 200 && (int)response.StatusCode < 400) + { + return; + } + + throw response.StatusCode switch + { + HttpStatusCode.Unauthorized => new APIUnauthorizedException(response), + // TODO: Remove hack once .netstandard 2.0 is not supported + (HttpStatusCode)429 => new APITooManyRequestsException(response), + _ => new APIException(response), + }; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/APIResponse.cs b/Source Files/SpotifyAPI.Web/Http/APIResponse.cs new file mode 100644 index 0000000..6700a17 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/APIResponse.cs @@ -0,0 +1,17 @@ +namespace SpotifyAPI.Web.Http +{ + public class APIResponse : IAPIResponse + { + public APIResponse(IResponse response, T? body = default) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + Body = body; + Response = response; + } + + public T? Body { get; set; } + + public IResponse Response { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs new file mode 100644 index 0000000..9fd1294 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs @@ -0,0 +1,51 @@ +using System.Net; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + public interface IAPIConnector + { + // IAuthenticator Authenticator { get; } + + // IJSONSerializer JSONSerializer { get; } + + // IHTTPClient HTTPClient { get; } + + event EventHandler? ResponseReceived; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(Uri uri); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(Uri uri, IDictionary? parameters); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + Task Get(Uri uri, IDictionary? parameters, object? body); + + Task Post(Uri uri); + Task Post(Uri uri, IDictionary? parameters); + Task Post(Uri uri, IDictionary? parameters, object? body); + Task Post(Uri uri, IDictionary? parameters, object? body, Dictionary? headers); + Task Post(Uri uri, IDictionary? parameters, object? body); + + Task Put(Uri uri); + Task Put(Uri uri, IDictionary? parameters); + Task Put(Uri uri, IDictionary? parameters, object? body); + Task Put(Uri uri, IDictionary? parameters, object? body); + Task PutRaw(Uri uri, IDictionary? parameters, object? body); + + Task Delete(Uri uri); + Task Delete(Uri uri, IDictionary? parameters); + Task Delete(Uri uri, IDictionary? parameters, object? body); + Task Delete(Uri uri, IDictionary? parameters, object? body); + + Task SendAPIRequest( + Uri uri, HttpMethod method, + IDictionary? parameters = null, + object? body = null, + IDictionary? headers = null); + + void SetRequestTimeout(TimeSpan timeout); + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIResponse.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIResponse.cs new file mode 100644 index 0000000..aa2513b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IAPIResponse.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web.Http +{ + public interface IAPIResponse + { + T? Body { get; } + + IResponse Response { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IHTTPLogger.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IHTTPLogger.cs new file mode 100644 index 0000000..48525cc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IHTTPLogger.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web.Http +{ + public interface IHTTPLogger + { + void OnRequest(IRequest request); + void OnResponse(IResponse response); + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IHttpClient.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IHttpClient.cs new file mode 100644 index 0000000..78a74ef --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IHttpClient.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + public interface IHTTPClient : IDisposable + { + Task DoRequest(IRequest request); + void SetRequestTimeout(TimeSpan timeout); + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IJSONSerializer.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IJSONSerializer.cs new file mode 100644 index 0000000..6561b26 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IJSONSerializer.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + public interface IJSONSerializer + { + void SerializeRequest(IRequest request); + IAPIResponse DeserializeResponse(IResponse response); + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IProxyConfig.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IProxyConfig.cs new file mode 100644 index 0000000..7a6f281 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IProxyConfig.cs @@ -0,0 +1,15 @@ +namespace SpotifyAPI.Web +{ + public interface IProxyConfig + { + string Host { get; } + int Port { get; } + string? User { get; } + string? Password { get; } + bool SkipSSLCheck { get; } + /// + /// Whether to bypass the proxy server for local addresses. + /// + bool BypassProxyOnLocal { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IRequest.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IRequest.cs new file mode 100644 index 0000000..c5d9153 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace SpotifyAPI.Web.Http +{ + public interface IRequest + { + Uri BaseAddress { get; } + + Uri Endpoint { get; } + + IDictionary Headers { get; } + + IDictionary Parameters { get; } + + HttpMethod Method { get; } + + object? Body { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Interfaces/IResponse.cs b/Source Files/SpotifyAPI.Web/Http/Interfaces/IResponse.cs new file mode 100644 index 0000000..b4ace42 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Interfaces/IResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Net; + +namespace SpotifyAPI.Web.Http +{ + public interface IResponse + { + object? Body { get; } + + IReadOnlyDictionary Headers { get; } + + HttpStatusCode StatusCode { get; } + + string? ContentType { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/NetHttpClient.cs b/Source Files/SpotifyAPI.Web/Http/NetHttpClient.cs new file mode 100644 index 0000000..5a8a72c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/NetHttpClient.cs @@ -0,0 +1,141 @@ +using System.Net; +using System.Text; +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + public class NetHttpClient : IHTTPClient + { + private readonly HttpMessageHandler? _httpMessageHandler; + private readonly HttpClient _httpClient; + + public NetHttpClient() + { + _httpClient = new HttpClient(); + } + + public NetHttpClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public NetHttpClient(IProxyConfig proxyConfig) + { + Ensure.ArgumentNotNull(proxyConfig, nameof(proxyConfig)); + + _httpMessageHandler = CreateMessageHandler(proxyConfig); + _httpClient = new HttpClient(_httpMessageHandler); + } + + public async Task DoRequest(IRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + using HttpRequestMessage requestMsg = BuildRequestMessage(request); + var responseMsg = await _httpClient + .SendAsync(requestMsg, HttpCompletionOption.ResponseContentRead) + .ConfigureAwait(false); + + return await BuildResponse(responseMsg).ConfigureAwait(false); + } + + private static async Task BuildResponse(HttpResponseMessage responseMsg) + { + 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; + + return new Response(headers) + { + ContentType = contentType, + StatusCode = responseMsg.StatusCode, + Body = body + }; + } + + private static HttpRequestMessage BuildRequestMessage(IRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var fullUri = new Uri(request.BaseAddress, request.Endpoint).ApplyParameters(request.Parameters); + var requestMsg = new HttpRequestMessage(request.Method, fullUri); + foreach (var header in request.Headers) + { + requestMsg.Headers.Add(header.Key, header.Value); + } + + switch (request.Body) + { + case HttpContent body: + requestMsg.Content = body; + break; + case string body: + requestMsg.Content = new StringContent(body, Encoding.UTF8, "application/json"); + break; + case Stream body: + requestMsg.Content = new StreamContent(body); + break; + } + + return requestMsg; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _httpClient?.Dispose(); + _httpMessageHandler?.Dispose(); + } + } + + public void SetRequestTimeout(TimeSpan timeout) + { + _httpClient.Timeout = timeout; + } + + private static HttpMessageHandler CreateMessageHandler(IProxyConfig proxyConfig) + { + var proxy = new WebProxy + { + Address = new UriBuilder(proxyConfig.Host) { Port = proxyConfig.Port }.Uri, + UseDefaultCredentials = true, + BypassProxyOnLocal = proxyConfig.BypassProxyOnLocal + }; + + if (!string.IsNullOrEmpty(proxyConfig.User) || !string.IsNullOrEmpty(proxyConfig.Password)) + { + proxy.UseDefaultCredentials = false; + proxy.Credentials = new NetworkCredential(proxyConfig.User, proxyConfig.Password); + } + + var httpClientHandler = new HttpClientHandler + { + PreAuthenticate = proxy.UseDefaultCredentials, + UseDefaultCredentials = proxy.UseDefaultCredentials, + UseProxy = true, + Proxy = proxy, + }; + if (proxyConfig.SkipSSLCheck) + { + httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; + } + + return httpClientHandler; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs b/Source Files/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs new file mode 100644 index 0000000..2db938a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/NewtonsoftJSONSerializer.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Reflection; +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace SpotifyAPI.Web.Http +{ + public class NewtonsoftJSONSerializer : IJSONSerializer + { + private readonly JsonSerializerSettings _serializerSettings; + + public NewtonsoftJSONSerializer() + { + var contractResolver = new PrivateFieldDefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + _serializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = contractResolver + }; + } + + public IAPIResponse DeserializeResponse(IResponse response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if ( + ( + response.ContentType?.Equals("application/json", StringComparison.Ordinal) is true || response.ContentType == null + )) + { + var body = JsonConvert.DeserializeObject(response.Body as string ?? "", _serializerSettings); + return new APIResponse(response, body!); + } + return new APIResponse(response); + } + + public void SerializeRequest(IRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + if (request.Body is string || request.Body is Stream || request.Body is HttpContent || request.Body is null) + { + return; + } + + request.Body = JsonConvert.SerializeObject(request.Body, _serializerSettings); + } + + private class PrivateFieldDefaultContractResolver : DefaultContractResolver + { + protected override List GetSerializableMembers(Type objectType) + { + // Does not seem to work, still need DefaultMembersSearchFlags |= BindingFlags.NonPublic + var list = base.GetSerializableMembers(objectType); + list.AddRange(objectType.GetProperties(BindingFlags.NonPublic)); + return list; + } + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/ProxyConfig.cs b/Source Files/SpotifyAPI.Web/Http/ProxyConfig.cs new file mode 100644 index 0000000..0392c4d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/ProxyConfig.cs @@ -0,0 +1,20 @@ +namespace SpotifyAPI.Web +{ + public class ProxyConfig : IProxyConfig + { + public ProxyConfig(string host, int port) + { + Ensure.ArgumentNotNullOrEmptyString(host, nameof(host)); + + Host = host; + Port = port; + } + + public string Host { get; } + public int Port { get; } + public string? User { get; set; } + public string? Password { get; set; } + public bool BypassProxyOnLocal { get; set; } + public bool SkipSSLCheck { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Request.cs b/Source Files/SpotifyAPI.Web/Http/Request.cs new file mode 100644 index 0000000..42dd4cc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Request.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace SpotifyAPI.Web.Http +{ + public class Request : IRequest + { + public Request(Uri baseAddress, Uri endpoint, HttpMethod method) + { + Headers = new Dictionary(); + Parameters = new Dictionary(); + BaseAddress = baseAddress; + Endpoint = endpoint; + Method = method; + } + + public Request(Uri baseAddress, Uri endpoint, HttpMethod method, IDictionary headers) + { + Headers = headers; + Parameters = new Dictionary(); + BaseAddress = baseAddress; + Endpoint = endpoint; + Method = method; + } + + public Request( + Uri baseAddress, + Uri endpoint, + HttpMethod method, + IDictionary headers, + IDictionary parameters) + { + Headers = headers; + Parameters = parameters; + BaseAddress = baseAddress; + Endpoint = endpoint; + Method = method; + } + + public Uri BaseAddress { get; set; } + + public Uri Endpoint { get; set; } + + public IDictionary Headers { get; } + + public IDictionary Parameters { get; } + + public HttpMethod Method { get; set; } + + public object? Body { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/Response.cs b/Source Files/SpotifyAPI.Web/Http/Response.cs new file mode 100644 index 0000000..de16744 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/Response.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; + +namespace SpotifyAPI.Web.Http +{ + public class Response : IResponse + { + public Response(IDictionary headers) + { + Ensure.ArgumentNotNull(headers, nameof(headers)); + + Headers = new ReadOnlyDictionary(headers); + } + + public object? Body { get; set; } + + public IReadOnlyDictionary Headers { get; set; } + + public HttpStatusCode StatusCode { get; set; } + + public string? ContentType { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Http/SimpleConsoleHTTPLogger.cs b/Source Files/SpotifyAPI.Web/Http/SimpleConsoleHTTPLogger.cs new file mode 100644 index 0000000..6049e17 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Http/SimpleConsoleHTTPLogger.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System; + +namespace SpotifyAPI.Web.Http +{ + public class SimpleConsoleHTTPLogger : IHTTPLogger + { + private const string OnRequestFormat = "\n{0} {1} [{2}] {3}"; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303")] + public void OnRequest(IRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + string? parameters = null; + if (request.Parameters != null) + { + parameters = string.Join(",", + request.Parameters.Select(kv => kv.Key + "=" + kv.Value)?.ToArray() ?? Array.Empty() + ); + } + + Console.WriteLine(OnRequestFormat, request.Method, request.Endpoint, parameters, request.Body); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303")] + public void OnResponse(IResponse response) + { + Ensure.ArgumentNotNull(response, nameof(response)); +#if NETSTANDARD2_0 + string? body = response.Body?.ToString().Replace("\n", ""); +#else + string? body = response.Body?.ToString()?.Replace("\n", "", StringComparison.InvariantCulture); +#endif + + body = body?.Substring(0, Math.Min(50, body.Length)); + Console.WriteLine("--> {0} {1} {2}\n", response.StatusCode, response.ContentType, body); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/IClient.cs b/Source Files/SpotifyAPI.Web/IClient.cs deleted file mode 100644 index c0960a6..0000000 --- a/Source Files/SpotifyAPI.Web/IClient.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using SpotifyAPI.Web.Models; - -namespace SpotifyAPI.Web -{ - public interface IClient : IDisposable - { - JsonSerializerSettings JsonSettings { get; set; } - - /// - /// Downloads data from an URL and returns it - /// - /// An URL - /// - /// - Tuple Download(string url, Dictionary headers = null); - - /// - /// Downloads data async from an URL and returns it - /// - /// - /// - /// - Task> DownloadAsync(string url, Dictionary headers = null); - - /// - /// Downloads data from an URL and returns it - /// - /// An URL - /// - /// - Tuple DownloadRaw(string url, Dictionary headers = null); - - /// - /// Downloads data async from an URL and returns it - /// - /// - /// - /// - Task> DownloadRawAsync(string url, Dictionary headers = null); - - /// - /// Downloads data from an URL and converts it to an object - /// - /// The Type which the object gets converted to - /// An URL - /// - /// - Tuple DownloadJson(string url, Dictionary headers = null); - - /// - /// Downloads data async from an URL and converts it to an object - /// - /// The Type which the object gets converted to - /// An URL - /// - /// - Task> DownloadJsonAsync(string url, Dictionary headers = null); - - /// - /// Uploads data from an URL and returns the response - /// - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Tuple Upload(string url, string body, string method, Dictionary headers = null); - - /// - /// Uploads data async from an URL and returns the response - /// - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Task> UploadAsync(string url, string body, string method, Dictionary headers = null); - - /// - /// Uploads data from an URL and returns the response - /// - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Tuple UploadRaw(string url, string body, string method, Dictionary headers = null); - - /// - /// Uploads data async from an URL and returns the response - /// - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Task> UploadRawAsync(string url, string body, string method, Dictionary headers = null); - - /// - /// Uploads data from an URL and converts the response to an object - /// - /// The Type which the object gets converted to - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Tuple UploadJson(string url, string body, string method, Dictionary headers = null); - - /// - /// Uploads data async from an URL and converts the response to an object - /// - /// The Type which the object gets converted to - /// An URL - /// The Body-Data (most likely a JSON String) - /// The Upload-method (POST,DELETE,PUT) - /// - /// - Task> UploadJsonAsync(string url, string body, string method, Dictionary headers = null); - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/AnalysisMeta.cs b/Source Files/SpotifyAPI.Web/Models/AnalysisMeta.cs deleted file mode 100644 index bb9b777..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AnalysisMeta.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AnalysisMeta - { - [JsonProperty("analyzer_platform")] - public string AnalyzerVersion { get; set; } - - [JsonProperty("platform")] - public string Platform { get; set; } - - [JsonProperty("status_code")] - public int StatusCode { get; set; } - - [JsonProperty("detailed_status")] - public string DetailedStatus { get; set; } - - [JsonProperty("timestamp")] - public long Timestamp { get; set; } - - [JsonProperty("analysis_time")] - public double AnalysisTime { get; set; } - - [JsonProperty("input_process")] - public string InputProcess { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/AnalysisSection.cs b/Source Files/SpotifyAPI.Web/Models/AnalysisSection.cs deleted file mode 100644 index bcb92bc..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AnalysisSection.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AnalysisSection - { - [JsonProperty("start")] - public double Start { get; set; } - - [JsonProperty("duration")] - public double Duration { get; set; } - - [JsonProperty("confidence")] - public double Confidence { get; set; } - - [JsonProperty("loudness")] - public double Loudness { get; set; } - - [JsonProperty("tempo")] - public double Tempo { get; set; } - - [JsonProperty("tempo_confidence")] - public double TempoConfidence { get; set; } - - [JsonProperty("key")] - public int Key { get; set; } - - [JsonProperty("key_confidence")] - public double KeyConfidence { get; set; } - - [JsonProperty("mode")] - public int Mode { get; set; } - - [JsonProperty("mode_confidence")] - public double ModeConfidence { get; set; } - - [JsonProperty("time_signature")] - public int TimeSignature { get; set; } - - [JsonProperty("time_signature_confidence")] - public double TimeSignatureConfidence { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/AnalysisSegment.cs b/Source Files/SpotifyAPI.Web/Models/AnalysisSegment.cs deleted file mode 100644 index bdf5270..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AnalysisSegment.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class AnalysisSegment - { - [JsonProperty("start")] - public double Start { get; set; } - - [JsonProperty("duration")] - public double Duration { get; set; } - - [JsonProperty("confidence")] - public double Confidence { get; set; } - - [JsonProperty("loudness_start")] - public double LoudnessStart { get; set; } - - [JsonProperty("loudness_max_time")] - public double LoudnessMaxTime { get; set; } - - [JsonProperty("loudness_max")] - public double LoudnessMax { get; set; } - - [JsonProperty("loudness_end")] - public double LoudnessEnd { get; set; } - - [JsonProperty("pitches")] - public List Pitches { get; set; } - - [JsonProperty("timbre")] - public List Timbre { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/AnalysisTimeSlice.cs b/Source Files/SpotifyAPI.Web/Models/AnalysisTimeSlice.cs deleted file mode 100644 index 41ab5f1..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AnalysisTimeSlice.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AnalysisTimeSlice - { - [JsonProperty("start")] - public double Start { get; set; } - - [JsonProperty("duration")] - public double Duration { get; set; } - - [JsonProperty("confidence")] - public double Confidence { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/AnalysisTrack.cs b/Source Files/SpotifyAPI.Web/Models/AnalysisTrack.cs deleted file mode 100644 index e84bc62..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AnalysisTrack.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AnalysisTrack - { - [JsonProperty("num_samples")] - public int NumSamples { get; set; } - - [JsonProperty("duration")] - public double Duration { get; set; } - - [JsonProperty("sample_md5")] - public string SampleMD5 { get; set; } - - [JsonProperty("offset_seconds")] - public double OffsetSeconds { get; set; } - - [JsonProperty("window_seconds")] - public double WindowSeconds { get; set; } - - [JsonProperty("analysis_sample_rate")] - public int AnalysisSampleRate { get; set; } - - [JsonProperty("analysis_channels")] - public int AnalysisChannels { get; set; } - - [JsonProperty("end_of_fade_in")] - public double EndOfFadeIn { get; set; } - - [JsonProperty("start_of_fade_out")] - public double StartOfFadeOut { get; set; } - - [JsonProperty("loudness")] - public double Loudness { get; set; } - - [JsonProperty("tempo")] - public double Tempo { get; set; } - - [JsonProperty("tempo_confidence")] - public double TempoConfidence { get; set; } - - [JsonProperty("time_signature")] - public double TimeSignature { get; set; } - - [JsonProperty("time_signature_confidence")] - public double TimeSignatureConfidence { get; set; } - - [JsonProperty("key")] - public int Key { get; set; } - - [JsonProperty("key_confidence")] - public double KeyConfidence { get; set; } - - [JsonProperty("mode")] - public int Mode { get; set; } - - [JsonProperty("mode_confidence")] - public double ModeConfidence { get; set; } - - [JsonProperty("codestring")] - public string Codestring { get; set; } - - [JsonProperty("code_version")] - public double CodeVersion { get; set; } - - [JsonProperty("echoprintstring")] - public string Echoprintstring { get; set; } - - [JsonProperty("echoprint_version")] - public double EchoprintVersion { get; set; } - - [JsonProperty("synchstring")] - public string Synchstring { get; set; } - - [JsonProperty("synch_version")] - public double SynchVersion { get; set; } - - [JsonProperty("rhythmstring")] - public string Rhythmstring { get; set; } - - [JsonProperty("rhythm_version")] - public double RhythmVersion { get; set; } - - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/ArrayResponse.cs b/Source Files/SpotifyAPI.Web/Models/ArrayResponse.cs deleted file mode 100644 index eea3be6..0000000 --- a/Source Files/SpotifyAPI.Web/Models/ArrayResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class ListResponse : BasicModel - { - public List List { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/AudioAnalysis.cs b/Source Files/SpotifyAPI.Web/Models/AudioAnalysis.cs deleted file mode 100644 index bc8f622..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AudioAnalysis.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class AudioAnalysis : BasicModel - { - [JsonProperty("bars")] - public List Bars { get; set; } - - [JsonProperty("beats")] - public List Beats { get; set; } - - [JsonProperty("meta")] - public AnalysisMeta Meta { get; set; } - - [JsonProperty("sections")] - public List Sections { get; set; } - - [JsonProperty("segments")] - public List Segments { get; set; } - - [JsonProperty("tatums")] - public List Tatums { get; set; } - - [JsonProperty("track")] - public AnalysisTrack Track { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/AudioFeatures.cs b/Source Files/SpotifyAPI.Web/Models/AudioFeatures.cs deleted file mode 100644 index a3b029f..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AudioFeatures.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AudioFeatures : BasicModel - { - [JsonProperty("acousticness")] - public float Acousticness { get; set; } - - [JsonProperty("analysis_url")] - public string AnalysisUrl { get; set; } - - [JsonProperty("danceability")] - public float Danceability { get; set; } - - [JsonProperty("duration_ms")] - public int DurationMs { get; set; } - - [JsonProperty("energy")] - public float Energy { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("instrumentalness")] - public float Instrumentalness { get; set; } - - [JsonProperty("key")] - public int Key { get; set; } - - [JsonProperty("liveness")] - public float Liveness { get; set; } - - [JsonProperty("loudness")] - public float Loudness { get; set; } - - [JsonProperty("mode")] - public int Mode { get; set; } - - [JsonProperty("speechiness")] - public float Speechiness { get; set; } - - [JsonProperty("tempo")] - public float Tempo { get; set; } - - [JsonProperty("time_signature")] - public int TimeSignature { get; set; } - - [JsonProperty("track_href")] - public string TrackHref { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - - [JsonProperty("valence")] - public float Valence { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/AvailabeDevices.cs b/Source Files/SpotifyAPI.Web/Models/AvailabeDevices.cs deleted file mode 100644 index 6275ec0..0000000 --- a/Source Files/SpotifyAPI.Web/Models/AvailabeDevices.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class AvailabeDevices : BasicModel - { - [JsonProperty("devices")] - public List Devices { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/BasicModel.cs b/Source Files/SpotifyAPI.Web/Models/BasicModel.cs deleted file mode 100644 index d888dc2..0000000 --- a/Source Files/SpotifyAPI.Web/Models/BasicModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using System.Net; - -namespace SpotifyAPI.Web.Models -{ - public abstract class BasicModel - { - [JsonProperty("error")] - public Error Error { get; set; } - - private ResponseInfo _info; - - public bool HasError() => Error != null; - - internal void AddResponseInfo(ResponseInfo info) => _info = info; - - public string Header(string key) => _info.Headers?.Get(key); - - public WebHeaderCollection Headers() => _info.Headers; - - public HttpStatusCode StatusCode() => _info.StatusCode; - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Category.cs b/Source Files/SpotifyAPI.Web/Models/Category.cs deleted file mode 100644 index 03be7a0..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Category.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class Category : BasicModel - { - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("icons")] - public List Icons { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/CategoryList.cs b/Source Files/SpotifyAPI.Web/Models/CategoryList.cs deleted file mode 100644 index c46d5cb..0000000 --- a/Source Files/SpotifyAPI.Web/Models/CategoryList.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class CategoryList : BasicModel - { - [JsonProperty("categories")] - public Paging Categories { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/CategoryPlaylist.cs b/Source Files/SpotifyAPI.Web/Models/CategoryPlaylist.cs deleted file mode 100644 index 02875d5..0000000 --- a/Source Files/SpotifyAPI.Web/Models/CategoryPlaylist.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class CategoryPlaylist : BasicModel - { - [JsonProperty("playlists")] - public Paging Playlists { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Converters/PlayableItemConverter.cs b/Source Files/SpotifyAPI.Web/Models/Converters/PlayableItemConverter.cs new file mode 100644 index 0000000..bb5d5ef --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Converters/PlayableItemConverter.cs @@ -0,0 +1,51 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace SpotifyAPI.Web +{ + public class PlayableItemConverter : JsonConverter + { + public override bool CanWrite { get => false; } + + public override bool CanConvert(Type objectType) => true; + + public override object? ReadJson(JsonReader reader, Type objectType, + object? existingValue, JsonSerializer serializer) + { + Ensure.ArgumentNotNull(serializer, nameof(serializer)); + + var token = JToken.ReadFrom(reader); + if (token.Type == JTokenType.Null) + { + return null; + } + + var type = token["type"]?.Value(); + if (type == "track") + { + var obj = new FullTrack(); + serializer.Populate(token.CreateReader(), obj); + return obj; + } + else if (type == "episode") + { + var obj = new FullEpisode(); + serializer.Populate(token.CreateReader(), obj); + return obj; + } + else + { + throw new APIException($@"Received unkown playlist element type: {type}. +If you're requesting a subset of available fields via the fields query paramter, +make sure to include at least the type field. Often it's `items(track(type))` or `item(type)`"); + } + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/CursorPaging.cs b/Source Files/SpotifyAPI.Web/Models/CursorPaging.cs deleted file mode 100644 index 3496212..0000000 --- a/Source Files/SpotifyAPI.Web/Models/CursorPaging.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class CursorPaging : BasicModel - { - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("items")] - public List Items { get; set; } - - [JsonProperty("limit")] - public int Limit { get; set; } - - [JsonProperty("next")] - public string Next { get; set; } - - [JsonProperty("cursors")] - public Cursor Cursors { get; set; } - - [JsonProperty("total")] - public int Total { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Device.cs b/Source Files/SpotifyAPI.Web/Models/Device.cs deleted file mode 100644 index 3a0d79c..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Device.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class Device - { - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("is_active")] - public bool IsActive { get; set; } - - [JsonProperty("is_restricted")] - public bool IsRestricted { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("volume_percent")] - public int VolumePercent { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/FeaturedPlaylists.cs b/Source Files/SpotifyAPI.Web/Models/FeaturedPlaylists.cs deleted file mode 100644 index ab1dc09..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FeaturedPlaylists.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class FeaturedPlaylists : BasicModel - { - [JsonProperty("message")] - public string Message { get; set; } - - [JsonProperty("playlists")] - public Paging Playlists { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/FollowedArtists.cs b/Source Files/SpotifyAPI.Web/Models/FollowedArtists.cs deleted file mode 100644 index 163ce87..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FollowedArtists.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class FollowedArtists : BasicModel - { - [JsonProperty("artists")] - public CursorPaging Artists { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/FullAlbum.cs b/Source Files/SpotifyAPI.Web/Models/FullAlbum.cs deleted file mode 100644 index b22e4f2..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FullAlbum.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class FullAlbum : BasicModel - { - [JsonProperty("album_type")] - public string AlbumType { get; set; } - - [JsonProperty("artists")] - public List Artists { get; set; } - - [JsonProperty("available_markets")] - public List AvailableMarkets { get; set; } - - [JsonProperty("copyrights")] - public List Copyrights { get; set; } - - [JsonProperty("external_ids")] - public Dictionary ExternalIds { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("genres")] - public List Genres { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("label")] - public string Label { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("popularity")] - public int Popularity { get; set; } - - [JsonProperty("release_date")] - public string ReleaseDate { get; set; } - - [JsonProperty("release_date_precision")] - public string ReleaseDatePrecision { get; set; } - - [JsonProperty("tracks")] - public Paging Tracks { get; set; } - - [JsonProperty("restrictions")] - public Dictionary Restrictions { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} diff --git a/Source Files/SpotifyAPI.Web/Models/FullArtist.cs b/Source Files/SpotifyAPI.Web/Models/FullArtist.cs deleted file mode 100644 index 1b38a74..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FullArtist.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class FullArtist : BasicModel - { - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("followers")] - public Followers Followers { get; set; } - - [JsonProperty("genres")] - public List Genres { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("popularity")] - public int Popularity { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/FullPlaylist.cs b/Source Files/SpotifyAPI.Web/Models/FullPlaylist.cs deleted file mode 100644 index aec32d0..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FullPlaylist.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class FullPlaylist : BasicModel - { - [JsonProperty("collaborative")] - public bool Collaborative { get; set; } - - [JsonProperty("description")] - public string Description { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("followers")] - public Followers Followers { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("owner")] - public PublicProfile Owner { get; set; } - - [JsonProperty("public")] - public bool Public { get; set; } - - [JsonProperty("snapshot_id")] - public string SnapshotId { get; set; } - - [JsonProperty("tracks")] - public Paging Tracks { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/FullTrack.cs b/Source Files/SpotifyAPI.Web/Models/FullTrack.cs deleted file mode 100644 index bab52fe..0000000 --- a/Source Files/SpotifyAPI.Web/Models/FullTrack.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class FullTrack : BasicModel - { - [JsonProperty("album")] - public SimpleAlbum Album { get; set; } - - [JsonProperty("artists")] - public List Artists { get; set; } - - [JsonProperty("available_markets")] - public List AvailableMarkets { get; set; } - - [JsonProperty("disc_number")] - public int DiscNumber { get; set; } - - [JsonProperty("duration_ms")] - public int DurationMs { get; set; } - - [JsonProperty("explicit")] - public bool Explicit { get; set; } - - [JsonProperty("external_ids")] - public Dictionary ExternalIds { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("popularity")] - public int Popularity { get; set; } - - [JsonProperty("preview_url")] - public string PreviewUrl { get; set; } - - [JsonProperty("track_number")] - public int TrackNumber { get; set; } - - [JsonProperty("restrictions")] - public Dictionary Restrictions { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - - /// - /// Only filled when the "market"-parameter was supplied! - /// - [JsonProperty("is_playable")] - public bool? IsPlayable { get; set; } - - /// - /// Only filled when the "market"-parameter was supplied! - /// - [JsonProperty("linked_from")] - public LinkedFrom LinkedFrom { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/GeneralModels.cs b/Source Files/SpotifyAPI.Web/Models/GeneralModels.cs deleted file mode 100644 index 8dffd92..0000000 --- a/Source Files/SpotifyAPI.Web/Models/GeneralModels.cs +++ /dev/null @@ -1,159 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SpotifyAPI.Web.Models -{ - public class Image - { - [JsonProperty("url")] - public string Url { get; set; } - - [JsonProperty("width")] - public int Width { get; set; } - - [JsonProperty("height")] - public int Height { get; set; } - } - - public class ErrorResponse : BasicModel - { - } - - public class Error - { - [JsonProperty("status")] - public int Status { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } - } - - public class PlaylistTrackCollection - { - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("total")] - public int Total { get; set; } - } - - public class Followers - { - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("total")] - public int Total { get; set; } - } - - public class PlaylistTrack - { - [JsonProperty("added_at")] - public DateTime AddedAt { get; set; } - - [JsonProperty("added_by")] - public PublicProfile AddedBy { get; set; } - - [JsonProperty("track")] - public FullTrack Track { get; set; } - - [JsonProperty("is_local")] - public bool IsLocal { get; set; } - } - - public class DeleteTrackUri - { - /// - /// Delete-Track Wrapper - /// - /// An Spotify-URI - /// Optional positions - public DeleteTrackUri(string uri, params int[] positions) - { - Positions = positions.ToList(); - Uri = uri; - } - - [JsonProperty("uri")] - public string Uri { get; set; } - - [JsonProperty("positions")] - public List Positions { get; set; } - - public bool ShouldSerializePositions() - { - return Positions.Count > 0; - } - } - - public class Copyright - { - [JsonProperty("text")] - public string Text { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - } - - public class LinkedFrom - { - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } - - public class SavedTrack - { - [JsonProperty("added_at")] - public DateTime AddedAt { get; set; } - - [JsonProperty("track")] - public FullTrack Track { get; set; } - } - - public class SavedAlbum - { - [JsonProperty("added_at")] - public DateTime AddedAt { get; set; } - - [JsonProperty("album")] - public FullAlbum Album { get; set; } - } - - public class Cursor - { - [JsonProperty("after")] - public string After { get; set; } - - [JsonProperty("before")] - public string Before { get; set; } - } - - public class Context - { - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/IPaginatable.cs b/Source Files/SpotifyAPI.Web/Models/IPaginatable.cs new file mode 100644 index 0000000..682c5ac --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/IPaginatable.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public interface IPaginatable + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + string? Next { get; set; } + + List? Items { get; set; } + } + + public interface IPaginatable + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716")] + string? Next { get; set; } + + List? Items { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/NewAlbumReleases.cs b/Source Files/SpotifyAPI.Web/Models/NewAlbumReleases.cs deleted file mode 100644 index c85139e..0000000 --- a/Source Files/SpotifyAPI.Web/Models/NewAlbumReleases.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class NewAlbumReleases : BasicModel - { - [JsonProperty("albums")] - public Paging Albums { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Paging.cs b/Source Files/SpotifyAPI.Web/Models/Paging.cs deleted file mode 100644 index 03e5305..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Paging.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class Paging : BasicModel - { - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("items")] - public List Items { get; set; } - - [JsonProperty("limit")] - public int Limit { get; set; } - - [JsonProperty("next")] - public string Next { get; set; } - - [JsonProperty("offset")] - public int Offset { get; set; } - - [JsonProperty("previous")] - public string Previous { get; set; } - - [JsonProperty("total")] - public int Total { get; set; } - - public bool HasNextPage() - { - return Next != null; - } - - public bool HasPreviousPage() - { - return Next != null; - } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/PlayHistory.cs b/Source Files/SpotifyAPI.Web/Models/PlayHistory.cs deleted file mode 100644 index ae1c28e..0000000 --- a/Source Files/SpotifyAPI.Web/Models/PlayHistory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class PlayHistory : BasicModel - { - [JsonProperty("track")] - public SimpleTrack Track { get; set; } - - [JsonProperty("played_at")] - public DateTime PlayedAt { get; set; } - - [JsonProperty("context")] - public Context Context { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/PlaybackContext.cs b/Source Files/SpotifyAPI.Web/Models/PlaybackContext.cs deleted file mode 100644 index 37d6d4a..0000000 --- a/Source Files/SpotifyAPI.Web/Models/PlaybackContext.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using SpotifyAPI.Web.Enums; - -namespace SpotifyAPI.Web.Models -{ - public class PlaybackContext : BasicModel - { - [JsonProperty("device")] - public Device Device { get; set; } - - [JsonProperty("repeat_state")] - [JsonConverter(typeof(StringEnumConverter))] - public RepeatState RepeatState { get; set; } - - [JsonProperty("shuffle_state")] - public bool ShuffleState { get; set; } - - [JsonProperty("context")] - public Context Context { get; set; } - - [JsonProperty("timestamp")] - public long Timestamp { get; set; } - - [JsonProperty("progress_ms")] - public int ProgressMs { get; set; } - - [JsonProperty("is_playing")] - public bool IsPlaying { get; set; } - - [JsonProperty("item")] - public FullTrack Item { get; set; } - - [JsonProperty("currently_playing_type")] - [JsonConverter(typeof(StringEnumConverter))] - public TrackType CurrentlyPlayingType { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/PrivateProfile.cs b/Source Files/SpotifyAPI.Web/Models/PrivateProfile.cs deleted file mode 100644 index 243d6b9..0000000 --- a/Source Files/SpotifyAPI.Web/Models/PrivateProfile.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class PrivateProfile : BasicModel - { - [JsonProperty("birthdate")] - public string Birthdate { get; set; } - - [JsonProperty("country")] - public string Country { get; set; } - - [JsonProperty("display_name")] - public string DisplayName { get; set; } - - [JsonProperty("email")] - public string Email { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("followers")] - public Followers Followers { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("product")] - public string Product { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/PublicProfile.cs b/Source Files/SpotifyAPI.Web/Models/PublicProfile.cs deleted file mode 100644 index a28b8ea..0000000 --- a/Source Files/SpotifyAPI.Web/Models/PublicProfile.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class PublicProfile : BasicModel - { - [JsonProperty("display_name")] - public string DisplayName { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("followers")] - public Followers Followers { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/RecommendationSeed .cs b/Source Files/SpotifyAPI.Web/Models/RecommendationSeed .cs deleted file mode 100644 index b0cd94a..0000000 --- a/Source Files/SpotifyAPI.Web/Models/RecommendationSeed .cs +++ /dev/null @@ -1,25 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class RecommendationSeed - { - [JsonProperty("afterFilteringSize")] - public int AfterFilteringSize { get; set; } - - [JsonProperty("afterRelinkingSize")] - public int AfterRelinkingSize { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("initialPoolSize")] - public int InitialPoolSize { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/RecommendationSeedGenres.cs b/Source Files/SpotifyAPI.Web/Models/RecommendationSeedGenres.cs deleted file mode 100644 index 663c8bf..0000000 --- a/Source Files/SpotifyAPI.Web/Models/RecommendationSeedGenres.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class RecommendationSeedGenres : BasicModel - { - [JsonProperty("genres")] - public List Genres { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Recommendations.cs b/Source Files/SpotifyAPI.Web/Models/Recommendations.cs deleted file mode 100644 index 921522e..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Recommendations.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class Recommendations : BasicModel - { - [JsonProperty("seeds")] - public List Seeds { get; set; } - - [JsonProperty("tracks")] - public List Tracks { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Request/AlbumRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/AlbumRequest.cs new file mode 100644 index 0000000..db30953 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/AlbumRequest.cs @@ -0,0 +1,12 @@ +namespace SpotifyAPI.Web +{ + public class AlbumRequest : RequestParams + { + /// + /// The market you’d like to request. Synonym for country. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Request/AlbumTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/AlbumTracksRequest.cs new file mode 100644 index 0000000..00a165e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/AlbumTracksRequest.cs @@ -0,0 +1,29 @@ +namespace SpotifyAPI.Web +{ + public class AlbumTracksRequest : RequestParams + { + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// The maximum number of tracks to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first track to return. Default: 0 (the first object). + /// Use with limit to get the next set of tracks. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/AlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/AlbumsRequest.cs new file mode 100644 index 0000000..e858893 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/AlbumsRequest.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class AlbumsRequest : RequestParams + { + /// + /// AlbumsRequest + /// + /// A comma-separated list of the Spotify IDs for the albums. Maximum: 20 IDs. + public AlbumsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the albums. Maximum: 20 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ArtistsAlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsAlbumsRequest.cs new file mode 100644 index 0000000..b159427 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsAlbumsRequest.cs @@ -0,0 +1,58 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class ArtistsAlbumsRequest : RequestParams + { + /// + /// A comma-separated list of keywords that will be used to filter the response. + /// If not supplied, all album types will be returned. + /// + /// + [QueryParam("include_groups")] + public IncludeGroups? IncludeGroupsParam { get; set; } + + /// + /// Synonym for country. An ISO 3166-1 alpha-2 country code or the string from_token. + /// Supply this parameter to limit the response to one particular geographical market. + /// For example, for albums available in Sweden: market=SE. + /// If not given, results will be returned for all markets and you are likely to get duplicate results per album, + /// one for each market in which the album is available! + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// The number of album objects to return. Default: 20. Minimum: 1. Maximum: 50. For example: limit=2 + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first album to return. Default: 0 (i.e., the first album). Use with limit to get the next set of albums. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + [Flags] + public enum IncludeGroups + { + [String("album")] + Album = 1, + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720")] + [String("single")] + Single = 2, + + [String("appears_on")] + AppearsOn = 4, + + [String("compilation")] + Compilation = 8, + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ArtistsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsRequest.cs new file mode 100644 index 0000000..a5305e6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsRequest.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ArtistsRequest : RequestParams + { + /// + /// ArtistsRequest + /// + /// A comma-separated list of the Spotify IDs for the artists. Maximum: 50 IDs. + public ArtistsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the artists. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ArtistsTopTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsTopTracksRequest.cs new file mode 100644 index 0000000..26cecbc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ArtistsTopTracksRequest.cs @@ -0,0 +1,24 @@ +namespace SpotifyAPI.Web +{ + public class ArtistsTopTracksRequest : RequestParams + { + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. Synonym for country. + /// + /// + public ArtistsTopTracksRequest(string market) + { + Ensure.ArgumentNotNullOrEmptyString(market, nameof(market)); + + Market = market; + } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. Synonym for country. + /// + /// + [QueryParam("market")] + public string Market { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeRefreshRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeRefreshRequest.cs new file mode 100644 index 0000000..ce3debb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeRefreshRequest.cs @@ -0,0 +1,44 @@ +namespace SpotifyAPI.Web +{ + /// + /// Used when requesting a refreshed token from spotify oauth services (Authorization Code Auth) + /// + public class AuthorizationCodeRefreshRequest + { + /// + /// + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard) + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard) + /// The refresh token received from an earlier authorization code grant + public AuthorizationCodeRefreshRequest(string clientId, string clientSecret, string refreshToken) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret)); + Ensure.ArgumentNotNullOrEmptyString(refreshToken, nameof(refreshToken)); + + ClientId = clientId; + ClientSecret = clientSecret; + RefreshToken = refreshToken; + } + + /// + /// The refresh token received from an earlier authorization code grant + /// + /// + public string RefreshToken { get; } + + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard) + /// + /// + public string ClientId { get; } + + /// + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard) + /// + /// + public string ClientSecret { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeTokenRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeTokenRequest.cs new file mode 100644 index 0000000..10cf0d3 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/AuthorizationCodeTokenRequest.cs @@ -0,0 +1,54 @@ +using System; +namespace SpotifyAPI.Web +{ + /// + /// Used when requesting a token from spotify oauth services (Authorization Code Auth) + /// + public class AuthorizationCodeTokenRequest + { + /// + /// + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard). + /// The code received from the spotify response. + /// The redirectUri which was used to initiate the authentication. + public AuthorizationCodeTokenRequest(string clientId, string clientSecret, string code, Uri redirectUri) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret)); + Ensure.ArgumentNotNullOrEmptyString(code, nameof(code)); + Ensure.ArgumentNotNull(redirectUri, nameof(redirectUri)); + + ClientId = clientId; + ClientSecret = clientSecret; + Code = code; + RedirectUri = redirectUri; + } + + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// + /// + public string ClientId { get; } + + /// + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard). + /// + /// + public string ClientSecret { get; } + + /// + /// The code received from the spotify response. + /// + /// + public string Code { get; } + + /// + /// The redirectUri which was used to initiate the authentication. + /// + /// + public Uri RedirectUri { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/CategoriesRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/CategoriesRequest.cs new file mode 100644 index 0000000..5b0a5b8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/CategoriesRequest.cs @@ -0,0 +1,42 @@ +namespace SpotifyAPI.Web +{ + public class CategoriesRequest : RequestParams + { + /// + /// A country: an ISO 3166-1 alpha-2 country code. + /// Provide this parameter if you want to narrow the list of returned categories to those relevant to a particular country. + /// If omitted, the returned items will be globally relevant. + /// + /// + [QueryParam("country")] + public string? Country { get; set; } + + /// + /// The desired language, consisting of an ISO 639-1 language code and an ISO 3166-1 alpha-2 country code, + /// joined by an underscore. For example: es_MX, meaning “Spanish (Mexico)”. Provide this parameter if + /// you want the category metadata returned in a particular language. + /// Note that, if locale is not supplied, or if the specified language is not available, all strings will + /// be returned in the Spotify default language (American English). + /// The locale parameter, combined with the country parameter, may give odd results if not carefully matched. + /// For example country=SE&locale=de_DE will return a list of categories relevant to Sweden but as German language strings. + /// + /// + [QueryParam("locale")] + public string? Locale { get; set; } + + /// + /// The maximum number of categories to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first item to return. Default: 0 (the first object). Use with limit to get the next set of categories. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/CategoryPlaylistsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/CategoryPlaylistsRequest.cs new file mode 100644 index 0000000..84e6b18 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/CategoryPlaylistsRequest.cs @@ -0,0 +1,28 @@ +namespace SpotifyAPI.Web +{ + public class CategoriesPlaylistsRequest : RequestParams + { + /// + /// A country: an ISO 3166-1 alpha-2 country code. + /// Provide this parameter to ensure that the category exists for a particular country. + /// + /// + [QueryParam("country")] + public string? Country { get; set; } + + /// + /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first item to return. Default: 0 (the first object). Use with limit to get the next set of items. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/CategoryRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/CategoryRequest.cs new file mode 100644 index 0000000..9cf14c4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/CategoryRequest.cs @@ -0,0 +1,25 @@ +namespace SpotifyAPI.Web +{ + public class CategoryRequest : RequestParams + { + /// + /// A country: an ISO 3166-1 alpha-2 country code. + /// Provide this parameter to ensure that the category exists for a particular country. + /// + /// + [QueryParam("country")] + public string? Country { get; set; } + + /// + /// The desired language, consisting of an ISO 639-1 language code and an ISO 3166-1 alpha-2 country code, + /// joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". + /// Provide this parameter if you want the category strings returned in a particular language. + /// Note that, if locale is not supplied, or if the specified language is not available, + /// the category strings returned will be in the Spotify default language (American English). + /// + /// + [QueryParam("locale")] + public string? Locale { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ClientCredentialsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ClientCredentialsRequest.cs new file mode 100644 index 0000000..bb4bedd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ClientCredentialsRequest.cs @@ -0,0 +1,35 @@ +namespace SpotifyAPI.Web +{ + /// + /// Used when requesting a token from spotify oauth services (Client Credentials Auth) + /// + public class ClientCredentialsRequest + { + /// + /// + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard) + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard) + public ClientCredentialsRequest(string clientId, string clientSecret) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret)); + + ClientId = clientId; + ClientSecret = clientSecret; + } + + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard) + /// + /// + public string ClientId { get; } + + /// + /// The Client Secret of your Spotify Application (See Spotify Dev Dashboard) + /// + /// + public string ClientSecret { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/EpisodeRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/EpisodeRequest.cs new file mode 100644 index 0000000..2a45657 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/EpisodeRequest.cs @@ -0,0 +1,18 @@ +namespace SpotifyAPI.Web +{ + public class EpisodeRequest : RequestParams + { + /// + /// An ISO 3166-1 alpha-2 country code. If a country code is specified, + /// only shows and episodes that are available in that market will be returned. + /// If a valid user access token is specified in the request header, the country + /// associated with the user account will take priority over this parameter. + /// Note: If neither market or user country are provided, the content is considered unavailable for the client. + /// Users can view the country that is associated with their account in the account settings. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/EpisodesRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/EpisodesRequest.cs new file mode 100644 index 0000000..01b0c69 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/EpisodesRequest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class EpisodesRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. + public EpisodesRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + + /// + /// An ISO 3166-1 alpha-2 country code. If a country code is specified, only shows and episodes + /// that are available in that market will be returned.If a valid user access token is specified + /// in the request header, the country associated with the user account will take priority over + /// this parameter.Note: If neither market or user country are provided, the content is considered + /// unavailable for the client.Users can view the country that is associated with their account + /// in the account settings. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FeaturedPlaylistsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FeaturedPlaylistsRequest.cs new file mode 100644 index 0000000..6aeab6d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FeaturedPlaylistsRequest.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using System; +namespace SpotifyAPI.Web +{ + public class FeaturedPlaylistsRequest : RequestParams + { + /// + /// A country: an ISO 3166-1 alpha-2 country code. + /// Provide this parameter if you want the list of returned items to be relevant to a particular country. + /// If omitted, the returned items will be relevant to all countries. + /// + /// + [QueryParam("country")] + public string? Country { get; set; } + + /// + /// The desired language, consisting of a lowercase ISO 639-1 language code and an uppercase ISO 3166-1 + /// alpha-2 country code, joined by an underscore. For example: es_MX, meaning “Spanish (Mexico)”. Provide + /// this parameter if you want the results returned in a particular language (where available). Note that, + /// if locale is not supplied, or if the specified language is not available, all strings will be returned + /// in the Spotify default language (American English). The locale parameter, combined with the country + /// parameter, may give odd results if not carefully matched. + /// For example country=SE&locale=de_DE will return a list of categories relevant to Sweden but as German language strings. + /// + /// + [QueryParam("locale")] + public string? Locale { get; set; } + + /// + /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first item to return. Default: 0 (the first object). Use with limit to get the next set of items. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user’s + /// local time to get results tailored for that specific date and time in the day. If not provided, + /// the response defaults to the current UTC time. Example: “2014-10-23T09:00:00” for a user whose local + /// time is 9AM. If there were no featured playlists (or there is no data) at the specified time, + /// the response will revert to the current UTC time. + /// + /// + public DateTime? Timestamp { get; set; } + + [QueryParam("timestamp")] + protected string? TimestampFormatted + { + get => Timestamp?.ToString("o", CultureInfo.InvariantCulture); + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckCurrentUserRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckCurrentUserRequest.cs new file mode 100644 index 0000000..e9515fd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckCurrentUserRequest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FollowCheckCurrentUserRequest : RequestParams + { + /// + /// + /// + /// The ID type: either artist or user. + /// + /// A list of the artist or the user Spotify IDs to check. + /// For example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + public FollowCheckCurrentUserRequest(Type type, IList ids) + { + Ensure.ArgumentNotNull(type, nameof(type)); + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + TypeParam = type; + Ids = ids; + } + + /// + /// The ID type: either artist or user. + /// + /// + [QueryParam("type")] + public Type TypeParam { get; } + + /// + /// A list of the artist or the user Spotify IDs to check. + /// For example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + + public enum Type + { + [String("artist")] + Artist, + [String("user")] + User + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckPlaylistRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckPlaylistRequest.cs new file mode 100644 index 0000000..24d76db --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FollowCheckPlaylistRequest.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FollowCheckPlaylistRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of Spotify User IDs ; + /// the ids of the users that you want to check to see if they follow the playlist. + /// Maximum: 5 ids. + /// + public FollowCheckPlaylistRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of Spotify User IDs ; + /// the ids of the users that you want to check to see if they follow the playlist. + /// Maximum: 5 ids. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FollowGetCurrentUserRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FollowGetCurrentUserRequest.cs new file mode 100644 index 0000000..f58be57 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FollowGetCurrentUserRequest.cs @@ -0,0 +1,42 @@ +namespace SpotifyAPI.Web +{ + public class FollowOfCurrentUserRequest : RequestParams + { + /// + /// The ID type: currently only artist is supported. + /// + /// + public FollowOfCurrentUserRequest(Type type = Type.Artist) + { + TypeParam = type; + } + + /// + /// The ID type: currently only artist is supported. + /// + /// + [QueryParam("type")] + public Type TypeParam { get; set; } + + /// + /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The last artist ID retrieved from the previous request. + /// + /// + [QueryParam("after")] + public string? After { get; set; } + + public enum Type + { + [String("artist")] + Artist + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FollowPlaylistRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FollowPlaylistRequest.cs new file mode 100644 index 0000000..714a3fc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FollowPlaylistRequest.cs @@ -0,0 +1,15 @@ +namespace SpotifyAPI.Web +{ + public class FollowPlaylistRequest : RequestParams + { + /// + /// Defaults to true. If true the playlist will be included in user’s public playlists, + /// if false it will remain private. To be able to follow playlists privately, + /// the user must have granted the playlist-modify-private scope. + /// + /// + [BodyParam("public")] + public bool? Public { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/FollowRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/FollowRequest.cs new file mode 100644 index 0000000..96b932b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/FollowRequest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FollowRequest : RequestParams + { + /// + /// + /// + /// The ID type: either artist or user. + /// + /// A comma-separated list of the artist or the user Spotify IDs. + /// For example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + public FollowRequest(Type type, IList ids) + { + Ensure.ArgumentNotNull(type, nameof(type)); + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + TypeParam = type; + Ids = ids; + } + + /// + /// The ID type: either artist or user. + /// + /// + [QueryParam("type")] + public Type? TypeParam { get; set; } + + /// + /// A comma-separated list of the artist or the user Spotify IDs. + /// For example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + /// + [BodyParam("ids")] + public IList Ids { get; } + + public enum Type + { + [String("artist")] + Artist, + [String("user")] + User + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryAlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryAlbumsRequest.cs new file mode 100644 index 0000000..0b867f1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryAlbumsRequest.cs @@ -0,0 +1,29 @@ +namespace SpotifyAPI.Web +{ + public class LibraryAlbumsRequest : RequestParams + { + /// + /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first object to return. Default: 0 (i.e., the first object). + /// Use with limit to get the next set of objects. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. Provide this parameter + /// if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckAlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckAlbumsRequest.cs new file mode 100644 index 0000000..9ab3e54 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckAlbumsRequest.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryCheckAlbumsRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the albums. Maximum: 50 IDs. + /// + public LibraryCheckAlbumsRequest(IList ids) + { + Ensure.ArgumentNotNull(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the albums. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckShowsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckShowsRequest.cs new file mode 100644 index 0000000..dbe830c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckShowsRequest.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryCheckShowsRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the shows. Maximum: 50 ids. + public LibraryCheckShowsRequest(IList ids) + { + Ensure.ArgumentNotNull(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the shows. Maximum: 50 ids. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckTracksRequest.cs new file mode 100644 index 0000000..2be35b4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryCheckTracksRequest.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryCheckTracksRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 50 IDs. + /// + public LibraryCheckTracksRequest(IList ids) + { + Ensure.ArgumentNotNull(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveAlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveAlbumsRequest.cs new file mode 100644 index 0000000..4221cdd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveAlbumsRequest.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryRemoveAlbumsRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + public LibraryRemoveAlbumsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + /// + [BodyParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveShowsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveShowsRequest.cs new file mode 100644 index 0000000..05a462a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveShowsRequest.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryRemoveShowsRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. + /// + public LibraryRemoveShowsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. + /// + /// + [BodyParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveTracksRequest.cs new file mode 100644 index 0000000..e21d190 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryRemoveTracksRequest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibraryRemoveTracksRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of the Spotify IDs. For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. + /// Maximum: 50 IDs. + /// + public LibraryRemoveTracksRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs. For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. + /// Maximum: 50 IDs. + /// + /// + [BodyParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveAlbumsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveAlbumsRequest.cs new file mode 100644 index 0000000..c7394b4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveAlbumsRequest.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibrarySaveAlbumsRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + public LibrarySaveAlbumsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveShowsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveShowsRequest.cs new file mode 100644 index 0000000..6dd617b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveShowsRequest.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibrarySaveShowsRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of Spotify IDs for the shows to be added to the user’s library. + public LibrarySaveShowsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of Spotify IDs for the shows to be added to the user’s library. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveTracksRequest.cs new file mode 100644 index 0000000..1991de1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibrarySaveTracksRequest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class LibrarySaveTracksRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + public LibrarySaveTracksRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs. + /// For example: ids=4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryShowsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryShowsRequest.cs new file mode 100644 index 0000000..0105a5b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryShowsRequest.cs @@ -0,0 +1,21 @@ +namespace SpotifyAPI.Web +{ + public class LibraryShowsRequest : RequestParams + { + /// + /// The maximum number of shows to return. Default: 20. Minimum: 1. Maximum: 50 + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first show to return. Default: 0 (the first object). + /// Use with limit to get the next set of shows. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LibraryTracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LibraryTracksRequest.cs new file mode 100644 index 0000000..3256b05 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LibraryTracksRequest.cs @@ -0,0 +1,29 @@ +namespace SpotifyAPI.Web +{ + public class LibraryTracksRequest : RequestParams + { + /// + /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first object to return. + /// Default: 0 (i.e., the first object). Use with limit to get the next set of objects. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/LoginRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/LoginRequest.cs new file mode 100644 index 0000000..40a9fa9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/LoginRequest.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; + +namespace SpotifyAPI.Web +{ + public class LoginRequest + { + public LoginRequest(Uri redirectUri, string clientId, ResponseType responseType) + { + Ensure.ArgumentNotNull(redirectUri, nameof(redirectUri)); + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + + RedirectUri = redirectUri; + ClientId = clientId; + ResponseTypeParam = responseType; + } + + public Uri RedirectUri { get; } + public ResponseType ResponseTypeParam { get; } + public string ClientId { get; } + public string? State { get; set; } + public ICollection? Scope { get; set; } + public bool? ShowDialog { get; set; } + public string? CodeChallengeMethod { get; set; } + public string? CodeChallenge { get; set; } + + public Uri ToUri() + { + var builder = new StringBuilder(SpotifyUrls.Authorize.ToString()); + builder.Append($"?client_id={ClientId}"); + builder.Append($"&response_type={ResponseTypeParam.ToString().ToLower(CultureInfo.InvariantCulture)}"); + builder.Append($"&redirect_uri={HttpUtility.UrlEncode(RedirectUri.ToString())}"); + if (!string.IsNullOrEmpty(State)) + { + builder.Append($"&state={HttpUtility.UrlEncode(State)}"); + } + if (Scope != null) + { + builder.Append($"&scope={HttpUtility.UrlEncode(string.Join(" ", Scope))}"); + } + if (ShowDialog != null) + { + builder.Append($"&show_dialog={ShowDialog.Value}"); + } + if (CodeChallenge != null) + { + builder.Append($"&code_challenge={CodeChallenge}"); + } + if (CodeChallengeMethod != null) + { + builder.Append($"&code_challenge_method={CodeChallengeMethod}"); + } + + return new Uri(builder.ToString()); + } + + public enum ResponseType + { + Code, + Token + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/NewReleasesRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/NewReleasesRequest.cs new file mode 100644 index 0000000..0123bbd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/NewReleasesRequest.cs @@ -0,0 +1,30 @@ +namespace SpotifyAPI.Web +{ + public class NewReleasesRequest : RequestParams + { + /// + /// A country: an ISO 3166-1 alpha-2 country code. + /// Provide this parameter if you want the list of returned items to be relevant to a particular country. + /// If omitted, the returned items will be relevant to all countries. + /// + /// + [QueryParam("country")] + public string? Country { get; set; } + + /// + /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first item to return. Default: 0 (the first object). + /// Use with limit to get the next set of items. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRefreshRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRefreshRequest.cs new file mode 100644 index 0000000..5cfe754 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRefreshRequest.cs @@ -0,0 +1,33 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class PKCETokenRefreshRequest + { + /// + /// Request model for refreshing a access token via PKCE Token + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// The received refresh token. Expires after one refresh + public PKCETokenRefreshRequest(string clientId, string refreshToken) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(refreshToken, nameof(refreshToken)); + + ClientId = clientId; + RefreshToken = refreshToken; + } + + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// + /// + public string ClientId { get; } + + /// + /// The received refresh token. + /// + /// + public string RefreshToken { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRequest.cs new file mode 100644 index 0000000..00a0a57 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PKCETokenRequest.cs @@ -0,0 +1,55 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class PKCETokenRequest + { + /// + /// + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// The code received from the spotify response. + /// The redirectUri which was used to initiate the authentication. + /// + /// The value of this parameter must match the value of the code_verifier + /// that your app generated in step 1. + /// + public PKCETokenRequest(string clientId, string code, Uri redirectUri, string codeVerifier) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(code, nameof(code)); + Ensure.ArgumentNotNullOrEmptyString(codeVerifier, nameof(codeVerifier)); + Ensure.ArgumentNotNull(redirectUri, nameof(redirectUri)); + + ClientId = clientId; + CodeVerifier = codeVerifier; + Code = code; + RedirectUri = redirectUri; + } + + /// + /// The Client ID of your Spotify Application (See Spotify Dev Dashboard). + /// + /// + public string ClientId { get; } + + /// + /// The value of this parameter must match the value of the code_verifier + /// that your app generated in step 1. + /// + /// + public string CodeVerifier { get; } + + /// + /// The code received from the spotify response. + /// + /// + public string Code { get; } + + /// + /// The redirectUri which was used to initiate the authentication. + /// + /// + public Uri RedirectUri { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PersonalizationTopRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PersonalizationTopRequest.cs new file mode 100644 index 0000000..d4e1f26 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PersonalizationTopRequest.cs @@ -0,0 +1,41 @@ +namespace SpotifyAPI.Web +{ + public class PersonalizationTopRequest : RequestParams + { + /// + /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50. For example: limit=2 + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// Over what time frame the affinities are computed. Valid values: long_term + /// (calculated from several years of data and including all new data as it becomes available), + /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). Default: medium_term + /// + /// + [QueryParam("time_range")] + public TimeRange? TimeRangeParam { get; set; } + + public enum TimeRange + { + [String("long_term")] + LongTerm, + + [String("medium_term")] + MediumTerm, + + [String("short_term")] + ShortTerm + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerAddToQueueRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerAddToQueueRequest.cs new file mode 100644 index 0000000..c75d074 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerAddToQueueRequest.cs @@ -0,0 +1,32 @@ +namespace SpotifyAPI.Web +{ + public class PlayerAddToQueueRequest : RequestParams + { + /// + /// + /// + /// The uri of the item to add to the queue. Must be a track or an episode uri. + public PlayerAddToQueueRequest(string uri) + { + Ensure.ArgumentNotNullOrEmptyString(uri, nameof(uri)); + + Uri = uri; + } + + /// + /// The uri of the item to add to the queue. Must be a track or an episode uri. + /// + /// + [QueryParam("uri")] + public string Uri { get; } + + /// + /// The id of the device this command is targeting. + /// If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentPlaybackRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentPlaybackRequest.cs new file mode 100644 index 0000000..c593627 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentPlaybackRequest.cs @@ -0,0 +1,46 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class PlayerCurrentPlaybackRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of item types that your client supports besides the default track type. + /// Valid types are: track and episode. An unsupported type in the response is expected to be represented + /// as null value in the item field. Note: This parameter was introduced to allow existing clients to + /// maintain their current behaviour and might be deprecated in the future. In addition to providing + /// this parameter, make sure that your client properly handles cases of new types in the future by + /// checking against the currently_playing_type field. + /// + public PlayerCurrentPlaybackRequest(AdditionalTypes types = AdditionalTypes.All) + { + Ensure.ArgumentNotNull(types, nameof(types)); + + AdditionalTypesParam = types; + } + + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// This is set to `"track", "episode"` by default. + /// + /// + [QueryParam("additional_types")] + public AdditionalTypes AdditionalTypesParam { get; } + + [Flags] + public enum AdditionalTypes + { + [String("track")] + Track = 1, + [String("episode")] + Episode = 2, + All = Track | Episode + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentlyPlayingRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentlyPlayingRequest.cs new file mode 100644 index 0000000..022d3fa --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerCurrentlyPlayingRequest.cs @@ -0,0 +1,55 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class PlayerCurrentlyPlayingRequest : RequestParams + { + + /// + /// A comma-separated list of item types that your client supports besides the default track type. + /// Valid types are: track and episode. An unsupported type in the response is expected to be represented + /// as null value in the item field. Note: This parameter was introduced to allow existing clients to + /// maintain their current behaviour and might be deprecated in the future. In addition to providing + /// this parameter, make sure that your client properly handles cases of new types in the future by + /// checking against the currently_playing_type field. Defaults to AdditionalTypes.All + /// + /// + public PlayerCurrentlyPlayingRequest(AdditionalTypes types = AdditionalTypes.All) + { + Ensure.ArgumentNotNull(types, nameof(types)); + + AdditionalTypesParam = types; + } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// A comma-separated list of item types that your client supports besides the default track type. + /// Valid types are: track and episode. An unsupported type in the response is expected to be represented + /// as null value in the item field. Note: This parameter was introduced to allow existing clients to + /// maintain their current behaviour and might be deprecated in the future. In addition to providing + /// this parameter, make sure that your client properly handles cases of new types in the future by + /// checking against the currently_playing_type field. Defaults to AdditionalTypes.All + /// + /// + [QueryParam("additional_types")] + public AdditionalTypes AdditionalTypesParam { get; } + + [Flags] + public enum AdditionalTypes + { + [String("track")] + Track = 1, + [String("episode")] + Episode = 2, + All = Track | Episode + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerPausePlaybackRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerPausePlaybackRequest.cs new file mode 100644 index 0000000..1af4e44 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerPausePlaybackRequest.cs @@ -0,0 +1,13 @@ +namespace SpotifyAPI.Web +{ + public class PlayerPausePlaybackRequest : RequestParams + { + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerRecentlyPlayedRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerRecentlyPlayedRequest.cs new file mode 100644 index 0000000..8fc5586 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerRecentlyPlayedRequest.cs @@ -0,0 +1,29 @@ +namespace SpotifyAPI.Web +{ + public class PlayerRecentlyPlayedRequest : RequestParams + { + /// + /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position. + /// If after is specified, before must not be specified. + /// + /// + [QueryParam("after")] + public long? After { get; set; } + + /// + /// A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position. + /// If before is specified, after must not be specified. + /// + /// + [QueryParam("before")] + public long? Before { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerResumePlaybackRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerResumePlaybackRequest.cs new file mode 100644 index 0000000..ea617ad --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerResumePlaybackRequest.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PlayerResumePlaybackRequest : RequestParams + { + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [BodyParam("context_uri")] + public string? ContextUri { get; set; } + + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227")] + [BodyParam("uris")] + public IList? Uris { get; set; } + + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [BodyParam("offset")] + public Offset? OffsetParam { get; set; } + + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [BodyParam("position_ms")] + public int? PositionMs { get; set; } + + public class Offset + { + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] + public string? Uri { get; set; } + + /// + /// Undocumented by Spotify Beta Docs + /// + /// + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerSeekToRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSeekToRequest.cs new file mode 100644 index 0000000..1951c12 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSeekToRequest.cs @@ -0,0 +1,35 @@ +namespace SpotifyAPI.Web +{ + public class PlayerSeekToRequest : RequestParams + { + /// + /// + /// + /// + /// The position in milliseconds to seek to. Must be a positive number. + /// Passing in a position that is greater than the length of the track will + /// cause the player to start playing the next song. + /// + public PlayerSeekToRequest(long positionMs) + { + PositonMs = positionMs; + } + + /// + /// The position in milliseconds to seek to. Must be a positive number. + /// Passing in a position that is greater than the length of the track will cause + /// the player to start playing the next song. + /// + /// + [QueryParam("position_ms")] + public long PositonMs { get; } + + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerSetRepeatRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSetRepeatRequest.cs new file mode 100644 index 0000000..7ab2834 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSetRepeatRequest.cs @@ -0,0 +1,45 @@ +namespace SpotifyAPI.Web +{ + public class PlayerSetRepeatRequest : RequestParams + { + /// + /// + /// track, context or off. track will repeat the current track. context will repeat the current context. + /// off will turn repeat off. + /// + public PlayerSetRepeatRequest(State state) + { + Ensure.ArgumentNotNull(state, nameof(state)); + + StateParam = state; + } + + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + + /// + /// track, context or off. track will repeat the current track. context will repeat the current context. + /// off will turn repeat off. + /// + /// + [QueryParam("state")] + public State StateParam { get; } + + public enum State + { + [String("track")] + Track, + + [String("context")] + Context, + + [String("off")] + Off + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerShuffleRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerShuffleRequest.cs new file mode 100644 index 0000000..4c333f8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerShuffleRequest.cs @@ -0,0 +1,30 @@ +namespace SpotifyAPI.Web +{ + public class PlayerShuffleRequest : RequestParams + { + /// + /// + /// + /// true : Shuffle user’s playback false : Do not shuffle user’s playback. + public PlayerShuffleRequest(bool state) + { + State = state; + } + + /// + /// true : Shuffle user’s playback false : Do not shuffle user’s playback. + /// + /// + [QueryParam("state")] + public bool State { get; } + + /// + /// The id of the device this command is targeting. If not supplied, + /// the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipNextRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipNextRequest.cs new file mode 100644 index 0000000..5a33fcd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipNextRequest.cs @@ -0,0 +1,13 @@ +namespace SpotifyAPI.Web +{ + public class PlayerSkipNextRequest : RequestParams + { + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipPreviousRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipPreviousRequest.cs new file mode 100644 index 0000000..6e6cacb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerSkipPreviousRequest.cs @@ -0,0 +1,14 @@ +namespace SpotifyAPI.Web +{ + public class PlayerSkipPreviousRequest : RequestParams + { + /// + /// The id of the device this command is targeting. + /// If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerTransferPlaybackRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerTransferPlaybackRequest.cs new file mode 100644 index 0000000..d0ba154 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerTransferPlaybackRequest.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class PlayerTransferPlaybackRequest : RequestParams + { + /// + /// + /// + /// + /// A JSON array containing the ID of the device on which playback should be started/transferred. + /// For example:{device_ids:["74ASZWbe4lXaubB36ztrGX"]} + /// Note: Although an array is accepted, only a single device_id is currently supported. + /// Supplying more than one will return 400 Bad Request + /// + public PlayerTransferPlaybackRequest(IList deviceIds) + { + Ensure.ArgumentNotNullOrEmptyList(deviceIds, nameof(deviceIds)); + + DeviceIds = deviceIds; + } + + /// + /// A JSON array containing the ID of the device on which playback should be started/transferred. + /// For example:{device_ids:["74ASZWbe4lXaubB36ztrGX"]} + /// Note: Although an array is accepted, only a single device_id is currently supported. + /// Supplying more than one will return 400 Bad Request + /// + /// + [BodyParam("device_ids")] + public IList DeviceIds { get; } + + /// + /// true: ensure playback happens on new device. false or not provided: keep the current playback state. + /// + /// + [BodyParam("play")] + public bool? Play { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlayerVolumeRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlayerVolumeRequest.cs new file mode 100644 index 0000000..bbd5a1c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlayerVolumeRequest.cs @@ -0,0 +1,29 @@ +namespace SpotifyAPI.Web +{ + public class PlayerVolumeRequest : RequestParams + { + /// + /// + /// + /// The volume to set. Must be a value from 0 to 100 inclusive. + public PlayerVolumeRequest(int volumePercent) + { + VolumePercent = volumePercent; + } + + /// + /// The volume to set. Must be a value from 0 to 100 inclusive. + /// + /// + [QueryParam("volume_percent")] + public int VolumePercent { get; } + + /// + /// The id of the device this command is targeting. If not supplied, the user’s currently active device is the target. + /// + /// + [QueryParam("device_id")] + public string? DeviceId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistAddItemsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistAddItemsRequest.cs new file mode 100644 index 0000000..ba6bca7 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistAddItemsRequest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class PlaylistAddItemsRequest : RequestParams + { + /// + /// + /// + /// + /// A JSON array of the Spotify URIs to add. + /// For example: {"uris": ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", + /// "spotify:track:1301WleyT98MSxVHPZCA6M", "spotify:episode:512ojhOuo1ktJprKbVcKyQ"]} + /// A maximum of 100 items can be added in one request. + /// + public PlaylistAddItemsRequest(IList uris) + { + Ensure.ArgumentNotNull(uris, nameof(uris)); + + Uris = uris; + } + + /// + /// A JSON array of the Spotify URIs to add. + /// For example: {"uris": ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", + /// "spotify:track:1301WleyT98MSxVHPZCA6M", "spotify:episode:512ojhOuo1ktJprKbVcKyQ"]} + /// A maximum of 100 items can be added in one request. + /// + /// + [BodyParam("uris")] + public IList Uris { get; } + + /// + /// The position to insert the items, a zero-based index. + /// For example, to insert the items in the first position: position=0 ; + /// to insert the items in the third position: position=2. If omitted, the items will be appended to the playlist. + /// Items are added in the order they appear in the uris array. + /// + /// + [BodyParam("position")] + public int? Position { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistChangeDetailsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistChangeDetailsRequest.cs new file mode 100644 index 0000000..f4acc01 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistChangeDetailsRequest.cs @@ -0,0 +1,35 @@ +namespace SpotifyAPI.Web +{ + public class PlaylistChangeDetailsRequest : RequestParams + { + /// + /// The new name for the playlist, for example "My New Playlist Title" + /// + /// + [BodyParam("name")] + public string? Name { get; set; } + + /// + /// If true the playlist will be public, if false it will be private. + /// + /// + [BodyParam("public")] + public bool? Public { get; set; } + + /// + /// If true , the playlist will become collaborative and other users will be able to modify the + /// playlist in their Spotify client. Note: You can only set collaborative to true on non-public playlists. + /// + /// + [BodyParam("collaborative")] + public bool? Collaborative { get; set; } + + /// + /// Value for playlist description as displayed in Spotify Clients and in the Web API. + /// + /// + [BodyParam("description")] + public string? Description { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCreateRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCreateRequest.cs new file mode 100644 index 0000000..c4f879f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCreateRequest.cs @@ -0,0 +1,52 @@ +namespace SpotifyAPI.Web +{ + public class PlaylistCreateRequest : RequestParams + { + /// + /// + /// + /// The name for the new playlist, for example "Your Coolest Playlist" . + /// This name does not need to be unique; a user may have several playlists with the same name. + /// + public PlaylistCreateRequest(string name) + { + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + + Name = name; + } + + /// + /// The name for the new playlist, for example "Your Coolest Playlist" . + /// This name does not need to be unique; a user may have several playlists with the same name. + /// + /// + [BodyParam("name")] + public string Name { get; } + + /// + /// Defaults to true . If true the playlist will be public, if false it will be private. + /// To be able to create private playlists, the user must have granted the playlist-modify-private scope + /// + /// + [BodyParam("public")] + public bool? Public { get; set; } + + /// + /// Defaults to false . If true the playlist will be collaborative. + /// Note that to create a collaborative playlist you must also set public to false . + /// To create collaborative playlists you must have + /// granted playlist-modify-private and playlist-modify-public scopes . + /// + /// + [BodyParam("collaborative")] + public bool? Collaborative { get; set; } + + /// + /// value for playlist description as displayed in Spotify Clients and in the Web API. + /// + /// + [BodyParam("description")] + public string? Description { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCurrentUsersRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCurrentUsersRequest.cs new file mode 100644 index 0000000..da41e5c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistCurrentUsersRequest.cs @@ -0,0 +1,21 @@ +namespace SpotifyAPI.Web +{ + public class PlaylistCurrentUsersRequest : RequestParams + { + /// + /// The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first playlist to return. + /// Default: 0 (the first object). Maximum offset: 100.000. Use with limit to get the next set of playlists. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetItemsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetItemsRequest.cs new file mode 100644 index 0000000..a175e03 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetItemsRequest.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class PlaylistGetItemsRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of item types that your client supports + /// besides the default track type. Valid types are: track and episode. + /// Note: This parameter was introduced to allow existing clients to maintain + /// their current behaviour and might be deprecated in the future. In addition to + /// providing this parameter, make sure that your client properly handles cases of new types in the + /// future by checking against the type field of each object. Defaults to ALL + /// + public PlaylistGetItemsRequest(AdditionalTypes types = AdditionalTypes.All) + { + Ensure.ArgumentNotNull(types, nameof(types)); + + AdditionalTypesParam = types; + Fields = new List(); + } + + /// + /// Filters for the query: a comma-separated list of the fields to return. + /// If omitted, all fields are returned. For example, to get just the total number of items and the request limit: + /// fields=total,limit + /// A dot separator can be used to specify non-reoccurring fields, while parentheses can be used to specify + /// reoccurring fields within objects. For example, to get just the added date and user ID of the adder: + /// fields=items(added_at,added_by.id) + /// Use multiple parentheses to drill down into nested objects, for example: + /// fields=items(track(name,href,album(name,href))) + /// Fields can be excluded by prefixing them with an exclamation mark, for example: + /// fields=items.track.album(!external_urls,images) + /// + /// + [QueryParam("fields")] + public IList Fields { get; } + + /// + /// The maximum number of items to return. Default: 100. Minimum: 1. Maximum: 100. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first item to return. Default: 0 (the first object). + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. For episodes, if a valid user access token is + /// specified in the request header, the country associated with the user account will take priority over this + /// parameter. Note: If neither market or user country are provided, + /// the episode is considered unavailable for the client. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// This is set to `"track", "episode"` by default. + /// + /// + [QueryParam("additional_types")] + public AdditionalTypes AdditionalTypesParam { get; } + + [Flags] + public enum AdditionalTypes + { + [String("track")] + Track = 1, + [String("episode")] + Episode = 2, + All = Track | Episode + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetRequest.cs new file mode 100644 index 0000000..558ed9d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetRequest.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class PlaylistGetRequest : RequestParams + { + public PlaylistGetRequest(AdditionalTypes types = AdditionalTypes.All) + { + Ensure.ArgumentNotNull(types, nameof(types)); + + AdditionalTypesParam = types; + Fields = new List(); + } + + /// + /// Filters for the query: a comma-separated list of the fields to return. + /// If omitted, all fields are returned. For example, to get just the playlist’s description and URI: fields=description,uri. + /// A dot separator can be used to specify non-reoccurring fields, + /// while parentheses can be used to specify reoccurring fields within objects. + /// For example, to get just the added date and user ID of the adder: + /// fields=tracks.items(added_at,added_by.id). Use multiple parentheses to drill down into nested objects, for example: + /// fields=tracks.items(track(name,href,album(name,href))). + /// Fields can be excluded by prefixing them with an exclamation mark, for example: + /// fields=tracks.items(track(name,href,album(!name,href))) + /// + /// + [QueryParam("fields")] + public IList Fields { get; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// For episodes, if a valid user access token is specified in the request header, + /// the country associated with the user account will take priority over this parameter. + /// Note: If neither market or user country are provided, the episode is considered unavailable for the client. + /// + /// + [QueryParam("market")] + public string? Market { get; } + + /// + /// This is set to `"track", "episode"` by default. + /// + /// + [QueryParam("additional_types")] + public AdditionalTypes AdditionalTypesParam { get; } + + [Flags] + public enum AdditionalTypes + { + [String("track")] + Track = 1, + [String("episode")] + Episode = 2, + All = Track | Episode + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetUsersRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetUsersRequest.cs new file mode 100644 index 0000000..e553d53 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistGetUsersRequest.cs @@ -0,0 +1,21 @@ +namespace SpotifyAPI.Web +{ + public class PlaylistGetUsersRequest : RequestParams + { + /// + /// The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first playlist to return. Default: 0 (the first object). + /// Maximum offset: 100.000. Use with limit to get the next set of playlists. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs new file mode 100644 index 0000000..595f280 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PlaylistRemoveItemsRequest : RequestParams + { + /// + /// An array of objects containing Spotify URIs of the tracks or episodes to remove. + /// For example: { "tracks": [{ "uri": "spotify:track:4iV5W9uYEdYUVa79Axb7Rh" }, + /// { "uri": "spotify:track:1301WleyT98MSxVHPZCA6M" }] }. + /// A maximum of 100 objects can be sent at once. + /// + /// + [BodyParam("tracks")] + public IList? Tracks { get; set; } + + /// + /// An array of positions to delete. This also supports local tracks. + /// SnapshotId MUST be supplied when using this parameter + /// + /// + [BodyParam("positions")] + public IList? Positions { get; set; } + + /// + /// The playlist’s snapshot ID against which you want to make the changes. + /// The API will validate that the specified items exist and in the specified positions and make the changes, + /// even if more recent changes have been made to the playlist. + /// + /// + [BodyParam("snapshot_id")] + public string? SnapshotId { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034")] + public class Item + { + [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] + public string? Uri { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227")] + [JsonProperty("positions", NullValueHandling = NullValueHandling.Ignore)] + public List? Positions { get; set; } + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReorderItemsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReorderItemsRequest.cs new file mode 100644 index 0000000..c0c5ebc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReorderItemsRequest.cs @@ -0,0 +1,63 @@ +namespace SpotifyAPI.Web +{ + public class PlaylistReorderItemsRequest : RequestParams + { + /// + /// + /// + /// + /// The position of the first item to be reordered. + /// + /// + /// The position where the items should be inserted. + /// To reorder the items to the end of the playlist, + /// simply set insert_before to the position after the last item. + /// Examples: To reorder the first item to the last position in a playlist with 10 items, + /// set range_start to 0, and insert_before to 10. To reorder the last item in a playlist + /// with 10 items to the start of the playlist, set range_start to 9, and insert_before to 0. + /// + public PlaylistReorderItemsRequest(int rangeStart, int insertBefore) + { + RangeStart = rangeStart; + InsertBefore = insertBefore; + } + + /// + /// The position of the first item to be reordered. + /// + /// + [BodyParam("range_start")] + public int RangeStart { get; set; } + + /// + /// The position where the items should be inserted. + /// To reorder the items to the end of the playlist, + /// simply set insert_before to the position after the last item. + /// Examples: To reorder the first item to the last position in a playlist with 10 items, + /// set range_start to 0, and insert_before to 10. To reorder the last item in a playlist + /// with 10 items to the start of the playlist, set range_start to 9, and insert_before to 0. + /// + /// + [BodyParam("insert_before")] + public int InsertBefore { get; set; } + + /// + /// The amount of items to be reordered. Defaults to 1 if not set. + /// The range of items to be reordered begins from the range_start position, and + /// includes the range_length subsequent items. + /// Example: To move the items at index 9-10 to the start of the playlist, + /// range_start is set to 9, and range_length is set to 2. + /// + /// + [BodyParam("range_length")] + public int? RangeLength { get; set; } + + /// + /// The playlist’s snapshot ID against which you want to make the changes. + /// + /// + [BodyParam("snapshot_id")] + public string? SnapshotId { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReplaceItemsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReplaceItemsRequest.cs new file mode 100644 index 0000000..45458b9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/PlaylistReplaceItemsRequest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class PlaylistReplaceItemsRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of Spotify URIs to set, can be track or episode URIs. + /// A maximum of 100 items can be set in one request. + /// + public PlaylistReplaceItemsRequest(List uris) + { + Ensure.ArgumentNotNull(uris, nameof(uris)); + + Uris = uris; + } + + /// + /// A comma-separated list of Spotify URIs to set, can be track or episode URIs. + /// A maximum of 100 items can be set in one request. + /// + /// + [BodyParam("uris")] + public IList Uris { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs new file mode 100644 index 0000000..c463e2f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class RecommendationsRequest : RequestParams + { + public RecommendationsRequest() + { + Min = new Dictionary(); + Max = new Dictionary(); + Target = new Dictionary(); + SeedArtists = new List(); + SeedGenres = new List(); + SeedTracks = new List(); + } + + /// + /// A comma separated list of Spotify IDs for seed artists. + /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. + /// + /// + [QueryParam("seed_artists")] + public IList SeedArtists { get; } + + /// + /// A comma separated list of any genres in the set of available genre seeds. + /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. + /// + /// + [QueryParam("seed_genres")] + public IList SeedGenres { get; } + + /// + /// A comma separated list of Spotify IDs for a seed track. + /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. + /// + /// + [QueryParam("seed_tracks")] + public IList SeedTracks { get; } + + /// + /// The target size of the list of recommended tracks. + /// For seeds with unusually small pools or when highly restrictive filtering is applied, + /// it may be impossible to generate the requested number of recommended tracks. + /// Debugging information for such cases is available in the response. Default: 20. Minimum: 1. Maximum: 100. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// Because min_*, max_* and target_* are applied to pools before relinking, the generated results + /// may not precisely match the filters applied. Original, + /// non-relinked tracks are available via the linked_from attribute of the relinked track response. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// Multiple values. For each tunable track attribute, a hard floor on the selected track attribute’s value can be provided. + /// See tunable track attributes below for the list of available options. + /// For example, min_tempo=140 would restrict results to only those tracks with a tempo of greater than 140 beats per minute. + /// DO NOT INCLUDE min_ IN THE KEY + /// + /// + public Dictionary Min { get; } + + /// + /// Multiple values. For each tunable track attribute, a hard ceiling on the selected track attribute’s value can be provided. + /// See tunable track attributes below for the list of available options. + /// For example, max_instrumentalness=0.35 would filter out most tracks that are likely to be instrumental. + /// DO NOT INCLUDE max_ IN THE KEY + /// + /// + public Dictionary Max { get; } + + /// + /// Multiple values. For each of the tunable track attributes (below) a target value may be provided. + /// Tracks with the attribute values nearest to the target values will be preferred. + /// For example, you might request target_energy=0.6 and target_danceability=0.8. + /// All target values will be weighed equally in ranking results. + /// DO NOT INCLUDE target_ IN THE KEY + /// + /// + public Dictionary Target { get; } + + protected override void CustomEnsure() + { + if (SeedArtists.Count == 0 && SeedGenres.Count == 0 && SeedTracks.Count == 0) + { + throw new ArgumentException("At least one of the seeds has to be non-empty"); + } + } + + protected override void AddCustomQueryParams(Dictionary queryParams) + { + Ensure.ArgumentNotNull(queryParams, nameof(queryParams)); + + foreach (KeyValuePair pair in Min) + { + queryParams.Add($"min_{pair.Key}", pair.Value); + } + foreach (KeyValuePair pair in Max) + { + queryParams.Add($"max_{pair.Key}", pair.Value); + } + foreach (KeyValuePair pair in Target) + { + queryParams.Add($"target_{pair.Key}", pair.Value); + } + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/RequestParams.cs b/Source Files/SpotifyAPI.Web/Models/Request/RequestParams.cs new file mode 100644 index 0000000..564ffdd --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/RequestParams.cs @@ -0,0 +1,182 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections; +using Newtonsoft.Json.Linq; + +namespace SpotifyAPI.Web +{ + public abstract class RequestParams + { + private static readonly ConcurrentDictionary> _bodyParamsCache = + new(); + + public JObject BuildBodyParams() + { + // Make sure everything is okay before building body params + CustomEnsure(); + + var body = new JObject(); + var type = GetType(); + + if (!_bodyParamsCache.IsEmpty && _bodyParamsCache.ContainsKey(type)) + { + foreach (var (info, attribute) in _bodyParamsCache[type]) + { + AddBodyParam(body, info, attribute); + } + } + else + { + var bodyProps = GetType() + .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) + .Where(prop => prop.GetCustomAttributes(typeof(BodyParamAttribute), true).Length > 0); + + var cachedParams = new List<(PropertyInfo, BodyParamAttribute)>(); + foreach (var prop in bodyProps) + { + var attribute = prop.GetCustomAttribute(); + if (attribute != null) + { + cachedParams.Add((prop, attribute)); + AddBodyParam(body, prop, attribute); + } + } + _bodyParamsCache[type] = cachedParams; + } + + return body; + } + + private void AddBodyParam(JObject body, PropertyInfo prop, BodyParamAttribute attribute) + { + object? value = prop.GetValue(this); + if (value != null) + { + body[attribute.Key ?? prop.Name] = JToken.FromObject(value); + } + } + + private static readonly ConcurrentDictionary> _queryParamsCache = + new(); + + public Dictionary BuildQueryParams() + { + // Make sure everything is okay before building query params + CustomEnsure(); + + var queryParams = new Dictionary(); + var type = GetType(); + + if (!_queryParamsCache.IsEmpty && _queryParamsCache.ContainsKey(type)) + { + foreach (var (info, attribute) in _queryParamsCache[type]) + { + AddQueryParam(queryParams, info, attribute); + } + } + else + { + var queryProps = GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) + .Where(prop => prop.GetCustomAttributes(typeof(QueryParamAttribute), true).Length > 0); + + var cachedParams = new List<(PropertyInfo, QueryParamAttribute)>(); + foreach (var prop in queryProps) + { + var attribute = prop.GetCustomAttribute(); + if (attribute != null) + { + cachedParams.Add((prop, attribute)); + AddQueryParam(queryParams, prop, attribute); + } + } + _queryParamsCache[type] = cachedParams; + } + + AddCustomQueryParams(queryParams); + + return queryParams; + } + + private void AddQueryParam(Dictionary queryParams, PropertyInfo prop, QueryParamAttribute attribute) + { + object? value = prop.GetValue(this); + if (value != null) + { + if (value is IList list) + { + if (list.Count > 0) + { + var str = string.Join(",", list); + queryParams.Add(attribute.Key ?? prop.Name, str); + } + } + else if (value is bool valueAsBool) + { + queryParams.Add(attribute.Key ?? prop.Name, valueAsBool ? "true" : "false"); + } + else if (value is Enum valueAsEnum) + { + var enumType = valueAsEnum.GetType(); + var valueList = new List(); + + if (enumType.IsDefined(typeof(FlagsAttribute), false)) + { + foreach (Enum enumVal in Enum.GetValues(valueAsEnum.GetType())) + { + if (valueAsEnum.HasFlag(enumVal)) + { + if (StringAttribute.GetValue(enumType, enumVal, out var stringVal)) + { + // .netstandard2.0 requires ! + valueList.Add(stringVal!); + } + } + } + } + else + { + if (StringAttribute.GetValue(enumType, valueAsEnum, out var stringVal)) + { + // .netstandard2.0 requires ! + valueList.Add(stringVal!); + } + } + queryParams.Add(attribute.Key ?? prop.Name, string.Join(",", valueList)); + } + else + { + queryParams.Add(attribute.Key ?? prop.Name, value.ToString() ?? throw new APIException("ToString returned null for query parameter")); + } + } + } + + protected virtual void CustomEnsure() { } + protected virtual void AddCustomQueryParams(Dictionary queryParams) { } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class QueryParamAttribute : Attribute + { + public string Key { get; } + + public QueryParamAttribute(string key) + { + Key = key; + } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class BodyParamAttribute : Attribute + { + public string Key { get; } + + public BodyParamAttribute(string key) + { + Key = key; + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/SearchRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/SearchRequest.cs new file mode 100644 index 0000000..5d645b8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/SearchRequest.cs @@ -0,0 +1,120 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class SearchRequest : RequestParams + { + /// + /// + /// + /// + /// A comma-separated list of item types to search across. + /// Valid types are: album , artist, playlist, track, show and episode. + /// Search results include hits from all the specified item types. + /// + /// + /// Search query keywords and optional field filters and operators. + /// + /// + /// https://developer.spotify.com/documentation/web-api/reference/search/search/ + /// + public SearchRequest(Types type, string query) + { + Ensure.ArgumentNotNull(type, nameof(type)); + Ensure.ArgumentNotNullOrEmptyString(query, nameof(query)); + + Type = type; + Query = query; + } + + /// + /// A comma-separated list of item types to search across. + /// Valid types are: album , artist, playlist, track, show and episode. + /// Search results include hits from all the specified item types. + /// + /// + [QueryParam("type")] + public Types Type { get; set; } + + /// + /// Search query keywords and optional field filters and operators. + /// + /// + [QueryParam("q")] + public string Query { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// If a country code is specified, only content that is playable in that market is returned. + /// Note: + /// - Playlist results are not affected by the market parameter. + /// - If market is set to from_token, and a valid access token is + /// specified in the request header, only content playable in the country + /// associated with the user account, is returned. + /// - Users can view the country that is associated with their account in the + /// account settings. A user must grant access to the user-read-private scope + /// prior to when the access token is issued. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + + /// + /// Maximum number of results to return. + /// Default: 20 + /// Minimum: 1 + /// Maximum: 50 + /// Note: The limit is applied within each type, not on the total response. + /// For example, if the limit value is 3 and the type is artist,album, + /// the response contains 3 artists and 3 albums. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first result to return. + /// Default: 0 (the first result). Maximum offset (including limit): 2,000. + /// Use with limit to get the next page of search results. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// Possible values: audio + /// If include_external = audio is specified the response + /// will include any relevant audio content that is hosted externally. + /// By default external content is filtered out from responses. + /// + /// + [QueryParam("include_external")] + public IncludeExternals? IncludeExternal { get; set; } + + [Flags] + public enum IncludeExternals + { + [String("audio")] + Audio = 1, + } + + [Flags] + public enum Types + { + [String("album")] + Album = 1, + [String("artist")] + Artist = 2, + [String("playlist")] + Playlist = 4, + [String("track")] + Track = 8, + [String("show")] + Show = 16, + [String("episode")] + Episode = 32, + All = Album | Artist | Playlist | Track | Show | Episode + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs new file mode 100644 index 0000000..8f355eb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs @@ -0,0 +1,33 @@ +namespace SpotifyAPI.Web +{ + public class ShowEpisodesRequest : RequestParams + { + /// + /// The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50. + /// + /// + [QueryParam("limit")] + public int? Limit { get; set; } + + /// + /// The index of the first episode to return. + /// Default: 0 (the first object). Use with limit to get the next set of episodes. + /// + /// + [QueryParam("offset")] + public int? Offset { get; set; } + + /// + /// An ISO 3166-1 alpha-2 country code. If a country code is specified, only shows and episodes + /// that are available in that market will be returned. + /// If a valid user access token is specified in the request header, + /// the country associated with the user account will take priority over this parameter. + /// Note: If neither market or user country are provided, the content is considered unavailable for the client. + /// Users can view the country that is associated with their account in the account settings. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ShowRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ShowRequest.cs new file mode 100644 index 0000000..e2cd1fb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ShowRequest.cs @@ -0,0 +1,19 @@ +namespace SpotifyAPI.Web +{ + public class ShowRequest : RequestParams + { + /// + /// An ISO 3166-1 alpha-2 country code. If a country code is specified, + /// only shows and episodes that are available in that market will be returned. + /// If a valid user access token is specified in the request header, + /// the country associated with the user account will take priority over this parameter. + /// Note: If neither market or user country are provided, the content + /// is considered unavailable for the client. + /// Users can view the country that is associated with their account in the account settings. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/ShowsRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/ShowsRequest.cs new file mode 100644 index 0000000..edccc42 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/ShowsRequest.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ShowsRequest : RequestParams + { + /// + /// Get Spotify catalog information for several shows based on their Spotify IDs. + /// + /// + /// A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// + public ShowsRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + + /// + /// An ISO 3166-1 alpha-2 country code. If a country code is specified, only shows and episodes + /// that are available in that market will be returned. + /// If a valid user access token is specified in the request header, + /// the country associated with the user account will take priority over this parameter. + /// Note: If neither market or user country are provided, + /// the content is considered unavailable for the client. + /// Users can view the country that is associated with their account in the account settings. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapRefreshRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapRefreshRequest.cs new file mode 100644 index 0000000..8f0ddd9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapRefreshRequest.cs @@ -0,0 +1,20 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class TokenSwapRefreshRequest + { + public TokenSwapRefreshRequest(Uri refreshUri, string refreshToken) + { + Ensure.ArgumentNotNull(refreshUri, nameof(refreshUri)); + Ensure.ArgumentNotNullOrEmptyString(refreshToken, nameof(refreshToken)); + + RefreshUri = refreshUri; + RefreshToken = refreshToken; + } + + public string RefreshToken { get; } + + public Uri RefreshUri { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapTokenRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapTokenRequest.cs new file mode 100644 index 0000000..068dc7a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/TokenSwapTokenRequest.cs @@ -0,0 +1,20 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class TokenSwapTokenRequest + { + public TokenSwapTokenRequest(Uri tokenUri, string code) + { + Ensure.ArgumentNotNull(tokenUri, nameof(tokenUri)); + Ensure.ArgumentNotNullOrEmptyString(code, nameof(code)); + + TokenUri = tokenUri; + Code = code; + } + + public string Code { get; } + + public Uri TokenUri { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Request/TrackRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/TrackRequest.cs new file mode 100644 index 0000000..17536d7 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/TrackRequest.cs @@ -0,0 +1,14 @@ +namespace SpotifyAPI.Web +{ + public class TrackRequest : RequestParams + { + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/TracksAudioFeaturesRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/TracksAudioFeaturesRequest.cs new file mode 100644 index 0000000..31d9e4a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/TracksAudioFeaturesRequest.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class TracksAudioFeaturesRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 100 IDs. + public TracksAudioFeaturesRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 100 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/TracksRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/TracksRequest.cs new file mode 100644 index 0000000..1edd2fa --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/TracksRequest.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class TracksRequest : RequestParams + { + /// + /// + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 50 IDs. + public TracksRequest(IList ids) + { + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + Ids = ids; + } + + /// + /// A comma-separated list of the Spotify IDs for the tracks. Maximum: 50 IDs. + /// + /// + [QueryParam("ids")] + public IList Ids { get; } + + /// + /// An ISO 3166-1 alpha-2 country code or the string from_token. + /// Provide this parameter if you want to apply Track Relinking. + /// + /// + [QueryParam("market")] + public string? Market { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Request/UnfollowRequest.cs b/Source Files/SpotifyAPI.Web/Models/Request/UnfollowRequest.cs new file mode 100644 index 0000000..91acf8f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Request/UnfollowRequest.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +namespace SpotifyAPI.Web +{ + public class UnfollowRequest : RequestParams + { + /// + /// + /// + /// The ID type: either artist or user. + /// + /// A comma-separated list of the artist or the user Spotify IDs. F + /// or example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + public UnfollowRequest(Type type, IList ids) + { + Ensure.ArgumentNotNull(type, nameof(type)); + Ensure.ArgumentNotNullOrEmptyList(ids, nameof(ids)); + + TypeParam = type; + Ids = ids; + } + + /// + /// The ID type: either artist or user. + /// + /// + [QueryParam("type")] + public Type TypeParam { get; } + + /// + /// A comma-separated list of the artist or the user Spotify IDs. F + /// or example: ids=74ASZWbe4lXaubB36ztrGX,08td7MxkoHQkXnWAYD8d6Q. + /// A maximum of 50 IDs can be sent in one request. + /// + /// + [BodyParam("ids")] + public IList Ids { get; } + + public enum Type + { + [String("artist")] + Artist, + [String("user")] + User + } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Actions.cs b/Source Files/SpotifyAPI.Web/Models/Response/Actions.cs new file mode 100644 index 0000000..2114a48 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Actions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class Actions + { + public Dictionary Disallows { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/AlbumsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/AlbumsResponse.cs new file mode 100644 index 0000000..887a6ed --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/AlbumsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class AlbumsResponse + { + public List Albums { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ArtistsRelatedArtistsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsRelatedArtistsResponse.cs new file mode 100644 index 0000000..ea03366 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsRelatedArtistsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ArtistsRelatedArtistsResponse + { + public List Artists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ArtistsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsResponse.cs new file mode 100644 index 0000000..c47e70f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ArtistsResponse + { + public List Artists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ArtistsTopTracksResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsTopTracksResponse.cs new file mode 100644 index 0000000..abf120d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ArtistsTopTracksResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ArtistsTopTracksResponse + { + public List Tracks { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeRefreshResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeRefreshResponse.cs new file mode 100644 index 0000000..b4dc1c4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeRefreshResponse.cs @@ -0,0 +1,21 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class AuthorizationCodeRefreshResponse: IUserToken + { + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } + public string Scope { get; set; } = default!; + + /// + /// Auto-Initalized to UTC Now + /// + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeTokenResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeTokenResponse.cs new file mode 100644 index 0000000..05cea2a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/AuthorizationCodeTokenResponse.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class AuthorizationCodeTokenResponse: IRefreshableToken + { + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } + public string Scope { get; set; } = default!; + public string RefreshToken { get; set; } = default!; + + /// + /// Auto-Initalized to UTC Now + /// + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/CategoriesResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/CategoriesResponse.cs new file mode 100644 index 0000000..ca63e5b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/CategoriesResponse.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web +{ + public class CategoriesResponse + { + public Paging Categories { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Category.cs b/Source Files/SpotifyAPI.Web/Models/Response/Category.cs new file mode 100644 index 0000000..66e300f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Category.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class Category + { + public string Href { get; set; } = default!; + public List Icons { get; set; } = default!; + public string Id { get; set; } = default!; + public string Name { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/CategoryPlaylistsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/CategoryPlaylistsResponse.cs new file mode 100644 index 0000000..e65fa8a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/CategoryPlaylistsResponse.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web +{ + public class CategoryPlaylistsResponse + { + public Paging Playlists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ClientCredentialsTokenResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/ClientCredentialsTokenResponse.cs new file mode 100644 index 0000000..3261c44 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ClientCredentialsTokenResponse.cs @@ -0,0 +1,20 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class ClientCredentialsTokenResponse: IToken + { + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } + + /// + /// Auto-Initalized to UTC Now + /// + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Context.cs b/Source Files/SpotifyAPI.Web/Models/Response/Context.cs new file mode 100644 index 0000000..924f9f8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Context.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class Context + { + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Copyright.cs b/Source Files/SpotifyAPI.Web/Models/Response/Copyright.cs new file mode 100644 index 0000000..59dac4c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Copyright.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class Copyright + { + public string Text { get; set; } = default!; + public string Type { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs b/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs new file mode 100644 index 0000000..e39fd6e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class CurrentlyPlaying + { + public Context Context { get; set; } = default!; + public string CurrentlyPlayingType { get; set; } = default!; + public bool IsPlaying { get; set; } + + /// + /// Can be a FullTrack or FullEpisode + /// + /// + [JsonConverter(typeof(PlayableItemConverter))] + public IPlayableItem Item { get; set; } = default!; + public int? ProgressMs { get; set; } + public long Timestamp { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs b/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs new file mode 100644 index 0000000..0ac9c4c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class CurrentlyPlayingContext + { + public Device Device { get; set; } = default!; + public string RepeatState { get; set; } = default!; + public bool ShuffleState { get; set; } + public Context Context { get; set; } = default!; + public long Timestamp { get; set; } + public int ProgressMs { get; set; } + public bool IsPlaying { get; set; } + + /// + /// Can be a FullTrack or FullEpisode + /// + /// + [JsonConverter(typeof(PlayableItemConverter))] + public IPlayableItem Item { get; set; } = default!; + + public string CurrentlyPlayingType { get; set; } = default!; + public Actions Actions { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Cursor.cs b/Source Files/SpotifyAPI.Web/Models/Response/Cursor.cs new file mode 100644 index 0000000..2033342 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Cursor.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class Cursor + { + public string Before { get; set; } = default!; + public string After { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/CursorPaging.cs b/Source Files/SpotifyAPI.Web/Models/Response/CursorPaging.cs new file mode 100644 index 0000000..262c1fb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/CursorPaging.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class CursorPaging : IPaginatable + { + public string Href { get; set; } = default!; + public List? Items { get; set; } = default!; + public int Limit { get; set; } + public string? Next { get; set; } = default!; + public Cursor Cursors { get; set; } = default!; + public int Total { get; set; } + } + + public class CursorPaging : IPaginatable + { + public string Href { get; set; } = default!; + public List? Items { get; set; } = default!; + public int Limit { get; set; } + public string? Next { get; set; } = default!; + public Cursor Cursors { get; set; } = default!; + public int Total { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Device.cs b/Source Files/SpotifyAPI.Web/Models/Response/Device.cs new file mode 100644 index 0000000..670e856 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Device.cs @@ -0,0 +1,14 @@ +namespace SpotifyAPI.Web +{ + public class Device + { + public string Id { get; set; } = default!; + public bool IsActive { get; set; } + public bool IsPrivateSession { get; set; } + public bool IsRestricted { get; set; } + public string Name { get; set; } = default!; + public string Type { get; set; } = default!; + public int? VolumePercent { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/DeviceResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/DeviceResponse.cs new file mode 100644 index 0000000..fa901bc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/DeviceResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class DeviceResponse + { + public List Devices { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/EpisodesResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/EpisodesResponse.cs new file mode 100644 index 0000000..f231a5c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/EpisodesResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class EpisodesResponse + { + public List Episodes { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FeaturedPlaylistsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/FeaturedPlaylistsResponse.cs new file mode 100644 index 0000000..f38b734 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FeaturedPlaylistsResponse.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class FeaturedPlaylistsResponse + { + public string Message { get; set; } = default!; + public Paging Playlists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FollowedArtistsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/FollowedArtistsResponse.cs new file mode 100644 index 0000000..8c37a29 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FollowedArtistsResponse.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web +{ + public class FollowedArtistsResponse + { + public CursorPaging Artists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Followers.cs b/Source Files/SpotifyAPI.Web/Models/Response/Followers.cs new file mode 100644 index 0000000..21d7365 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Followers.cs @@ -0,0 +1,10 @@ +namespace SpotifyAPI.Web +{ + public class Followers + { + public string Href { get; set; } = default!; + + public int Total { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullAlbum.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullAlbum.cs new file mode 100644 index 0000000..1595a07 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullAlbum.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FullAlbum + { + public string AlbumType { get; set; } = default!; + public List Artists { get; set; } = default!; + public List AvailableMarkets { get; set; } = default!; + public List Copyrights { get; set; } = default!; + public Dictionary ExternalIds { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public List Genres { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Label { get; set; } = default!; + public string Name { get; set; } = default!; + public int Popularity { get; set; } + public string ReleaseDate { get; set; } = default!; + public string ReleaseDatePrecision { get; set; } = default!; + public Dictionary Restrictions { get; set; } = default!; + public Paging Tracks { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullArtist.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullArtist.cs new file mode 100644 index 0000000..dad2844 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullArtist.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FullArtist + { + public Dictionary ExternalUrls { get; set; } = default!; + public Followers Followers { get; set; } = default!; + public List Genres { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Name { get; set; } = default!; + public int Popularity { get; set; } + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullEpisode.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullEpisode.cs new file mode 100644 index 0000000..733595d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullEpisode.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public class FullEpisode : IPlayableItem + { + public string AudioPreviewUrl { get; set; } = default!; + public string Description { get; set; } = default!; + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public bool IsExternallyHosted { get; set; } + public bool IsPlayable { get; set; } + public List Languages { get; set; } = default!; + public string Name { get; set; } = default!; + public string ReleaseDate { get; set; } = default!; + public string ReleaseDatePrecision { get; set; } = default!; + public ResumePoint ResumePoint { get; set; } = default!; + public SimpleShow Show { get; set; } = default!; + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullPlaylist.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullPlaylist.cs new file mode 100644 index 0000000..0a6bc64 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullPlaylist.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +namespace SpotifyAPI.Web +{ + public class FullPlaylist + { + public bool? Collaborative { get; set; } + public string? Description { get; set; } = default!; + public Dictionary? ExternalUrls { get; set; } = default!; + public Followers Followers { get; set; } = default!; + public string? Href { get; set; } = default!; + public string? Id { get; set; } = default!; + public List? Images { get; set; } = default!; + public string? Name { get; set; } = default!; + public PublicUser? Owner { get; set; } = default!; + public bool? Public { get; set; } + public string? SnapshotId { get; set; } = default!; + + /// + /// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode + /// + /// + public Paging>? Tracks { get; set; } = default!; + public string? Type { get; set; } = default!; + public string? Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullShow.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullShow.cs new file mode 100644 index 0000000..44c4ee1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullShow.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FullShow + { + public List AvailableMarkets { get; set; } = default!; + public List Copyrights { get; set; } = default!; + public string Description { get; set; } = default!; + public Paging Episodes { get; set; } = default!; + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public bool IsExternallyHosted { get; set; } + public List Languages { get; set; } = default!; + public string MediaType { get; set; } = default!; + public string Name { get; set; } = default!; + public string Publisher { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/FullTrack.cs b/Source Files/SpotifyAPI.Web/Models/Response/FullTrack.cs new file mode 100644 index 0000000..71331bc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/FullTrack.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public class FullTrack : IPlayableItem + { + public SimpleAlbum Album { get; set; } = default!; + public List Artists { get; set; } = default!; + public List AvailableMarkets { get; set; } = default!; + public int DiscNumber { get; set; } + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalIds { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public bool IsPlayable { get; set; } + public LinkedTrack LinkedFrom { get; set; } = default!; + public Dictionary Restrictions { get; set; } = default!; + public string Name { get; set; } = default!; + public int Popularity { get; set; } + public string PreviewUrl { get; set; } = default!; + public int TrackNumber { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } + public string Uri { get; set; } = default!; + public bool IsLocal { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Image.cs b/Source Files/SpotifyAPI.Web/Models/Response/Image.cs new file mode 100644 index 0000000..83d70e5 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Image.cs @@ -0,0 +1,10 @@ +namespace SpotifyAPI.Web +{ + public class Image + { + public int Height { get; set; } + public int Width { get; set; } + public string Url { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs new file mode 100644 index 0000000..56a5f5e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public enum ItemType + { + Track, + Episode + } + + public interface IPlayableItem + { + [JsonConverter(typeof(StringEnumConverter))] + ItemType Type { get; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IRefreshableToken.cs b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IRefreshableToken.cs new file mode 100644 index 0000000..ddb83eb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IRefreshableToken.cs @@ -0,0 +1,13 @@ +namespace SpotifyAPI.Web +{ + /// + /// An user token, which can be refreshed + /// + public interface IRefreshableToken : IUserToken + { + /// + /// Refresh token + /// + public string RefreshToken { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IToken.cs b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IToken.cs new file mode 100644 index 0000000..e9e6e93 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IToken.cs @@ -0,0 +1,30 @@ +using System; + +namespace SpotifyAPI.Web +{ + /// + /// A token to access the Spotify API + /// + public interface IToken + { + /// + /// Access token string + /// + public string AccessToken { get; set; } + + /// + /// Type of this token (eg. Bearer) + /// + public string TokenType { get; set; } + + /// + /// Auto-Initalized to UTC Now + /// + public DateTime CreatedAt { get; set; } + + /// + /// Is the token still valid? + /// + public bool IsExpired { get; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IUserToken.cs b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IUserToken.cs new file mode 100644 index 0000000..0745561 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Interfaces/IUserToken.cs @@ -0,0 +1,13 @@ +namespace SpotifyAPI.Web +{ + /// + /// A token which allows you to access the API as user + /// + public interface IUserToken : IToken + { + /// + /// Comma-Seperated list of scopes + /// + public string Scope { get; set; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Response/LinkedTrack.cs b/Source Files/SpotifyAPI.Web/Models/Response/LinkedTrack.cs new file mode 100644 index 0000000..093f578 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/LinkedTrack.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +namespace SpotifyAPI.Web +{ + public class LinkedTrack + { + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/NewReleasesResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/NewReleasesResponse.cs new file mode 100644 index 0000000..d9ec7aa --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/NewReleasesResponse.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class NewReleasesResponse + { + public string Message { get; set; } = default!; + public Paging Albums { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/PKCETokenResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/PKCETokenResponse.cs new file mode 100644 index 0000000..d274858 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/PKCETokenResponse.cs @@ -0,0 +1,21 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class PKCETokenResponse: IRefreshableToken + { + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } + public string Scope { get; set; } = default!; + public string RefreshToken { get; set; } = default!; + + /// + /// Auto-Initalized to UTC Now + /// + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; } + } +} diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Paging.cs b/Source Files/SpotifyAPI.Web/Models/Response/Paging.cs new file mode 100644 index 0000000..a03756c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Paging.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class Paging : IPaginatable + { + public string? Href { get; set; } = default!; + public List? Items { get; set; } = default!; + public int? Limit { get; set; } = default!; + public string? Next { get; set; } = default!; + public int? Offset { get; set; } = default!; + public string? Previous { get; set; } = default!; + public int? Total { get; set; } = default!; + } + + public class Paging : IPaginatable + { + public string? Href { get; set; } = default!; + public List? Items { get; set; } = default!; + public int? Limit { get; set; } = default!; + public string? Next { get; set; } = default!; + public int? Offset { get; set; } = default!; + public string? Previous { get; set; } = default!; + public int? Total { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/PlayHistoryItem.cs b/Source Files/SpotifyAPI.Web/Models/Response/PlayHistoryItem.cs new file mode 100644 index 0000000..9aa127c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/PlayHistoryItem.cs @@ -0,0 +1,11 @@ +using System; +namespace SpotifyAPI.Web +{ + public class PlayHistoryItem + { + public SimpleTrack Track { get; set; } = default!; + public DateTime PlayedAt { get; set; } + public Context Context { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs b/Source Files/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs new file mode 100644 index 0000000..ecdf511 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs @@ -0,0 +1,16 @@ +using System; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PlaylistTrack + { + public DateTime? AddedAt { get; set; } + public PublicUser AddedBy { get; set; } = default!; + public bool IsLocal { get; set; } + + [JsonConverter(typeof(PlayableItemConverter))] + public T Track { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/PrivateUser.cs b/Source Files/SpotifyAPI.Web/Models/Response/PrivateUser.cs new file mode 100644 index 0000000..7995df4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/PrivateUser.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PrivateUser + { + public string Country { get; set; } = default!; + public string DisplayName { get; set; } = default!; + public string Email { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public Followers Followers { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Product { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/PublicUser.cs b/Source Files/SpotifyAPI.Web/Models/Response/PublicUser.cs new file mode 100644 index 0000000..b7d0cff --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/PublicUser.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PublicUser + { + public string DisplayName { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public Followers Followers { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/RecommendationGenresResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationGenresResponse.cs new file mode 100644 index 0000000..07027b0 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationGenresResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class RecommendationGenresResponse + { + public List Genres { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs new file mode 100644 index 0000000..abb6c7e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationSeed.cs @@ -0,0 +1,19 @@ +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; } = default!; + public string Id { get; set; } = default!; + [JsonProperty("initialPoolSize")] + public int InitialPoolSize { get; set; } + public string Type { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs new file mode 100644 index 0000000..f97b59e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/RecommendationsResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class RecommendationsResponse + { + public List Seeds { get; set; } = default!; + public List Tracks { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ResumePointObject.cs b/Source Files/SpotifyAPI.Web/Models/Response/ResumePointObject.cs new file mode 100644 index 0000000..a5d9173 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ResumePointObject.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class ResumePoint + { + public bool FullyPlayed { get; set; } + public int ResumePositionMs { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SavedAlbum.cs b/Source Files/SpotifyAPI.Web/Models/Response/SavedAlbum.cs new file mode 100644 index 0000000..0415e27 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SavedAlbum.cs @@ -0,0 +1,11 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class SavedAlbum + { + public DateTime AddedAt { get; set; } + public FullAlbum Album { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SavedShow.cs b/Source Files/SpotifyAPI.Web/Models/Response/SavedShow.cs new file mode 100644 index 0000000..48402d1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SavedShow.cs @@ -0,0 +1,11 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class SavedShow + { + public DateTime AddedAt { get; set; } + public FullShow Show { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SavedTrack.cs b/Source Files/SpotifyAPI.Web/Models/Response/SavedTrack.cs new file mode 100644 index 0000000..b6d0b1b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SavedTrack.cs @@ -0,0 +1,11 @@ +using System; + +namespace SpotifyAPI.Web +{ + public class SavedTrack + { + public DateTime AddedAt { get; set; } + public FullTrack Track { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SearchResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/SearchResponse.cs new file mode 100644 index 0000000..064acfb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SearchResponse.cs @@ -0,0 +1,13 @@ +namespace SpotifyAPI.Web +{ + public class SearchResponse + { + public Paging Artists { get; set; } = default!; + public Paging Albums { get; set; } = default!; + public Paging Tracks { get; set; } = default!; + public Paging Shows { get; set; } = default!; + public Paging Episodes { get; set; } = default!; + public Paging Playlists { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Section.cs b/Source Files/SpotifyAPI.Web/Models/Response/Section.cs new file mode 100644 index 0000000..a2aaa32 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Section.cs @@ -0,0 +1,19 @@ +namespace SpotifyAPI.Web +{ + public class Section + { + public float Start { get; set; } + public float Duration { get; set; } + public float Confidence { get; set; } + public float Loudness { get; set; } + public float Tempo { get; set; } + public float TempoConfidence { get; set; } + public int Key { get; set; } + public float KeyConfidence { get; set; } + public int Mode { get; set; } + public float ModeConfidence { get; set; } + public int TimeSignature { get; set; } + public float TimeSignatureConfidence { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/Segment.cs b/Source Files/SpotifyAPI.Web/Models/Response/Segment.cs new file mode 100644 index 0000000..cb90683 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/Segment.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class Segment + { + public float Start { get; set; } + public float Duration { get; set; } + public float Confidence { get; set; } + public float LoudnessStart { get; set; } + public float LoudnessMax { get; set; } + public float LoudnessMaxTime { get; set; } + public float LoudnessEnd { get; set; } + public List Pitches { get; set; } = default!; + public List Timbre { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/ShowsResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/ShowsResponse.cs new file mode 100644 index 0000000..08e001c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/ShowsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ShowsResponse + { + public List Shows { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimpleAlbum.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimpleAlbum.cs new file mode 100644 index 0000000..b9140b6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimpleAlbum.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class SimpleAlbum + { + public string AlbumGroup { get; set; } = default!; + public string AlbumType { get; set; } = default!; + public List Artists { get; set; } = default!; + public List AvailableMarkets { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Name { get; set; } = default!; + public string ReleaseDate { get; set; } = default!; + public string ReleaseDatePrecision { get; set; } = default!; + public Dictionary Restrictions { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimpleArtist.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimpleArtist.cs new file mode 100644 index 0000000..da8ae4b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimpleArtist.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +namespace SpotifyAPI.Web +{ + public class SimpleArtist + { + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public string Name { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs new file mode 100644 index 0000000..eb48e0f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public class SimpleEpisode + { + public string AudioPreviewUrl { get; set; } = default!; + public string Description { get; set; } = default!; + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public bool IsExternallyHosted { get; set; } + public bool IsPlayable { get; set; } + + [Obsolete("This field is deprecated and might be removed in the future. Please use the languages field instead")] + public string Language { get; set; } = default!; + public List Languages { get; set; } = default!; + public string Name { get; set; } = default!; + public string ReleaseDate { get; set; } = default!; + public string ReleaseDatePrecision { get; set; } = default!; + public ResumePoint ResumePoint { get; set; } = default!; + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs new file mode 100644 index 0000000..3dc4255 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +namespace SpotifyAPI.Web +{ + /// + /// Docs + /// + public class SimplePlaylist + { + public bool Collaborative { get; set; } + public string Description { get; set; } = default!; + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public string Name { get; set; } = default!; + public PublicUser Owner { get; set; } = default!; + public bool? Public { get; set; } + public string SnapshotId { get; set; } = default!; + + /// + /// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode + /// + /// + public Paging> Tracks { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimpleShow.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimpleShow.cs new file mode 100644 index 0000000..d5e56f5 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimpleShow.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class SimpleShow + { + public List AvailableMarkets { get; set; } = default!; + public List Copyrights { get; set; } = default!; + public string Description { get; set; } = default!; + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public List Images { get; set; } = default!; + public bool IsExternallyHosted { get; set; } + public List Languages { get; set; } = default!; + public string MediaType { get; set; } = default!; + public string Name { get; set; } = default!; + public string Publisher { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SimpleTrack.cs b/Source Files/SpotifyAPI.Web/Models/Response/SimpleTrack.cs new file mode 100644 index 0000000..c395f14 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SimpleTrack.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public class SimpleTrack + { + public List Artists { get; set; } = default!; + public List AvailableMarkets { get; set; } = default!; + public int DiscNumber { get; set; } + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } = default!; + public string Href { get; set; } = default!; + public string Id { get; set; } = default!; + public bool IsPlayable { get; set; } + public LinkedTrack LinkedFrom { get; set; } = default!; + public string Name { get; set; } = default!; + public string PreviewUrl { get; set; } = default!; + public int TrackNumber { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } + public string Uri { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs new file mode 100644 index 0000000..ccf6a07 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web +{ + public class SnapshotResponse + { + public string SnapshotId { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/TimeInterval.cs b/Source Files/SpotifyAPI.Web/Models/Response/TimeInterval.cs new file mode 100644 index 0000000..036c50d --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/TimeInterval.cs @@ -0,0 +1,10 @@ +namespace SpotifyAPI.Web +{ + public class TimeInterval + { + public float Start { get; set; } + public float Duration { get; set; } + public float Confidence { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioAnalysis.cs b/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioAnalysis.cs new file mode 100644 index 0000000..e62fef4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioAnalysis.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class TrackAudioAnalysis + { + public List Bars { get; set; } = default!; + public List Beats { get; set; } = default!; + public List
Sections { get; set; } = default!; + public List Segments { get; set; } = default!; + public List Tatums { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioFeatures.cs b/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioFeatures.cs new file mode 100644 index 0000000..99d0d65 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/TrackAudioFeatures.cs @@ -0,0 +1,25 @@ +namespace SpotifyAPI.Web +{ + public class TrackAudioFeatures + { + public float Acousticness { get; set; } + public string AnalysisUrl { get; set; } = default!; + public float Danceability { get; set; } + public int DurationMs { get; set; } + public float Energy { get; set; } + public string Id { get; set; } = default!; + public float Instrumentalness { get; set; } + public int Key { get; set; } + public float Liveness { get; set; } + public float Loudness { get; set; } + public int Mode { get; set; } + public float Speechiness { get; set; } + public float Tempo { get; set; } + public int TimeSignature { get; set; } + public string TrackHref { get; set; } = default!; + public string Type { get; set; } = default!; + public string Uri { get; set; } = default!; + public float Valence { get; set; } + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/TracksAudioFeaturesResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/TracksAudioFeaturesResponse.cs new file mode 100644 index 0000000..0640dcb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/TracksAudioFeaturesResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class TracksAudioFeaturesResponse + { + public List AudioFeatures { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/Response/TracksResponse.cs b/Source Files/SpotifyAPI.Web/Models/Response/TracksResponse.cs new file mode 100644 index 0000000..6bb2d62 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Response/TracksResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class TracksResponse + { + public List Tracks { get; set; } = default!; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/ResponseInfo.cs b/Source Files/SpotifyAPI.Web/Models/ResponseInfo.cs deleted file mode 100644 index 1a8a12c..0000000 --- a/Source Files/SpotifyAPI.Web/Models/ResponseInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; - -namespace SpotifyAPI.Web.Models -{ - public class ResponseInfo - { - public WebHeaderCollection Headers { get; set; } - - public HttpStatusCode StatusCode { get; set; } - - public static readonly ResponseInfo Empty = new ResponseInfo(); - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Scopes.cs b/Source Files/SpotifyAPI.Web/Models/Scopes.cs new file mode 100644 index 0000000..5b311b8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Scopes.cs @@ -0,0 +1,26 @@ +namespace SpotifyAPI.Web +{ + public static class Scopes + { + public const string UgcImageUpload = "ugc-image-upload"; + public const string UserReadPlaybackState = "user-read-playback-state"; + public const string UserModifyPlaybackState = "user-modify-playback-state"; + public const string UserReadCurrentlyPlaying = "user-read-currently-playing"; + public const string Streaming = "streaming"; + public const string AppRemoteControl = "app-remote-control"; + public const string UserReadEmail = "user-read-email"; + public const string UserReadPrivate = "user-read-private"; + public const string PlaylistReadCollaborative = "playlist-read-collaborative"; + public const string PlaylistModifyPublic = "playlist-modify-public"; + public const string PlaylistReadPrivate = "playlist-read-private"; + public const string PlaylistModifyPrivate = "playlist-modify-private"; + public const string UserLibraryModify = "user-library-modify"; + public const string UserLibraryRead = "user-library-read"; + public const string UserTopRead = "user-top-read"; + public const string UserReadPlaybackPosition = "user-read-playback-position"; + public const string UserReadRecentlyPlayed = "user-read-recently-played"; + public const string UserFollowRead = "user-follow-read"; + public const string UserFollowModify = "user-follow-modify"; + } +} + diff --git a/Source Files/SpotifyAPI.Web/Models/SearchItem.cs b/Source Files/SpotifyAPI.Web/Models/SearchItem.cs deleted file mode 100644 index 65b76c5..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SearchItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class SearchItem : BasicModel - { - [JsonProperty("artists")] - public Paging Artists { get; set; } - - [JsonProperty("albums")] - public Paging Albums { get; set; } - - [JsonProperty("tracks")] - public Paging Tracks { get; set; } - - [JsonProperty("playlists")] - public Paging Playlists { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SeveralAlbums.cs b/Source Files/SpotifyAPI.Web/Models/SeveralAlbums.cs deleted file mode 100644 index 74025da..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SeveralAlbums.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class SeveralAlbums : BasicModel - { - [JsonProperty("albums")] - public List Albums { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SeveralArtists.cs b/Source Files/SpotifyAPI.Web/Models/SeveralArtists.cs deleted file mode 100644 index 28df5db..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SeveralArtists.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class SeveralArtists : BasicModel - { - [JsonProperty("artists")] - public List Artists { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SeveralAudioFeatures.cs b/Source Files/SpotifyAPI.Web/Models/SeveralAudioFeatures.cs deleted file mode 100644 index 34ea651..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SeveralAudioFeatures.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class SeveralAudioFeatures : BasicModel - { - [JsonProperty("audio_features")] - public List AudioFeatures { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SeveralTracks.cs b/Source Files/SpotifyAPI.Web/Models/SeveralTracks.cs deleted file mode 100644 index a72c216..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SeveralTracks.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class SeveralTracks : BasicModel - { - [JsonProperty("tracks")] - public List Tracks { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SimpleAlbum.cs b/Source Files/SpotifyAPI.Web/Models/SimpleAlbum.cs deleted file mode 100644 index 2e2aa0f..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SimpleAlbum.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class SimpleAlbum : BasicModel - { - [JsonProperty("album_group")] - public string AlbumGroup { get; set; } - - [JsonProperty("album_type")] - public string AlbumType { get; set; } - - [JsonProperty("artists")] - public List Artists { get; set; } - - [JsonProperty("available_markets")] - public List AvailableMarkets { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("release_date")] - public string ReleaseDate { get; set; } - - [JsonProperty("release_date_precision")] - public string ReleaseDatePrecision { get; set; } - - [JsonProperty("restrictions")] - public Dictionary Restrictions { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SimpleArtist.cs b/Source Files/SpotifyAPI.Web/Models/SimpleArtist.cs deleted file mode 100644 index c1294ee..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SimpleArtist.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class SimpleArtist : BasicModel - { - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SimplePlaylist.cs b/Source Files/SpotifyAPI.Web/Models/SimplePlaylist.cs deleted file mode 100644 index 37b3760..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SimplePlaylist.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class SimplePlaylist : BasicModel - { - [JsonProperty("collaborative")] - public bool Collaborative { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternalUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("owner")] - public PublicProfile Owner { get; set; } - - [JsonProperty("public")] - public bool Public { get; set; } - - [JsonProperty("snapshot_id")] - public string SnapshotId { get; set; } - - [JsonProperty("tracks")] - public PlaylistTrackCollection Tracks { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/SimpleTrack.cs b/Source Files/SpotifyAPI.Web/Models/SimpleTrack.cs deleted file mode 100644 index 85702a3..0000000 --- a/Source Files/SpotifyAPI.Web/Models/SimpleTrack.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace SpotifyAPI.Web.Models -{ - public class SimpleTrack : BasicModel - { - [JsonProperty("artists")] - public List Artists { get; set; } - - [JsonProperty("available_markets")] - public List AvailableMarkets { get; set; } - - [JsonProperty("disc_number")] - public int DiscNumber { get; set; } - - [JsonProperty("duration_ms")] - public int DurationMs { get; set; } - - [JsonProperty("explicit")] - public bool Explicit { get; set; } - - [JsonProperty("external_urls")] - public Dictionary ExternUrls { get; set; } - - [JsonProperty("href")] - public string Href { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("preview_url")] - public string PreviewUrl { get; set; } - - [JsonProperty("track_number")] - public int TrackNumber { get; set; } - - [JsonProperty("restrictions")] - public Dictionary Restrictions { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Snapshot.cs b/Source Files/SpotifyAPI.Web/Models/Snapshot.cs deleted file mode 100644 index 4a7e3e7..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Snapshot.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SpotifyAPI.Web.Models -{ - public class Snapshot : BasicModel - { - [JsonProperty("snapshot_id")] - public string SnapshotId { get; set; } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/Token.cs b/Source Files/SpotifyAPI.Web/Models/Token.cs deleted file mode 100644 index 941d6f9..0000000 --- a/Source Files/SpotifyAPI.Web/Models/Token.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace SpotifyAPI.Web.Models -{ - public class Token - { - public Token() - { - CreateDate = DateTime.Now; - } - - [JsonProperty("access_token")] - public string AccessToken { get; set; } - - [JsonProperty("token_type")] - public string TokenType { get; set; } - - [JsonProperty("expires_in")] - public double ExpiresIn { get; set; } - - [JsonProperty("refresh_token")] - public string RefreshToken { get; set; } - - [JsonProperty("error")] - public string Error { get; set; } - - [JsonProperty("error_description")] - public string ErrorDescription { get; set; } - - public DateTime CreateDate { get; set; } - - /// - /// Checks if the token has expired - /// - /// - public bool IsExpired() - { - return CreateDate.Add(TimeSpan.FromSeconds(ExpiresIn)) <= DateTime.Now; - } - - public bool HasError() - { - return Error != null; - } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Models/TuneableTrack.cs b/Source Files/SpotifyAPI.Web/Models/TuneableTrack.cs deleted file mode 100644 index 9f3943b..0000000 --- a/Source Files/SpotifyAPI.Web/Models/TuneableTrack.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Reflection; - -namespace SpotifyAPI.Web.Models -{ - public class TuneableTrack - { - [String("acousticness")] - public float? Acousticness { get; set; } - - [String("danceability")] - public float? Danceability { get; set; } - - [String("duration_ms")] - public int? DurationMs { get; set; } - - [String("energy")] - public float? Energy { get; set; } - - [String("instrumentalness")] - public float? Instrumentalness { get; set; } - - [String("key")] - public int? Key { get; set; } - - [String("liveness")] - public float? Liveness { get; set; } - - [String("loudness")] - public float? Loudness { get; set; } - - [String("mode")] - public int? Mode { get; set; } - - [String("popularity")] - public int? Popularity { get; set; } - - [String("speechiness")] - public float? Speechiness { get; set; } - - [String("tempo")] - public float? Tempo { get; set; } - - [String("time_signature")] - public int? TimeSignature { get; set; } - - [String("valence")] - public float? Valence { get; set; } - - public string BuildUrlParams(string prefix) - { - List urlParams = new List(); - foreach (PropertyInfo info in GetType().GetProperties()) - { - object value = info.GetValue(this); - string name = info.GetCustomAttribute()?.Text; - if(name == null || value == null) - continue; - urlParams.Add(value is float valueAsFloat - ? $"{prefix}_{name}={valueAsFloat.ToString(CultureInfo.InvariantCulture)}" - : $"{prefix}_{name}={value}"); - } - if (urlParams.Count > 0) - return "&" + string.Join("&", urlParams); - return ""; - } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/ProxyConfig.cs b/Source Files/SpotifyAPI.Web/ProxyConfig.cs deleted file mode 100644 index 309f577..0000000 --- a/Source Files/SpotifyAPI.Web/ProxyConfig.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Net; - -namespace SpotifyAPI.Web -{ - public class ProxyConfig - { - public string Host { get; set; } - - public int Port { get; set; } = 80; - - public string Username { get; set; } - - public string Password { get; set; } - - /// - /// Whether to bypass the proxy server for local addresses. - /// - public bool BypassProxyOnLocal { get; set; } - - public void Set(ProxyConfig proxyConfig) - { - Host = proxyConfig?.Host; - Port = proxyConfig?.Port ?? 80; - Username = proxyConfig?.Username; - Password = proxyConfig?.Password; - BypassProxyOnLocal = proxyConfig?.BypassProxyOnLocal ?? false; - } - - /// - /// Whether both and have valid values. - /// - /// - public bool IsValid() - { - return !string.IsNullOrWhiteSpace(Host) && Port > 0; - } - - /// - /// Create a from the host and port number - /// - /// A URI - public Uri GetUri() - { - UriBuilder uriBuilder = new UriBuilder(Host) - { - Port = Port - }; - return uriBuilder.Uri; - } - - /// - /// Creates a from the proxy details of this object. - /// - /// A or null if the proxy details are invalid. - public WebProxy CreateWebProxy() - { - if (!IsValid()) - return null; - - WebProxy proxy = new WebProxy - { - Address = GetUri(), - UseDefaultCredentials = true, - BypassProxyOnLocal = BypassProxyOnLocal - }; - - if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) - return proxy; - - proxy.UseDefaultCredentials = false; - proxy.Credentials = new NetworkCredential(Username, Password); - - return proxy; - } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/RetryHandlers/IRetryHandler.cs b/Source Files/SpotifyAPI.Web/RetryHandlers/IRetryHandler.cs new file mode 100644 index 0000000..1e0d9db --- /dev/null +++ b/Source Files/SpotifyAPI.Web/RetryHandlers/IRetryHandler.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + /// + /// The Retry Handler will be directly called after the response is retrived and before errors and body are processed. + /// + public interface IRetryHandler + { + delegate Task RetryFunc(IRequest request); + + Task HandleRetry(IRequest request, IResponse response, RetryFunc retry); + } +} diff --git a/Source Files/SpotifyAPI.Web/RetryHandlers/SimpleRetryHandler.cs b/Source Files/SpotifyAPI.Web/RetryHandlers/SimpleRetryHandler.cs new file mode 100644 index 0000000..9bb5f34 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/RetryHandlers/SimpleRetryHandler.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Net; +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class SimpleRetryHandler : IRetryHandler + { + private readonly Func _sleep; + + /// + /// Specifies after how many miliseconds should a failed request be retried. + /// + public TimeSpan RetryAfter { get; set; } + + /// + /// Maximum number of tries for one failed request. + /// + public int RetryTimes { get; set; } + + /// + /// Whether a failure of type "Too Many Requests" should use up one of the allocated retry attempts. + /// + public bool TooManyRequestsConsumesARetry { get; set; } + + /// + /// Error codes that will trigger auto-retry + /// + public IEnumerable RetryErrorCodes { get; set; } + + /// + /// A simple retry handler which retries a request based on status codes with a fixed sleep interval. + /// It also supports Retry-After headers sent by spotify. The execution will be delayed by the amount in + /// the Retry-After header + /// + /// + public SimpleRetryHandler() : this(Task.Delay) { } + public SimpleRetryHandler(Func sleep) + { + _sleep = sleep; + RetryAfter = TimeSpan.FromMilliseconds(50); + RetryTimes = 10; + TooManyRequestsConsumesARetry = false; + RetryErrorCodes = new[] { + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable + }; + } + + private static TimeSpan? ParseTooManyRetries(IResponse response) + { + if (response.StatusCode != (HttpStatusCode)429) + { + return null; + } + if ( + (response.Headers.ContainsKey("Retry-After") && int.TryParse(response.Headers["Retry-After"], out int secondsToWait)) + || (response.Headers.ContainsKey("retry-after") && int.TryParse(response.Headers["retry-after"], out secondsToWait))) + { + return TimeSpan.FromSeconds(secondsToWait); + } + + throw new APIException("429 received, but unable to parse Retry-After Header. This should not happen!"); + } + + public Task HandleRetry(IRequest request, IResponse response, IRetryHandler.RetryFunc retry) + { + Ensure.ArgumentNotNull(response, nameof(response)); + Ensure.ArgumentNotNull(retry, nameof(retry)); + + return HandleRetryInternally(request, response, retry, RetryTimes); + } + + private async Task HandleRetryInternally( + IRequest request, + IResponse response, + IRetryHandler.RetryFunc retry, + int triesLeft) + { + var secondsToWait = ParseTooManyRetries(response); + if (secondsToWait != null && (!TooManyRequestsConsumesARetry || triesLeft > 0)) + { + await _sleep(secondsToWait.Value).ConfigureAwait(false); + response = await retry(request).ConfigureAwait(false); + var newTriesLeft = TooManyRequestsConsumesARetry ? triesLeft - 1 : triesLeft; + return await HandleRetryInternally(request, response, retry, newTriesLeft).ConfigureAwait(false); + } + + while (RetryErrorCodes.Contains(response.StatusCode) && triesLeft > 0) + { + await _sleep(RetryAfter).ConfigureAwait(false); + response = await retry(request).ConfigureAwait(false); + return await HandleRetryInternally(request, response, retry, triesLeft - 1).ConfigureAwait(false); + } + + return response; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj index 9021175..f70019e 100644 --- a/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj +++ b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj @@ -1,30 +1,43 @@  - net46;netstandard2.0 - + net5.0;netstandard2.1;netstandard2.0 + 9.0 + enable + SpotifyAPI.Web + SpotifyAPI.Web + Jonas Dellinger + MIT + https://github.com/JohnnyCrazy/SpotifyAPI-NET/ + False + + A Client for Spotify's Web API, written in .NET - - bin\Debug\netstandard2.0\SpotifyAPI.Web.xml - 1701;1702;1705;1591 + For more infos, visit https://github.com/JohnnyCrazy/SpotifyAPI-NET + + + spotify api music .net c# spotify-client + + true + true + snupkg + true + true + 1591 - - bin\Release\netstandard2.0\SpotifyAPI.Web.xml - 1701;1702;1705;1591 + + true - - - - - - + + + None + - - - - + + AllEnabledByDefault + diff --git a/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec deleted file mode 100644 index a154e5b..0000000 --- a/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec +++ /dev/null @@ -1,30 +0,0 @@ - - - - - SpotifyAPI.Web - 3.0.0 - SpotifyAPI.Web - JohnnyCrazy - JohnnyCrazy - https://github.com/JohnnyCrazy/SpotifyAPI-NET/blob/master/LICENSE - https://github.com/JohnnyCrazy/SpotifyAPI-NET/ - false - - An API for Spotify's Web API, written in .NET - - For more infos, visit https://github.com/JohnnyCrazy/SpotifyAPI-NET - - - spotify api music .net c# spotify-client - - - - - - - - - - - diff --git a/Source Files/SpotifyAPI.Web/SpotifyUrls.cs b/Source Files/SpotifyAPI.Web/SpotifyUrls.cs new file mode 100644 index 0000000..b41364c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyUrls.cs @@ -0,0 +1,130 @@ +using System; +namespace SpotifyAPI.Web +{ + public static class SpotifyUrls + { + static private readonly URIParameterFormatProvider _provider = new(); + + public static readonly Uri APIV1 = new("https://api.spotify.com/v1/"); + + public static readonly Uri Authorize = new("https://accounts.spotify.com/authorize"); + + public static readonly Uri OAuthToken = new("https://accounts.spotify.com/api/token"); + + public static Uri Me() => EUri($"me"); + + public static Uri User(string userId) => EUri($"users/{userId}"); + + public static Uri Categories() => EUri($"browse/categories"); + + public static Uri Category(string categoryId) => EUri($"browse/categories/{categoryId}"); + + public static Uri CategoryPlaylists(string categoryId) => EUri($"browse/categories/{categoryId}/playlists"); + + public static Uri Recommendations() => EUri($"recommendations"); + + public static Uri RecommendationGenres() => EUri($"recommendations/available-genre-seeds"); + + public static Uri NewReleases() => EUri($"browse/new-releases"); + + public static Uri FeaturedPlaylists() => EUri($"browse/featured-playlists"); + + public static Uri Show(string showId) => EUri($"shows/{showId}"); + + public static Uri Shows() => EUri($"shows"); + + public static Uri ShowEpisodes(string showId) => EUri($"shows/{showId}/episodes"); + + public static Uri PlaylistTracks(string playlistId) => EUri($"playlists/{playlistId}/tracks"); + + public static Uri UserPlaylists(string userId) => EUri($"users/{userId}/playlists"); + + public static Uri PlaylistImages(string playlistId) => EUri($"playlists/{playlistId}/images"); + + public static Uri Playlist(string playlistId) => EUri($"playlists/{playlistId}"); + + public static Uri CurrentUserPlaylists() => EUri($"me/playlists"); + + public static Uri Search() => EUri($"search"); + + public static Uri CurrentUserFollowerContains() => EUri($"me/following/contains"); + + public static Uri PlaylistFollowersContains(string playlistId) => EUri($"playlists/{playlistId}/followers/contains"); + + public static Uri CurrentUserFollower() => EUri($"me/following"); + + public static Uri PlaylistFollowers(string playlistId) => EUri($"playlists/{playlistId}/followers"); + + public static Uri Tracks() => EUri($"tracks"); + + public static Uri Track(string trackId) => EUri($"tracks/{trackId}"); + + public static Uri AudioAnalysis(string trackId) => EUri($"audio-analysis/{trackId}"); + + public static Uri AudioFeatures(string trackId) => EUri($"audio-features/{trackId}"); + + public static Uri AudioFeatures() => EUri($"audio-features"); + + public static Uri Player() => EUri($"me/player"); + + public static Uri PlayerQueue() => EUri($"me/player/queue"); + + public static Uri PlayerDevices() => EUri($"me/player/devices"); + + public static Uri PlayerCurrentlyPlaying() => EUri($"me/player/currently-playing"); + + public static Uri PlayerRecentlyPlayed() => EUri($"me/player/recently-played"); + + public static Uri PlayerPause() => EUri($"me/player/pause"); + + public static Uri PlayerResume() => EUri($"me/player/play"); + + public static Uri PlayerSeek() => EUri($"me/player/seek"); + + public static Uri PlayerRepeat() => EUri($"me/player/repeat"); + + public static Uri PlayerShuffle() => EUri($"me/player/shuffle"); + + public static Uri PlayerVolume() => EUri($"me/player/volume"); + + public static Uri PlayerNext() => EUri($"me/player/next"); + + public static Uri PlayerPrevious() => EUri($"me/player/previous"); + + public static Uri Albums() => EUri($"albums"); + + public static Uri Album(string albumId) => EUri($"albums/{albumId}"); + + public static Uri AlbumTracks(string albumId) => EUri($"albums/{albumId}/tracks"); + + public static Uri Artists() => EUri($"artists"); + + public static Uri Artist(string artistId) => EUri($"artists/{artistId}"); + + public static Uri ArtistAlbums(string artistId) => EUri($"artists/{artistId}/albums"); + + public static Uri ArtistTopTracks(string artistId) => EUri($"artists/{artistId}/top-tracks"); + + public static Uri ArtistRelatedArtists(string artistId) => EUri($"artists/{artistId}/related-artists"); + + public static Uri PersonalizationTop(string type) => EUri($"me/top/{type}"); + + public static Uri Episode(string episodeId) => EUri($"episodes/{episodeId}"); + + public static Uri Episodes() => EUri($"episodes"); + + public static Uri LibraryAlbumsContains() => EUri($"me/albums/contains"); + + public static Uri LibraryAlbums() => EUri($"me/albums"); + + public static Uri LibraryTracksContains() => EUri($"me/tracks/contains"); + + public static Uri LibraryTracks() => EUri($"me/tracks"); + + public static Uri LibraryShowsContains() => EUri($"me/shows/contains"); + + public static Uri LibraryShows() => EUri($"me/shows"); + + private static Uri EUri(FormattableString path) => new(path.ToString(_provider), UriKind.Relative); + } +} diff --git a/Source Files/SpotifyAPI.Web/SpotifyWebAPI.cs b/Source Files/SpotifyAPI.Web/SpotifyWebAPI.cs deleted file mode 100644 index 1efe238..0000000 --- a/Source Files/SpotifyAPI.Web/SpotifyWebAPI.cs +++ /dev/null @@ -1,2958 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace SpotifyAPI.Web -{ - // ReSharper disable once InconsistentNaming - public sealed class SpotifyWebAPI : IDisposable - { - private readonly SpotifyWebBuilder _builder; - - public SpotifyWebAPI() : this(null) - { - } - - public SpotifyWebAPI(ProxyConfig proxyConfig) - { - _builder = new SpotifyWebBuilder(); - UseAuth = true; - WebClient = new SpotifyWebClient(proxyConfig) - { - JsonSettings = - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - TypeNameHandling = TypeNameHandling.All - } - }; - } - - public void Dispose() - { - WebClient.Dispose(); - GC.SuppressFinalize(this); - } - - #region Configuration - - /// - /// The type of the - /// - public string TokenType { get; set; } - - /// - /// A valid token issued by spotify. Used only when is true - /// - public string AccessToken { get; set; } - - /// - /// If true, an authorization header based on and will be used - /// - public bool UseAuth { get; set; } - - /// - /// A custom WebClient, used for Unit-Testing - /// - public IClient WebClient { get; set; } - - - /// - /// Specifies after how many miliseconds should a failed request be retried. - /// - public int RetryAfter { get; set; } = 50; - - /// - /// Should a failed request (specified by be automatically retried or not. - /// - public bool UseAutoRetry { get; set; } = false; - - /// - /// Maximum number of tries for one failed request. - /// - public int RetryTimes { get; set; } = 10; - - /// - /// Whether a failure of type "Too Many Requests" should use up one of the allocated retry attempts. - /// - public bool TooManyRequestsConsumesARetry { get; set; } = false; - - /// - /// Error codes that will trigger auto-retry if is enabled. - /// - public IEnumerable RetryErrorCodes { get; set; } = new[] { 500, 502, 503 }; - - #endregion Configuration - - #region Search - - /// - /// Get Spotify catalog information about artists, albums, tracks or playlists that match a keyword string. - /// - /// The search query's keywords (and optional field filters and operators), for example q=roadhouse+blues. - /// A list of item types to search across. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first result to return. Default: 0 - /// An ISO 3166-1 alpha-2 country code or the string from_token. - /// - public SearchItem SearchItems(string q, SearchType type, int limit = 20, int offset = 0, string market = "") - { - return DownloadData(_builder.SearchItems(q, type, limit, offset, market)); - } - - /// - /// Get Spotify catalog information about artists, albums, tracks or playlists that match a keyword string asynchronously. - /// - /// The search query's keywords (and optional field filters and operators), for example q=roadhouse+blues. - /// A list of item types to search across. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first result to return. Default: 0 - /// An ISO 3166-1 alpha-2 country code or the string from_token. - /// - public Task SearchItemsAsync(string q, SearchType type, int limit = 20, int offset = 0, string market = "") - { - return DownloadDataAsync(_builder.SearchItems(q, type, limit, offset, market)); - } - - /// - /// Get Spotify catalog information about artists, albums, tracks or playlists that match a keyword string. - /// - /// The search query's keywords (and optional field filters and operators), for example q=roadhouse+blues. (properly escaped) - /// A list of item types to search across. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first result to return. Default: 0 - /// An ISO 3166-1 alpha-2 country code or the string from_token. - /// - public SearchItem SearchItemsEscaped(string q, SearchType type, int limit = 20, int offset = 0, string market = "") - { - string escapedQuery = WebUtility.UrlEncode(q); - return DownloadData(_builder.SearchItems(escapedQuery, type, limit, offset, market)); - } - - /// - /// Get Spotify catalog information about artists, albums, tracks or playlists that match a keyword string asynchronously. - /// - /// The search query's keywords (and optional field filters and operators), for example q=roadhouse+blues. (properly escaped) - /// A list of item types to search across. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first result to return. Default: 0 - /// An ISO 3166-1 alpha-2 country code or the string from_token. - /// - public Task SearchItemsEscapedAsync(string q, SearchType type, int limit = 20, int offset = 0, string market = "") - { - string escapedQuery = WebUtility.UrlEncode(q); - return DownloadDataAsync(_builder.SearchItems(escapedQuery, type, limit, offset, market)); - } - - #endregion Search - - #region Albums - - /// - /// Get Spotify catalog information about an album’s tracks. Optional parameters can be used to limit the number of - /// tracks returned. - /// - /// The Spotify ID for the album. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first track to return. Default: 0 (the first object). - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Paging GetAlbumTracks(string id, int limit = 20, int offset = 0, string market = "") - { - return DownloadData>(_builder.GetAlbumTracks(id, limit, offset, market)); - } - - /// - /// Get Spotify catalog information about an album’s tracks asynchronously. Optional parameters can be used to limit the number of - /// tracks returned. - /// - /// The Spotify ID for the album. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first track to return. Default: 0 (the first object). - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task> GetAlbumTracksAsync(string id, int limit = 20, int offset = 0, string market = "") - { - return DownloadDataAsync>(_builder.GetAlbumTracks(id, limit, offset, market)); - } - - /// - /// Get Spotify catalog information for a single album. - /// - /// The Spotify ID for the album. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public FullAlbum GetAlbum(string id, string market = "") - { - return DownloadData(_builder.GetAlbum(id, market)); - } - - /// - /// Get Spotify catalog information for a single album asynchronously. - /// - /// The Spotify ID for the album. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetAlbumAsync(string id, string market = "") - { - return DownloadDataAsync(_builder.GetAlbum(id, market)); - } - - /// - /// Get Spotify catalog information for multiple albums identified by their Spotify IDs. - /// - /// A list of the Spotify IDs for the albums. Maximum: 20 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public SeveralAlbums GetSeveralAlbums(List ids, string market = "") - { - return DownloadData(_builder.GetSeveralAlbums(ids, market)); - } - - /// - /// Get Spotify catalog information for multiple albums identified by their Spotify IDs asynchrously. - /// - /// A list of the Spotify IDs for the albums. Maximum: 20 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetSeveralAlbumsAsync(List ids, string market = "") - { - return DownloadDataAsync(_builder.GetSeveralAlbums(ids, market)); - } - - #endregion Albums - - #region Artists - - /// - /// Get Spotify catalog information for a single artist identified by their unique Spotify ID. - /// - /// The Spotify ID for the artist. - /// - public FullArtist GetArtist(string id) - { - return DownloadData(_builder.GetArtist(id)); - } - - /// - /// Get Spotify catalog information for a single artist identified by their unique Spotify ID asynchronously. - /// - /// The Spotify ID for the artist. - /// - public Task GetArtistAsync(string id) - { - return DownloadDataAsync(_builder.GetArtist(id)); - } - - /// - /// Get Spotify catalog information about artists similar to a given artist. Similarity is based on analysis of the - /// Spotify community’s listening history. - /// - /// The Spotify ID for the artist. - /// - public SeveralArtists GetRelatedArtists(string id) - { - return DownloadData(_builder.GetRelatedArtists(id)); - } - - /// - /// Get Spotify catalog information about artists similar to a given artist asynchronously. Similarity is based on analysis of the - /// Spotify community’s listening history. - /// - /// The Spotify ID for the artist. - /// - public Task GetRelatedArtistsAsync(string id) - { - return DownloadDataAsync(_builder.GetRelatedArtists(id)); - } - - /// - /// Get Spotify catalog information about an artist’s top tracks by country. - /// - /// The Spotify ID for the artist. - /// The country: an ISO 3166-1 alpha-2 country code. - /// - public SeveralTracks GetArtistsTopTracks(string id, string country) - { - return DownloadData(_builder.GetArtistsTopTracks(id, country)); - } - - /// - /// Get Spotify catalog information about an artist’s top tracks by country asynchronously. - /// - /// The Spotify ID for the artist. - /// The country: an ISO 3166-1 alpha-2 country code. - /// - public Task GetArtistsTopTracksAsync(string id, string country) - { - return DownloadDataAsync(_builder.GetArtistsTopTracks(id, country)); - } - - /// - /// Get Spotify catalog information about an artist’s albums. Optional parameters can be specified in the query string - /// to filter and sort the response. - /// - /// The Spotify ID for the artist. - /// - /// A list of keywords that will be used to filter the response. If not supplied, all album types will - /// be returned - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first album to return. Default: 0 - /// - /// An ISO 3166-1 alpha-2 country code. Supply this parameter to limit the response to one particular - /// geographical market - /// - /// - public Paging GetArtistsAlbums(string id, AlbumType type = AlbumType.All, int limit = 20, int offset = 0, string market = "") - { - return DownloadData>(_builder.GetArtistsAlbums(id, type, limit, offset, market)); - } - - /// - /// Get Spotify catalog information about an artist’s albums asynchronously. Optional parameters can be specified in the query string - /// to filter and sort the response. - /// - /// The Spotify ID for the artist. - /// - /// A list of keywords that will be used to filter the response. If not supplied, all album types will - /// be returned - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first album to return. Default: 0 - /// - /// An ISO 3166-1 alpha-2 country code. Supply this parameter to limit the response to one particular - /// geographical market - /// - /// - public Task> GetArtistsAlbumsAsync(string id, AlbumType type = AlbumType.All, int limit = 20, int offset = 0, string market = "") - { - return DownloadDataAsync>(_builder.GetArtistsAlbums(id, type, limit, offset, market)); - } - - /// - /// Get Spotify catalog information for several artists based on their Spotify IDs. - /// - /// A list of the Spotify IDs for the artists. Maximum: 50 IDs. - /// - public SeveralArtists GetSeveralArtists(List ids) - { - return DownloadData(_builder.GetSeveralArtists(ids)); - } - - /// - /// Get Spotify catalog information for several artists based on their Spotify IDs asynchronously. - /// - /// A list of the Spotify IDs for the artists. Maximum: 50 IDs. - /// - public Task GetSeveralArtistsAsync(List ids) - { - return DownloadDataAsync(_builder.GetSeveralArtists(ids)); - } - - #endregion Artists - - #region Browse - - /// - /// Get a list of Spotify featured playlists (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// - /// The desired language, consisting of a lowercase ISO 639 language code and an uppercase ISO 3166-1 - /// alpha-2 country code, joined by an underscore. - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// A timestamp in ISO 8601 format - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// AUTH NEEDED - public FeaturedPlaylists GetFeaturedPlaylists(string locale = "", string country = "", DateTime timestamp = default(DateTime), int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetFeaturedPlaylists"); - return DownloadData(_builder.GetFeaturedPlaylists(locale, country, timestamp, limit, offset)); - } - - /// - /// Get a list of Spotify featured playlists asynchronously (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// - /// The desired language, consisting of a lowercase ISO 639 language code and an uppercase ISO 3166-1 - /// alpha-2 country code, joined by an underscore. - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// A timestamp in ISO 8601 format - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// AUTH NEEDED - public Task GetFeaturedPlaylistsAsync(string locale = "", string country = "", DateTime timestamp = default(DateTime), int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetFeaturedPlaylists"); - return DownloadDataAsync(_builder.GetFeaturedPlaylists(locale, country, timestamp, limit, offset)); - } - - /// - /// Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public NewAlbumReleases GetNewAlbumReleases(string country = "", int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetNewAlbumReleases"); - return DownloadData(_builder.GetNewAlbumReleases(country, limit, offset)); - } - - /// - /// Get a list of new album releases featured in Spotify asynchronously (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public Task GetNewAlbumReleasesAsync(string country = "", int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetNewAlbumReleases"); - return DownloadDataAsync(_builder.GetNewAlbumReleases(country, limit, offset)); - } - - /// - /// Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). - /// - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want to narrow the - /// list of returned categories to those relevant to a particular country - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// The maximum number of categories to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 (the first object). - /// - /// AUTH NEEDED - public CategoryList GetCategories(string country = "", string locale = "", int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetCategories"); - return DownloadData(_builder.GetCategories(country, locale, limit, offset)); - } - - /// - /// Get a list of categories used to tag items in Spotify asynchronously (on, for example, the Spotify player’s “Browse” tab). - /// - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want to narrow the - /// list of returned categories to those relevant to a particular country - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// The maximum number of categories to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 (the first object). - /// - /// AUTH NEEDED - public Task GetCategoriesAsync(string country = "", string locale = "", int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetCategories"); - return DownloadDataAsync(_builder.GetCategories(country, locale, limit, offset)); - } - - /// - /// Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). - /// - /// The Spotify category ID for the category. - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter to ensure that the category - /// exists for a particular country. - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// - /// AUTH NEEDED - public Category GetCategory(string categoryId, string country = "", string locale = "") - { - return DownloadData(_builder.GetCategory(categoryId, country, locale)); - } - - /// - /// Get a single category used to tag items in Spotify asynchronously (on, for example, the Spotify player’s “Browse” tab). - /// - /// The Spotify category ID for the category. - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter to ensure that the category - /// exists for a particular country. - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// - /// AUTH NEEDED - public Task GetCategoryAsync(string categoryId, string country = "", string locale = "") - { - return DownloadDataAsync(_builder.GetCategory(categoryId, country, locale)); - } - - /// - /// Get a list of Spotify playlists tagged with a particular category. - /// - /// The Spotify category ID for the category. - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public CategoryPlaylist GetCategoryPlaylists(string categoryId, string country = "", int limit = 20, int offset = 0) - { - return DownloadData(_builder.GetCategoryPlaylists(categoryId, country, limit, offset)); - } - - /// - /// Get a list of Spotify playlists tagged with a particular category asynchronously. - /// - /// The Spotify category ID for the category. - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public Task GetCategoryPlaylistsAsync(string categoryId, string country = "", int limit = 20, int offset = 0) - { - return DownloadDataAsync(_builder.GetCategoryPlaylists(categoryId, country, limit, offset)); - } - - /// - /// Create a playlist-style listening experience based on seed artists, tracks and genres. - /// - /// A comma separated list of Spotify IDs for seed artists. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of any genres in the set of available genre seeds. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of Spotify IDs for a seed track. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// Tracks with the attribute values nearest to the target values will be preferred. - /// For each tunable track attribute, a hard floor on the selected track attribute’s value can be provided - /// For each tunable track attribute, a hard ceiling on the selected track attribute’s value can be provided - /// The target size of the list of recommended tracks. Default: 20. Minimum: 1. Maximum: 100. - /// For seeds with unusually small pools or when highly restrictive filtering is applied, it may be impossible to generate the requested number of recommended tracks. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// Because min_*, max_* and target_* are applied to pools before relinking, the generated results may not precisely match the filters applied. - /// - /// AUTH NEEDED - public Recommendations GetRecommendations(List artistSeed = null, List genreSeed = null, List trackSeed = null, - TuneableTrack target = null, TuneableTrack min = null, TuneableTrack max = null, int limit = 20, string market = "") - { - return DownloadData(_builder.GetRecommendations(artistSeed, genreSeed, trackSeed, target, min, max, limit, market)); - } - - /// - /// Create a playlist-style listening experience based on seed artists, tracks and genres asynchronously. - /// - /// A comma separated list of Spotify IDs for seed artists. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of any genres in the set of available genre seeds. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of Spotify IDs for a seed track. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// Tracks with the attribute values nearest to the target values will be preferred. - /// For each tunable track attribute, a hard floor on the selected track attribute’s value can be provided - /// For each tunable track attribute, a hard ceiling on the selected track attribute’s value can be provided - /// The target size of the list of recommended tracks. Default: 20. Minimum: 1. Maximum: 100. - /// For seeds with unusually small pools or when highly restrictive filtering is applied, it may be impossible to generate the requested number of recommended tracks. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// Because min_*, max_* and target_* are applied to pools before relinking, the generated results may not precisely match the filters applied. - /// - /// AUTH NEEDED - public Task GetRecommendationsAsync(List artistSeed = null, List genreSeed = null, List trackSeed = null, - TuneableTrack target = null, TuneableTrack min = null, TuneableTrack max = null, int limit = 20, string market = "") - { - return DownloadDataAsync(_builder.GetRecommendations(artistSeed, genreSeed, trackSeed, target, min, max, limit, market)); - } - - /// - /// Retrieve a list of available genres seed parameter values for recommendations. - /// - /// - /// AUTH NEEDED - public RecommendationSeedGenres GetRecommendationSeedsGenres() - { - return DownloadData(_builder.GetRecommendationSeedsGenres()); - } - - /// - /// Retrieve a list of available genres seed parameter values for recommendations asynchronously. - /// - /// - /// AUTH NEEDED - public Task GetRecommendationSeedsGenresAsync() - { - return DownloadDataAsync(_builder.GetRecommendationSeedsGenres()); - } - - #endregion Browse - - #region Follow - - /// - /// Get the current user’s followed artists. - /// - /// The ID type: currently only artist is supported. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The last artist ID retrieved from the previous request. - /// - /// AUTH NEEDED - public FollowedArtists GetFollowedArtists(FollowType followType, int limit = 20, string after = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetFollowedArtists"); - return DownloadData(_builder.GetFollowedArtists(limit, after)); - } - - /// - /// Get the current user’s followed artists asynchronously. - /// - /// The ID type: currently only artist is supported. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The last artist ID retrieved from the previous request. - /// - /// AUTH NEEDED - public Task GetFollowedArtistsAsync(FollowType followType, int limit = 20, string after = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetFollowedArtists"); - return DownloadDataAsync(_builder.GetFollowedArtists(limit, after)); - } - - /// - /// Add the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs - /// - /// AUTH NEEDED - public ErrorResponse Follow(FollowType followType, List ids) - { - JObject ob = new JObject - { - {"ids", new JArray(ids)} - }; - return UploadData(_builder.Follow(followType), ob.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Add the current user as a follower of one or more artists or other Spotify users asynchronously. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs - /// - /// AUTH NEEDED - public async Task FollowAsync(FollowType followType, List ids) - { - JObject ob = new JObject - { - {"ids", new JArray(ids)} - }; - return await UploadDataAsync(_builder.Follow(followType), - ob.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Add the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public ErrorResponse Follow(FollowType followType, string id) - { - return Follow(followType, new List { id }); - } - - /// - /// Add the current user as a follower of one or more artists or other Spotify users asynchronously. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public Task FollowAsync(FollowType followType, string id) - { - return FollowAsync(followType, new List { id }); - } - - /// - /// Remove the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs - /// - /// AUTH NEEDED - public ErrorResponse Unfollow(FollowType followType, List ids) - { - JObject ob = new JObject - { - {"ids", new JArray(ids)} - }; - return UploadData(_builder.Unfollow(followType), ob.ToString(Formatting.None), "DELETE") ?? new ErrorResponse(); - } - - /// - /// Remove the current user as a follower of one or more artists or other Spotify users asynchronously. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs - /// - /// AUTH NEEDED - public async Task UnfollowAsync(FollowType followType, List ids) - { - JObject ob = new JObject - { - {"ids", new JArray(ids)} - }; - return await UploadDataAsync(_builder.Unfollow(followType), ob.ToString(Formatting.None), "DELETE").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Remove the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public ErrorResponse Unfollow(FollowType followType, string id) - { - return Unfollow(followType, new List { id }); - } - - /// - /// Remove the current user as a follower of one or more artists or other Spotify users asynchronously. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public Task UnfollowAsync(FollowType followType, string id) - { - return UnfollowAsync(followType, new List { id }); - } - - /// - /// Check to see if the current user is following one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs to check - /// - /// AUTH NEEDED - public ListResponse IsFollowing(FollowType followType, List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for IsFollowing"); - - string url = _builder.IsFollowing(followType, ids); - return DownloadList(url); - } - - - - /// - /// Check to see if the current user is following one or more artists or other Spotify users asynchronously. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs to check - /// - /// AUTH NEEDED - public Task> IsFollowingAsync(FollowType followType, List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for IsFollowing"); - - string url = _builder.IsFollowing(followType, ids); - return DownloadListAsync(url); - } - - /// - /// Check to see if the current user is following one artist or another Spotify user. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public ListResponse IsFollowing(FollowType followType, string id) - { - return IsFollowing(followType, new List { id }); - } - - /// - /// Check to see if the current user is following one artist or another Spotify user asynchronously. - /// - /// The ID type: either artist or user. - /// Artists or the Users Spotify ID - /// - /// AUTH NEEDED - public Task> IsFollowingAsync(FollowType followType, string id) - { - return IsFollowingAsync(followType, new List { id }); - } - - /// - /// Add the current user as a follower of a playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// - /// The Spotify ID of the playlist. Any playlist can be followed, regardless of its public/private - /// status, as long as you know its playlist ID. - /// - /// - /// If true the playlist will be included in user's public playlists, if false it will remain - /// private. - /// - /// - /// AUTH NEEDED - public ErrorResponse FollowPlaylist(string ownerId, string playlistId, bool showPublic = true) - { - JObject body = new JObject - { - {"public", showPublic} - }; - return UploadData(_builder.FollowPlaylist(ownerId, playlistId, showPublic), body.ToString(Formatting.None), "PUT"); - } - - /// - /// Add the current user as a follower of a playlist asynchronously. - /// - /// The Spotify user ID of the person who owns the playlist. - /// - /// The Spotify ID of the playlist. Any playlist can be followed, regardless of its public/private - /// status, as long as you know its playlist ID. - /// - /// - /// If true the playlist will be included in user's public playlists, if false it will remain - /// private. - /// - /// - /// AUTH NEEDED - public Task FollowPlaylistAsync(string ownerId, string playlistId, bool showPublic = true) - { - JObject body = new JObject - { - {"public", showPublic} - }; - return UploadDataAsync(_builder.FollowPlaylist(ownerId, playlistId, showPublic), body.ToString(Formatting.None), "PUT"); - } - - /// - /// Remove the current user as a follower of a playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist that is to be no longer followed. - /// - /// AUTH NEEDED - public ErrorResponse UnfollowPlaylist(string ownerId, string playlistId) - { - return UploadData(_builder.UnfollowPlaylist(ownerId, playlistId), "", "DELETE"); - } - - /// - /// Remove the current user as a follower of a playlist asynchronously. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist that is to be no longer followed. - /// - /// AUTH NEEDED - public Task UnfollowPlaylistAsync(string ownerId, string playlistId) - { - return UploadDataAsync(_builder.UnfollowPlaylist(ownerId, playlistId), "", "DELETE"); - } - - /// - /// Check to see if one or more Spotify users are following a specified playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist. - /// A list of Spotify User IDs - /// - /// AUTH NEEDED - public ListResponse IsFollowingPlaylist(string ownerId, string playlistId, List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for IsFollowingPlaylist"); - - string url = _builder.IsFollowingPlaylist(ownerId, playlistId, ids); - return DownloadList(url); - } - - /// - /// Check to see if one or more Spotify users are following a specified playlist asynchronously. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist. - /// A list of Spotify User IDs - /// - /// AUTH NEEDED - public Task> IsFollowingPlaylistAsync(string ownerId, string playlistId, List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for IsFollowingPlaylist"); - - string url = _builder.IsFollowingPlaylist(ownerId, playlistId, ids); - return DownloadListAsync(url); - } - - /// - /// Check to see if one or more Spotify users are following a specified playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist. - /// A Spotify User ID - /// - /// AUTH NEEDED - public ListResponse IsFollowingPlaylist(string ownerId, string playlistId, string id) - { - return IsFollowingPlaylist(ownerId, playlistId, new List { id }); - } - - /// - /// Check to see if one or more Spotify users are following a specified playlist asynchronously. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist. - /// A Spotify User ID - /// - /// AUTH NEEDED - public Task> IsFollowingPlaylistAsync(string ownerId, string playlistId, string id) - { - return IsFollowingPlaylistAsync(ownerId, playlistId, new List { id }); - } - - #endregion Follow - - #region Library - - /// - /// Save one or more tracks to the current user’s “Your Music” library. - /// - /// A list of the Spotify IDs - /// - /// AUTH NEEDED - public ErrorResponse SaveTracks(List ids) - { - JArray array = new JArray(ids); - return UploadData(_builder.SaveTracks(), array.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Save one or more tracks to the current user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs - /// - /// AUTH NEEDED - public async Task SaveTracksAsync(List ids) - { - JArray array = new JArray(ids); - return await UploadDataAsync(_builder.SaveTracks(), array.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Save one track to the current user’s “Your Music” library. - /// - /// A Spotify ID - /// - /// AUTH NEEDED - public ErrorResponse SaveTrack(string id) - { - return SaveTracks(new List { id }); - } - - /// - /// Save one track to the current user’s “Your Music” library asynchronously. - /// - /// A Spotify ID - /// - /// AUTH NEEDED - public Task SaveTrackAsync(string id) - { - return SaveTracksAsync(new List { id }); - } - - /// - /// Get a list of the songs saved in the current Spotify user’s “Your Music” library. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Paging GetSavedTracks(int limit = 20, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetSavedTracks"); - return DownloadData>(_builder.GetSavedTracks(limit, offset, market)); - } - - /// - /// Get a list of the songs saved in the current Spotify user’s “Your Music” library asynchronously. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Task> GetSavedTracksAsync(int limit = 20, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetSavedTracks"); - return DownloadDataAsync>(_builder.GetSavedTracks(limit, offset, market)); - } - - /// - /// Remove one or more tracks from the current user’s “Your Music” library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public ErrorResponse RemoveSavedTracks(List ids) - { - JArray array = new JArray(ids); - return UploadData(_builder.RemoveSavedTracks(), array.ToString(Formatting.None), "DELETE") ?? new ErrorResponse(); - } - - /// - /// Remove one or more tracks from the current user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public async Task RemoveSavedTracksAsync(List ids) - { - JArray array = new JArray(ids); - return await UploadDataAsync(_builder.RemoveSavedTracks(), array.ToString(Formatting.None), "DELETE").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public ListResponse CheckSavedTracks(List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for CheckSavedTracks"); - - string url = _builder.CheckSavedTracks(ids); - return DownloadList(url); - } - - /// - /// Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public Task> CheckSavedTracksAsync(List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for CheckSavedTracks"); - string url = _builder.CheckSavedTracks(ids); - return DownloadListAsync(url); - } - - /// - /// Save one or more albums to the current user’s “Your Music” library. - /// - /// A list of the Spotify IDs - /// - /// AUTH NEEDED - public ErrorResponse SaveAlbums(List ids) - { - JArray array = new JArray(ids); - return UploadData(_builder.SaveAlbums(), array.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Save one or more albums to the current user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs - /// - /// AUTH NEEDED - public async Task SaveAlbumsAsync(List ids) - { - JArray array = new JArray(ids); - return await UploadDataAsync(_builder.SaveAlbums(), array.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Save one album to the current user’s “Your Music” library. - /// - /// A Spotify ID - /// - /// AUTH NEEDED - public ErrorResponse SaveAlbum(string id) - { - return SaveAlbums(new List { id }); - } - - /// - /// Save one album to the current user’s “Your Music” library asynchronously. - /// - /// A Spotify ID - /// - /// AUTH NEEDED - public Task SaveAlbumAsync(string id) - { - return SaveAlbumsAsync(new List { id }); - } - - /// - /// Get a list of the albums saved in the current Spotify user’s “Your Music” library. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Paging GetSavedAlbums(int limit = 20, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetSavedAlbums"); - return DownloadData>(_builder.GetSavedAlbums(limit, offset, market)); - } - - /// - /// Get a list of the albums saved in the current Spotify user’s “Your Music” library asynchronously. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Task> GetSavedAlbumsAsync(int limit = 20, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetSavedAlbumsAsync"); - return DownloadDataAsync>(_builder.GetSavedAlbums(limit, offset, market)); - } - - /// - /// Remove one or more albums from the current user’s “Your Music” library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public ErrorResponse RemoveSavedAlbums(List ids) - { - JArray array = new JArray(ids); - return UploadData(_builder.RemoveSavedAlbums(), array.ToString(Formatting.None), "DELETE") ?? new ErrorResponse(); - } - - /// - /// Remove one or more albums from the current user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public async Task RemoveSavedAlbumsAsync(List ids) - { - JArray array = new JArray(ids); - return await UploadDataAsync(_builder.RemoveSavedAlbums(), array.ToString(Formatting.None), "DELETE").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Check if one or more albums is already saved in the current Spotify user’s “Your Music” library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public ListResponse CheckSavedAlbums(List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for CheckSavedTracks"); - - string url = _builder.CheckSavedAlbums(ids); - return DownloadList(url); - } - - /// - /// Check if one or more albums is already saved in the current Spotify user’s “Your Music” library asynchronously. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public Task> CheckSavedAlbumsAsync(List ids) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for CheckSavedAlbumsAsync"); - string url = _builder.CheckSavedAlbums(ids); - return DownloadListAsync(url); - } - - #endregion Library - - #region Personalization - - /// - /// Get the current user’s top tracks based on calculated affinity. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public Paging GetUsersTopTracks(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - return DownloadData>(_builder.GetUsersTopTracks(timeRange, limit, offest)); - } - - /// - /// Get the current user’s top tracks based on calculated affinity asynchronously. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public Task> GetUsersTopTracksAsync(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - return DownloadDataAsync>(_builder.GetUsersTopTracks(timeRange, limit, offest)); - } - - /// - /// Get the current user’s top artists based on calculated affinity. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public Paging GetUsersTopArtists(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - return DownloadData>(_builder.GetUsersTopArtists(timeRange, limit, offest)); - } - - /// - /// Get the current user’s top artists based on calculated affinity asynchronously. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public Task> GetUsersTopArtistsAsync(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - return DownloadDataAsync>(_builder.GetUsersTopArtists(timeRange, limit, offest)); - } - - /// - /// Get tracks from the current user’s recent play history. - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position. If after is specified, before must not be specified. - /// A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position. If before is specified, after must not be specified. - /// - /// AUTH NEEDED - public CursorPaging GetUsersRecentlyPlayedTracks(int limit = 20, DateTime? after = null, - DateTime? before = null) - { - return DownloadData>(_builder.GetUsersRecentlyPlayedTracks(limit, after, before)); - } - - /// - /// Get tracks from the current user’s recent play history asynchronously - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position. If after is specified, before must not be specified. - /// A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position. If before is specified, after must not be specified. - /// - /// AUTH NEEDED - public Task> GetUsersRecentlyPlayedTracksAsync(int limit = 20, DateTime? after = null, - DateTime? before = null) - { - return DownloadDataAsync>(_builder.GetUsersRecentlyPlayedTracks(limit, after, before)); - } - - #endregion - - #region Playlists - - /// - /// Get a list of the playlists owned or followed by a Spotify user. - /// - /// The user's Spotify user ID. - /// The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first playlist to return. Default: 0 (the first object) - /// - /// AUTH NEEDED - public Paging GetUserPlaylists(string userId, int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetUserPlaylists"); - return DownloadData>(_builder.GetUserPlaylists(userId, limit, offset)); - } - - /// - /// Get a list of the playlists owned or followed by a Spotify user asynchronously. - /// - /// The user's Spotify user ID. - /// The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first playlist to return. Default: 0 (the first object) - /// - /// AUTH NEEDED - public Task> GetUserPlaylistsAsync(string userId, int limit = 20, int offset = 0) - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetUserPlaylists"); - return DownloadDataAsync>(_builder.GetUserPlaylists(userId, limit, offset)); - } - - /// - /// Get a playlist owned by a Spotify user. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - [Obsolete("Calling GetPlaylist with a userId is deprecated, remove the parameter")] - public FullPlaylist GetPlaylist(string userId, string playlistId, string fields = "", string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylist"); - return DownloadData(_builder.GetPlaylist(userId, playlistId, fields, market)); - } - - /// - /// Get a playlist owned by a Spotify user. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public FullPlaylist GetPlaylist(string playlistId, string fields = "", string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylist"); - return DownloadData(_builder.GetPlaylist(playlistId, fields, market)); - } - - /// - /// Get a playlist owned by a Spotify user asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - [Obsolete("Calling GetPlaylist with a userId is deprecated, remove the parameter")] - public Task GetPlaylistAsync(string userId, string playlistId, string fields = "", string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylist"); - return DownloadDataAsync(_builder.GetPlaylist(userId, playlistId, fields, market)); - } - - /// - /// Get a playlist owned by a Spotify user asynchronously. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Task GetPlaylistAsync(string playlistId, string fields = "", string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylist"); - return DownloadDataAsync(_builder.GetPlaylist(playlistId, fields, market)); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - [Obsolete("Calling GetPlaylistTracks with a userId is deprecated, remove the parameter")] - public Paging GetPlaylistTracks(string userId, string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylistTracks"); - return DownloadData>(_builder.GetPlaylistTracks(userId, playlistId, fields, limit, offset, market)); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public Paging GetPlaylistTracks(string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylistTracks"); - return DownloadData>(_builder.GetPlaylistTracks(playlistId, fields, limit, offset, market)); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user asyncronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - [Obsolete("Calling GetPlaylistTracks with a userId is deprecated, remove the parameter")] - public Task> GetPlaylistTracksAsync(string userId, string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylistTracks"); - return DownloadDataAsync>(_builder.GetPlaylistTracks(userId, playlistId, fields, limit, offset, market)); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user asyncronously. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - [Obsolete("Calling GetPlaylistTracks with a userId is deprecated, remove the parameter")] - public Task> GetPlaylistTracksAsync(string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPlaylistTracks"); - return DownloadDataAsync>(_builder.GetPlaylistTracks(playlistId, fields, limit, offset, market)); - } - - /// - /// Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) - /// - /// The user's Spotify user ID. - /// - /// The name for the new playlist, for example "Your Coolest Playlist". This name does not need - /// to be unique. - /// - /// - /// default true. If true the playlist will be public, if false it will be private. To be able to - /// create private playlists, the user must have granted the playlist-modify-private scope. - /// - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. - /// Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - public FullPlaylist CreatePlaylist(string userId, string playlistName, bool isPublic = true, bool isCollaborative = false, string playlistDescription = "") - { - JObject body = new JObject - { - {"name", playlistName}, - {"public", isPublic}, - {"collaborative", isCollaborative}, - {"description", playlistDescription} - }; - return UploadData(_builder.CreatePlaylist(userId, playlistName, isPublic), body.ToString(Formatting.None)); - } - - /// - /// Create a playlist for a Spotify user asynchronously. (The playlist will be empty until you add tracks.) - /// - /// The user's Spotify user ID. - /// - /// The name for the new playlist, for example "Your Coolest Playlist". This name does not need - /// to be unique. - /// - /// - /// default true. If true the playlist will be public, if false it will be private. To be able to - /// create private playlists, the user must have granted the playlist-modify-private scope. - /// - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. - /// Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - public Task CreatePlaylistAsync(string userId, string playlistName, bool isPublic = true, bool isCollaborative = false, string playlistDescription = "") - { - JObject body = new JObject - { - {"name", playlistName}, - {"public", isPublic}, - {"collaborative", isCollaborative}, - {"description", playlistDescription} - }; - return UploadDataAsync(_builder.CreatePlaylist(userId, playlistName, isPublic), body.ToString(Formatting.None)); - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The new name for the playlist, for example "My New Playlist Title". - /// If true the playlist will be public, if false it will be private. - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. - /// Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - [Obsolete("Calling UpdatePlaylist with a userId is deprecated, remove the parameter")] - public ErrorResponse UpdatePlaylist(string userId, string playlistId, string newName = null, bool? newPublic = null, bool? newCollaborative = null, string newDescription = null) - { - JObject body = new JObject(); - if (newName != null) - body.Add("name", newName); - if (newPublic != null) - body.Add("public", newPublic); - if (newCollaborative != null) - body.Add("collaborative", newCollaborative); - if (newDescription != null) - body.Add("description", newDescription); - return UploadData(_builder.UpdatePlaylist(userId, playlistId), body.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The Spotify ID for the playlist. - /// The new name for the playlist, for example "My New Playlist Title". - /// If true the playlist will be public, if false it will be private. - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. - /// Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - public ErrorResponse UpdatePlaylist(string playlistId, string newName = null, bool? newPublic = null, bool? newCollaborative = null, string newDescription = null) - { - JObject body = new JObject(); - if (newName != null) - body.Add("name", newName); - if (newPublic != null) - body.Add("public", newPublic); - if (newCollaborative != null) - body.Add("collaborative", newCollaborative); - if (newDescription != null) - body.Add("description", newDescription); - return UploadData(_builder.UpdatePlaylist(playlistId), body.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state asynchronously. (The user must, of course, own the playlist.) - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The new name for the playlist, for example "My New Playlist Title". - /// If true the playlist will be public, if false it will be private. - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - [Obsolete("Calling UpdatePlaylist with a userId is deprecated, remove the parameter")] - public async Task UpdatePlaylistAsync(string userId, string playlistId, string newName = null, bool? newPublic = null, bool? newCollaborative = null, string newDescription = null) - { - JObject body = new JObject(); - if (newName != null) - body.Add("name", newName); - if (newPublic != null) - body.Add("public", newPublic); - if (newCollaborative != null) - body.Add("collaborative", newCollaborative); - if (newDescription != null) - body.Add("description", newDescription); - return await UploadDataAsync(_builder.UpdatePlaylist(userId, playlistId), body.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state asynchronously. (The user must, of course, own the playlist.) - /// - /// The Spotify ID for the playlist. - /// The new name for the playlist, for example "My New Playlist Title". - /// If true the playlist will be public, if false it will be private. - /// If true the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. Note: You can only set collaborative to true on non-public playlists. - /// Value for playlist description as displayed in Spotify Clients and in the Web API. - /// - /// AUTH NEEDED - public async Task UpdatePlaylistAsync(string playlistId, string newName = null, bool? newPublic = null, bool? newCollaborative = null, string newDescription = null) - { - JObject body = new JObject(); - if (newName != null) - body.Add("name", newName); - if (newPublic != null) - body.Add("public", newPublic); - if (newCollaborative != null) - body.Add("collaborative", newCollaborative); - if (newDescription != null) - body.Add("description", newDescription); - return await UploadDataAsync(_builder.UpdatePlaylist(playlistId), body.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The image as a base64 encoded string - /// - /// AUTH NEEDED - public ErrorResponse UploadPlaylistImage(string userId, string playlistId, string base64EncodedJpgImage) - { - return UploadData(_builder.UploadPlaylistImage(userId, playlistId), base64EncodedJpgImage, "PUT") ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state asynchronously. (The user must, of course, own the playlist.) - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The image as a base64 encoded string - /// - /// AUTH NEEDED - public async Task UploadPlaylistImageAsync(string userId, string playlistId, string base64EncodedJpgImage) - { - return await UploadDataAsync(_builder.UploadPlaylistImage(userId, playlistId), base64EncodedJpgImage, "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The Spotify ID for the playlist. - /// The image as a base64 encoded string - /// - /// AUTH NEEDED - public ErrorResponse UploadPlaylistImage(string playlistId, string base64EncodedJpgImage) - { - return UploadData(_builder.UploadPlaylistImage(playlistId), base64EncodedJpgImage, "PUT") ?? new ErrorResponse(); - } - - /// - /// Change a playlist’s name and public/private state asynchronously. (The user must, of course, own the playlist.) - /// - /// The Spotify ID for the playlist. - /// The image as a base64 encoded string - /// - /// AUTH NEEDED - public async Task UploadPlaylistImageAsync(string playlistId, string base64EncodedJpgImage) - { - return await UploadDataAsync(_builder.UploadPlaylistImage(playlistId), - base64EncodedJpgImage, "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Replace all the tracks in a playlist, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to set. A maximum of 100 tracks can be set in one request. - /// - /// AUTH NEEDED - [Obsolete("Calling ReplacePlaylistTracks with a userId is deprecated, remove the parameter")] - public ErrorResponse ReplacePlaylistTracks(string userId, string playlistId, List uris) - { - JObject body = new JObject - { - {"uris", new JArray(uris.Take(100))} - }; - return UploadData(_builder.ReplacePlaylistTracks(userId, playlistId), body.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Replace all the tracks in a playlist, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to set. A maximum of 100 tracks can be set in one request. - /// - /// AUTH NEEDED - public ErrorResponse ReplacePlaylistTracks(string playlistId, List uris) - { - JObject body = new JObject - { - {"uris", new JArray(uris.Take(100))} - }; - return UploadData(_builder.ReplacePlaylistTracks(playlistId), body.ToString(Formatting.None), "PUT") ?? new ErrorResponse(); - } - - /// - /// Replace all the tracks in a playlist asynchronously, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to set. A maximum of 100 tracks can be set in one request. - /// - /// AUTH NEEDED - [Obsolete("Calling ReplacePlaylistTracks with a userId is deprecated, remove the parameter")] - public async Task ReplacePlaylistTracksAsync(string userId, string playlistId, List uris) - { - JObject body = new JObject - { - {"uris", new JArray(uris.Take(100))} - }; - return await UploadDataAsync(_builder.ReplacePlaylistTracks(userId, playlistId), body.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Replace all the tracks in a playlist asynchronously, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to set. A maximum of 100 tracks can be set in one request. - /// - /// AUTH NEEDED - public async Task ReplacePlaylistTracksAsync(string playlistId, List uris) - { - JObject body = new JObject - { - {"uris", new JArray(uris.Take(100))} - }; - return await UploadDataAsync(_builder.ReplacePlaylistTracks(playlistId), body.ToString(Formatting.None), "PUT").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Remove one or more tracks from a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - [Obsolete("Calling RemovePlaylistTracks with a userId is deprecated, remove the parameter")] - public ErrorResponse RemovePlaylistTracks(string userId, string playlistId, List uris) - { - JObject body = new JObject - { - {"tracks", JArray.FromObject(uris.Take(100))} - }; - return UploadData(_builder.RemovePlaylistTracks(userId, playlistId, uris), body.ToString(Formatting.None), "DELETE") ?? new ErrorResponse(); - } - - /// - /// Remove one or more tracks from a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - public ErrorResponse RemovePlaylistTracks(string playlistId, List uris) - { - JObject body = new JObject - { - {"tracks", JArray.FromObject(uris.Take(100))} - }; - return UploadData(_builder.RemovePlaylistTracks(playlistId, uris), body.ToString(Formatting.None), "DELETE") ?? new ErrorResponse(); - } - - /// - /// Remove one or more tracks from a user’s playlist asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - [Obsolete("Calling RemovePlaylistTracks with a userId is deprecated, remove the parameter")] - public async Task RemovePlaylistTracksAsync(string userId, string playlistId, List uris) - { - JObject body = new JObject - { - {"tracks", JArray.FromObject(uris.Take(100))} - }; - return await UploadDataAsync(_builder.RemovePlaylistTracks(userId, playlistId, uris), body.ToString(Formatting.None), "DELETE").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Remove one or more tracks from a user’s playlist asynchronously. - /// - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - public async Task RemovePlaylistTracksAsync(string playlistId, List uris) - { - JObject body = new JObject - { - {"tracks", JArray.FromObject(uris.Take(100))} - }; - return await UploadDataAsync(_builder.RemovePlaylistTracks(playlistId, uris), body.ToString(Formatting.None), "DELETE").ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Remove a track from a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// Spotify URI - /// - /// AUTH NEEDED - [Obsolete("Calling RemovePlaylistTrack with a userId is deprecated, remove the parameter")] - public ErrorResponse RemovePlaylistTrack(string userId, string playlistId, DeleteTrackUri uri) - { - return RemovePlaylistTracks(playlistId, new List { uri }); - } - - /// - /// Remove a track from a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// Spotify URI - /// - /// AUTH NEEDED - public ErrorResponse RemovePlaylistTrack(string playlistId, DeleteTrackUri uri) - { - return RemovePlaylistTracks(playlistId, new List { uri }); - } - - /// - /// Remove a track from a user’s playlist asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// Spotify URI - /// - /// AUTH NEEDED - [Obsolete("Calling RemovePlaylistTrack with a userId is deprecated, remove the parameter")] - public Task RemovePlaylistTrackAsync(string userId, string playlistId, DeleteTrackUri uri) - { - return RemovePlaylistTracksAsync(playlistId, new List { uri }); - } - - /// - /// Remove a track from a user’s playlist asynchronously. - /// - /// The Spotify ID for the playlist. - /// Spotify URI - /// - /// AUTH NEEDED - public Task RemovePlaylistTrackAsync(string playlistId, DeleteTrackUri uri) - { - return RemovePlaylistTracksAsync(playlistId, new List { uri }); - } - - /// - /// Add one or more tracks to a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - [Obsolete("Calling AddPlaylistTracks with a userId is deprecated, remove the parameter")] - public ErrorResponse AddPlaylistTracks(string userId, string playlistId, List uris, int? position = null) - { - JObject body = new JObject - { - {"uris", JArray.FromObject(uris.Take(100))} - }; - return UploadData(_builder.AddPlaylistTracks(userId, playlistId, uris, position), body.ToString(Formatting.None)) ?? new ErrorResponse(); - } - - /// - /// Add one or more tracks to a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public ErrorResponse AddPlaylistTracks(string playlistId, List uris, int? position = null) - { - JObject body = new JObject - { - {"uris", JArray.FromObject(uris.Take(100))} - }; - return UploadData(_builder.AddPlaylistTracks(playlistId, uris, position), body.ToString(Formatting.None)) ?? new ErrorResponse(); - } - - /// - /// Add one or more tracks to a user’s playlist asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - [Obsolete("Calling AddPlaylistTracks with a userId is deprecated, remove the parameter")] - public async Task AddPlaylistTracksAsync(string userId, string playlistId, List uris, int? position = null) - { - JObject body = new JObject - { - {"uris", JArray.FromObject(uris.Take(100))} - }; - return await UploadDataAsync(_builder.AddPlaylistTracks(userId, playlistId, uris, position), body.ToString(Formatting.None)).ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Add one or more tracks to a user’s playlist asynchronously. - /// - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public async Task AddPlaylistTracksAsync(string playlistId, List uris, int? position = null) - { - JObject body = new JObject - { - {"uris", JArray.FromObject(uris.Take(100))} - }; - return await UploadDataAsync(_builder.AddPlaylistTracks(playlistId, uris, position), body.ToString(Formatting.None)).ConfigureAwait(false) ?? new ErrorResponse(); - } - - /// - /// Add a track to a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A Spotify Track URI - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - [Obsolete("Calling AddPlaylistTrack with a userId is deprecated, remove the parameter")] - public ErrorResponse AddPlaylistTrack(string userId, string playlistId, string uri, int? position = null) - { - return AddPlaylistTracks(playlistId, new List { uri }, position); - } - - - /// - /// Add a track to a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// A Spotify Track URI - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public ErrorResponse AddPlaylistTrack(string playlistId, string uri, int? position = null) - { - return AddPlaylistTracks(playlistId, new List { uri }, position); - } - - /// - /// Add a track to a user’s playlist asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A Spotify Track URI - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - [Obsolete("Calling AddPlaylistTrack with a userId is deprecated, remove the parameter")] - public Task AddPlaylistTrackAsync(string userId, string playlistId, string uri, int? position = null) - { - return AddPlaylistTracksAsync(userId, playlistId, new List { uri }, position); - } - - /// - /// Add a track to a user’s playlist asynchronously. - /// - /// The Spotify ID for the playlist. - /// A Spotify Track URI - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public Task AddPlaylistTrackAsync(string playlistId, string uri, int? position = null) - { - return AddPlaylistTracksAsync(playlistId, new List { uri }, position); - } - - /// - /// Reorder a track or a group of tracks in a playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The position of the first track to be reordered. - /// The position where the tracks should be inserted. - /// The amount of tracks to be reordered. Defaults to 1 if not set. - /// The playlist's snapshot ID against which you want to make the changes. - /// - /// AUTH NEEDED - [Obsolete("Calling ReorderPlaylist with a userId is deprecated, remove the parameter")] - public Snapshot ReorderPlaylist(string userId, string playlistId, int rangeStart, int insertBefore, int rangeLength = 1, string snapshotId = "") - { - JObject body = new JObject - { - {"range_start", rangeStart}, - {"range_length", rangeLength}, - {"insert_before", insertBefore} - }; - if (!string.IsNullOrEmpty(snapshotId)) - body.Add("snapshot_id", snapshotId); - return UploadData(_builder.ReorderPlaylist(userId, playlistId), body.ToString(Formatting.None), "PUT"); - } - - /// - /// Reorder a track or a group of tracks in a playlist. - /// - /// The Spotify ID for the playlist. - /// The position of the first track to be reordered. - /// The position where the tracks should be inserted. - /// The amount of tracks to be reordered. Defaults to 1 if not set. - /// The playlist's snapshot ID against which you want to make the changes. - /// - /// AUTH NEEDED - public Snapshot ReorderPlaylist(string playlistId, int rangeStart, int insertBefore, int rangeLength = 1, string snapshotId = "") - { - JObject body = new JObject - { - {"range_start", rangeStart}, - {"range_length", rangeLength}, - {"insert_before", insertBefore} - }; - if (!string.IsNullOrEmpty(snapshotId)) - body.Add("snapshot_id", snapshotId); - return UploadData(_builder.ReorderPlaylist(playlistId), body.ToString(Formatting.None), "PUT"); - } - - /// - /// Reorder a track or a group of tracks in a playlist asynchronously. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// The position of the first track to be reordered. - /// The position where the tracks should be inserted. - /// The amount of tracks to be reordered. Defaults to 1 if not set. - /// The playlist's snapshot ID against which you want to make the changes. - /// - /// AUTH NEEDED - [Obsolete("Calling ReorderPlaylist with a userId is deprecated, remove the parameter")] - public Task ReorderPlaylistAsync(string userId, string playlistId, int rangeStart, int insertBefore, int rangeLength = 1, string snapshotId = "") - { - JObject body = new JObject - { - {"range_start", rangeStart}, - {"range_length", rangeLength}, - {"insert_before", insertBefore}, - {"snapshot_id", snapshotId} - }; - if (!string.IsNullOrEmpty(snapshotId)) - body.Add("snapshot_id", snapshotId); - return UploadDataAsync(_builder.ReorderPlaylist(userId, playlistId), body.ToString(Formatting.None), "PUT"); - } - - /// - /// Reorder a track or a group of tracks in a playlist asynchronously. - /// - /// The Spotify ID for the playlist. - /// The position of the first track to be reordered. - /// The position where the tracks should be inserted. - /// The amount of tracks to be reordered. Defaults to 1 if not set. - /// The playlist's snapshot ID against which you want to make the changes. - /// - /// AUTH NEEDED - public Task ReorderPlaylistAsync(string playlistId, int rangeStart, int insertBefore, int rangeLength = 1, string snapshotId = "") - { - JObject body = new JObject - { - {"range_start", rangeStart}, - {"range_length", rangeLength}, - {"insert_before", insertBefore}, - {"snapshot_id", snapshotId} - }; - if (!string.IsNullOrEmpty(snapshotId)) - body.Add("snapshot_id", snapshotId); - return UploadDataAsync(_builder.ReorderPlaylist(playlistId), body.ToString(Formatting.None), "PUT"); - } - - #endregion Playlists - - #region Profiles - - /// - /// Get detailed profile information about the current user (including the current user’s username). - /// - /// - /// AUTH NEEDED - public PrivateProfile GetPrivateProfile() - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPrivateProfile"); - return DownloadData(_builder.GetPrivateProfile()); - } - - /// - /// Get detailed profile information about the current user asynchronously (including the current user’s username). - /// - /// - /// AUTH NEEDED - public Task GetPrivateProfileAsync() - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for GetPrivateProfile"); - return DownloadDataAsync(_builder.GetPrivateProfile()); - } - - /// - /// Get public profile information about a Spotify user. - /// - /// The user's Spotify user ID. - /// - public PublicProfile GetPublicProfile(string userId) - { - return DownloadData(_builder.GetPublicProfile(userId)); - } - - /// - /// Get public profile information about a Spotify user asynchronously. - /// - /// The user's Spotify user ID. - /// - public Task GetPublicProfileAsync(string userId) - { - return DownloadDataAsync(_builder.GetPublicProfile(userId)); - } - - #endregion Profiles - - #region Tracks - - /// - /// Get Spotify catalog information for multiple tracks based on their Spotify IDs. - /// - /// A list of the Spotify IDs for the tracks. Maximum: 50 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public SeveralTracks GetSeveralTracks(List ids, string market = "") - { - return DownloadData(_builder.GetSeveralTracks(ids, market)); - } - - /// - /// Get Spotify catalog information for multiple tracks based on their Spotify IDs asynchronously. - /// - /// A list of the Spotify IDs for the tracks. Maximum: 50 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetSeveralTracksAsync(List ids, string market = "") - { - return DownloadDataAsync(_builder.GetSeveralTracks(ids, market)); - } - - /// - /// Get Spotify catalog information for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public FullTrack GetTrack(string id, string market = "") - { - return DownloadData(_builder.GetTrack(id, market)); - } - - /// - /// Get Spotify catalog information for a single track identified by its unique Spotify ID asynchronously. - /// - /// The Spotify ID for the track. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetTrackAsync(string id, string market = "") - { - return DownloadDataAsync(_builder.GetTrack(id, market)); - } - - /// - /// Get a detailed audio analysis for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public AudioAnalysis GetAudioAnalysis(string id) - { - return DownloadData(_builder.GetAudioAnalysis(id)); - } - - /// - /// Get a detailed audio analysis for a single track identified by its unique Spotify ID asynchronously. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public Task GetAudioAnalysisAsync(string id) - { - return DownloadDataAsync(_builder.GetAudioAnalysis(id)); - } - - /// - /// Get audio feature information for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public AudioFeatures GetAudioFeatures(string id) - { - return DownloadData(_builder.GetAudioFeatures(id)); - } - - /// - /// Get audio feature information for a single track identified by its unique Spotify ID asynchronously. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public Task GetAudioFeaturesAsync(string id) - { - return DownloadDataAsync(_builder.GetAudioFeatures(id)); - } - - /// - /// Get audio features for multiple tracks based on their Spotify IDs. - /// - /// A list of Spotify Track-IDs. Maximum: 100 IDs. - /// - /// AUTH NEEDED - public SeveralAudioFeatures GetSeveralAudioFeatures(List ids) - { - return DownloadData(_builder.GetSeveralAudioFeatures(ids)); - } - - /// - /// Get audio features for multiple tracks based on their Spotify IDs asynchronously. - /// - /// A list of Spotify Track-IDs. Maximum: 100 IDs. - /// - /// AUTH NEEDED - public Task GetSeveralAudioFeaturesAsync(List ids) - { - return DownloadDataAsync(_builder.GetSeveralAudioFeatures(ids)); - } - - #endregion Tracks - - #region Player - - /// - /// Get information about a user’s available devices. - /// - /// - public AvailabeDevices GetDevices() - { - return DownloadData(_builder.GetDevices()); - } - - /// - /// Get information about a user’s available devices. - /// - /// - public Task GetDevicesAsync() - { - return DownloadDataAsync(_builder.GetDevices()); - } - - /// - /// Get information about the user’s current playback state, including track, track progress, and active device. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public PlaybackContext GetPlayback(string market = "") - { - return DownloadData(_builder.GetPlayback(market)); - } - - /// - /// Get information about the user’s current playback state, including track, track progress, and active device. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetPlaybackAsync(string market = "") - { - return DownloadDataAsync(_builder.GetPlayback(market)); - } - - /// - /// Get the object currently being played on the user’s Spotify account. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public PlaybackContext GetPlayingTrack(string market = "") - { - return DownloadData(_builder.GetPlayingTrack(market)); - } - - /// - /// Get the object currently being played on the user’s Spotify account. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public Task GetPlayingTrackAsync(string market = "") - { - return DownloadDataAsync(_builder.GetPlayingTrack(market)); - } - - /// - /// Transfer playback to a new device and determine if it should start playing. - /// - /// ID of the device on which playback should be started/transferred to - /// - /// true: ensure playback happens on new device. - /// false or not provided: keep the current playback state. - /// - /// - public ErrorResponse TransferPlayback(string deviceId, bool play = false) => TransferPlayback( - new List { deviceId }, play); - - /// - /// Transfer playback to a new device and determine if it should start playing. - /// - /// ID of the device on which playback should be started/transferred to - /// - /// true: ensure playback happens on new device. - /// false or not provided: keep the current playback state. - /// - /// - public Task TransferPlaybackAsync(string deviceId, bool play = false) => TransferPlaybackAsync( - new List { deviceId }, play); - - /// - /// Transfer playback to a new device and determine if it should start playing. - /// NOTE: Although an array is accepted, only a single device_id is currently supported. Supplying more than one will return 400 Bad Request - /// - /// A array containing the ID of the device on which playback should be started/transferred. - /// - /// true: ensure playback happens on new device. - /// false or not provided: keep the current playback state. - /// - /// - public ErrorResponse TransferPlayback(List deviceIds, bool play = false) - { - JObject ob = new JObject - { - { "play", play }, - { "device_ids", new JArray(deviceIds) } - }; - return UploadData(_builder.TransferPlayback(), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Transfer playback to a new device and determine if it should start playing. - /// NOTE: Although an array is accepted, only a single device_id is currently supported. Supplying more than one will return 400 Bad Request - /// - /// A array containing the ID of the device on which playback should be started/transferred. - /// - /// true: ensure playback happens on new device. - /// false or not provided: keep the current playback state. - /// - /// - public Task TransferPlaybackAsync(List deviceIds, bool play = false) - { - JObject ob = new JObject - { - { "play", play }, - { "device_ids", new JArray(deviceIds) } - }; - return UploadDataAsync(_builder.TransferPlayback(), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Start a new context or resume current playback on the user’s active device. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// Spotify URI of the context to play. - /// A JSON array of the Spotify track URIs to play. - /// Indicates from where in the context playback should start. - /// Only available when context_uri corresponds to an album or playlist object, or when the uris parameter is used. - /// The starting time to seek the track to - /// - public ErrorResponse ResumePlayback(string deviceId = "", string contextUri = "", List uris = null, - int? offset = null, int positionMs = 0) - { - JObject ob = new JObject(); - if(!string.IsNullOrEmpty(contextUri)) - ob.Add("context_uri", contextUri); - if(uris != null) - ob.Add("uris", new JArray(uris)); - if(offset != null) - ob.Add("offset", new JObject { { "position", offset } }); - if (positionMs > 0) - ob.Add("position_ms", positionMs); - return UploadData(_builder.ResumePlayback(deviceId), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Start a new context or resume current playback on the user’s active device. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// Spotify URI of the context to play. - /// A JSON array of the Spotify track URIs to play. - /// Indicates from where in the context playback should start. - /// Only available when context_uri corresponds to an album or playlist object, or when the uris parameter is used. - /// The starting time to seek the track to - /// - public Task ResumePlaybackAsync(string deviceId = "", string contextUri = "", List uris = null, - int? offset = null, int positionMs = 0) - { - JObject ob = new JObject(); - if (!string.IsNullOrEmpty(contextUri)) - ob.Add("context_uri", contextUri); - if (uris != null) - ob.Add("uris", new JArray(uris)); - if (offset != null) - ob.Add("offset", new JObject { { "position", offset } }); - if (positionMs > 0) - ob.Add("position_ms", positionMs); - return UploadDataAsync(_builder.ResumePlayback(deviceId), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Start a new context or resume current playback on the user’s active device. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// Spotify URI of the context to play. - /// A JSON array of the Spotify track URIs to play. - /// Indicates from where in the context playback should start. - /// Only available when context_uri corresponds to an album or playlist object, or when the uris parameter is used. - /// The starting time to seek the track to - /// - public ErrorResponse ResumePlayback(string deviceId = "", string contextUri = "", List uris = null, - string offset = "", int positionMs = 0) - { - JObject ob = new JObject(); - if (!string.IsNullOrEmpty(contextUri)) - ob.Add("context_uri", contextUri); - if (uris != null) - ob.Add("uris", new JArray(uris)); - if (!string.IsNullOrEmpty(offset)) - ob.Add("offset", new JObject {{"uri", offset}}); - if (positionMs > 0) - ob.Add("position_ms", positionMs); - return UploadData(_builder.ResumePlayback(deviceId), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Start a new context or resume current playback on the user’s active device. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// Spotify URI of the context to play. - /// A JSON array of the Spotify track URIs to play. - /// Indicates from where in the context playback should start. - /// Only available when context_uri corresponds to an album or playlist object, or when the uris parameter is used. - /// The starting time to seek the track to - /// - public Task ResumePlaybackAsync(string deviceId = "", string contextUri = "", List uris = null, - string offset = "", int positionMs = 0) - { - JObject ob = new JObject(); - if (!string.IsNullOrEmpty(contextUri)) - ob.Add("context_uri", contextUri); - if (uris != null) - ob.Add("uris", new JArray(uris)); - if (!string.IsNullOrEmpty(offset)) - ob.Add("offset", new JObject { { "uri", offset } }); - if (positionMs > 0) - ob.Add("position_ms", positionMs); - return UploadDataAsync(_builder.ResumePlayback(deviceId), ob.ToString(Formatting.None), "PUT"); - } - - /// - /// Pause playback on the user’s account. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse PausePlayback(string deviceId = "") - { - return UploadData(_builder.PausePlayback(deviceId), string.Empty, "PUT"); - } - - /// - /// Pause playback on the user’s account. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task PausePlaybackAsync(string deviceId = "") - { - return UploadDataAsync(_builder.PausePlayback(deviceId), string.Empty, "PUT"); - } - - /// - /// Skips to next track in the user’s queue. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SkipPlaybackToNext(string deviceId = "") - { - return UploadData(_builder.SkipPlaybackToNext(deviceId), string.Empty); - } - - /// - /// Skips to next track in the user’s queue. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SkipPlaybackToNextAsync(string deviceId = "") - { - return UploadDataAsync(_builder.SkipPlaybackToNext(deviceId), string.Empty); - } - - /// - /// Skips to previous track in the user’s queue. - /// Note that this will ALWAYS skip to the previous track, regardless of the current track’s progress. - /// Returning to the start of the current track should be performed using the https://api.spotify.com/v1/me/player/seek endpoint. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SkipPlaybackToPrevious(string deviceId = "") - { - return UploadData(_builder.SkipPlaybackToPrevious(deviceId), string.Empty); - } - - /// - /// Skips to previous track in the user’s queue. - /// Note that this will ALWAYS skip to the previous track, regardless of the current track’s progress. - /// Returning to the start of the current track should be performed using the https://api.spotify.com/v1/me/player/seek endpoint. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SkipPlaybackToPreviousAsync(string deviceId = "") - { - return UploadDataAsync(_builder.SkipPlaybackToPrevious(deviceId), string.Empty); - } - - /// - /// Seeks to the given position in the user’s currently playing track. - /// - /// The position in milliseconds to seek to. Must be a positive number. - /// Passing in a position that is greater than the length of the track will cause the player to start playing the next song. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SeekPlayback(int positionMs, string deviceId = "") - { - return UploadData(_builder.SeekPlayback(positionMs, deviceId), string.Empty, "PUT"); - } - - /// - /// Seeks to the given position in the user’s currently playing track. - /// - /// The position in milliseconds to seek to. Must be a positive number. - /// Passing in a position that is greater than the length of the track will cause the player to start playing the next song. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SeekPlaybackAsync(int positionMs, string deviceId = "") - { - return UploadDataAsync(_builder.SeekPlayback(positionMs, deviceId), string.Empty, "PUT"); - } - - /// - /// Set the repeat mode for the user’s playback. Options are repeat-track, repeat-context, and off. - /// - /// track, context or off. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SetRepeatMode(RepeatState state, string deviceId = "") - { - return UploadData(_builder.SetRepeatMode(state, deviceId), string.Empty, "PUT"); - } - - /// - /// Set the repeat mode for the user’s playback. Options are repeat-track, repeat-context, and off. - /// - /// track, context or off. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SetRepeatModeAsync(RepeatState state, string deviceId = "") - { - return UploadDataAsync(_builder.SetRepeatMode(state, deviceId), string.Empty, "PUT"); - } - - /// - /// Set the volume for the user’s current playback device. - /// - /// Integer. The volume to set. Must be a value from 0 to 100 inclusive. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SetVolume(int volumePercent, string deviceId = "") - { - return UploadData(_builder.SetVolume(volumePercent, deviceId), string.Empty, "PUT"); - } - - /// - /// Set the volume for the user’s current playback device. - /// - /// Integer. The volume to set. Must be a value from 0 to 100 inclusive. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SetVolumeAsync(int volumePercent, string deviceId = "") - { - return UploadDataAsync(_builder.SetVolume(volumePercent, deviceId), string.Empty, "PUT"); - } - - /// - /// Toggle shuffle on or off for user’s playback. - /// - /// True or False - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public ErrorResponse SetShuffle(bool shuffle, string deviceId = "") - { - return UploadData(_builder.SetShuffle(shuffle, deviceId), string.Empty, "PUT"); - } - - /// - /// Toggle shuffle on or off for user’s playback. - /// - /// True or False - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public Task SetShuffleAsync(bool shuffle, string deviceId = "") - { - return UploadDataAsync(_builder.SetShuffle(shuffle, deviceId), string.Empty, "PUT"); - } - - #endregion - - #region Util - - public TOut GetNextPage(Paging paging) where TOut : BasicModel - { - if (!paging.HasNextPage()) - throw new InvalidOperationException("This Paging-Object has no Next-Page"); - return DownloadData(paging.Next); - } - - public Paging GetNextPage(Paging paging) - { - return GetNextPage, T>(paging); - } - - public Task GetNextPageAsync(Paging paging) where TOut : BasicModel - { - if (!paging.HasNextPage()) - throw new InvalidOperationException("This Paging-Object has no Next-Page"); - return DownloadDataAsync(paging.Next); - } - - public Task> GetNextPageAsync(Paging paging) - { - return GetNextPageAsync, T>(paging); - } - - public TOut GetPreviousPage(Paging paging) where TOut : BasicModel - { - if (!paging.HasPreviousPage()) - throw new InvalidOperationException("This Paging-Object has no Previous-Page"); - return DownloadData(paging.Previous); - } - - public Paging GetPreviousPage(Paging paging) - { - return GetPreviousPage, T>(paging); - } - - public Task GetPreviousPageAsync(Paging paging) where TOut : BasicModel - { - if (!paging.HasPreviousPage()) - throw new InvalidOperationException("This Paging-Object has no Previous-Page"); - return DownloadDataAsync(paging.Previous); - } - - public Task> GetPreviousPageAsync(Paging paging) - { - return GetPreviousPageAsync, T>(paging); - } - - private ListResponse DownloadList(string url) - { - int triesLeft = RetryTimes + 1; - Error lastError; - - ListResponse data = null; - do - { - if (data != null) { Thread.Sleep(RetryAfter); } - Tuple res = DownloadDataAlt(url); - data = ExtractDataToListResponse(res); - - lastError = data.Error; - - triesLeft -= 1; - - } while (UseAutoRetry && triesLeft > 0 && lastError != null && RetryErrorCodes.Contains(lastError.Status)); - - return data; - } - - private async Task> DownloadListAsync(string url) - { - int triesLeft = RetryTimes + 1; - Error lastError; - - ListResponse data = null; - do - { - if (data != null) { await Task.Delay(RetryAfter).ConfigureAwait(false); } - Tuple res = await DownloadDataAltAsync(url).ConfigureAwait(false); - data = ExtractDataToListResponse(res); - - lastError = data.Error; - - triesLeft -= 1; - - } while (UseAutoRetry && triesLeft > 0 && lastError != null && RetryErrorCodes.Contains(lastError.Status)); - - return data; - } - - private static ListResponse ExtractDataToListResponse(Tuple res) - { - ListResponse ret; - if (res.Item2 is JArray) - { - ret = new ListResponse - { - List = res.Item2.ToObject>(), - Error = null - }; - } - else - { - ret = new ListResponse - { - List = null, - Error = res.Item2["error"].ToObject() - }; - } - ret.AddResponseInfo(res.Item1); - return ret; - } - - public T UploadData(string url, string uploadData, string method = "POST") where T : BasicModel - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for all Upload-Actions"); - int triesLeft = RetryTimes + 1; - Error lastError; - - Tuple response = null; - do - { - Dictionary headers = new Dictionary - { - { "Authorization", TokenType + " " + AccessToken}, - { "Content-Type", "application/json" } - }; - - if (response != null) { Thread.Sleep(RetryAfter); } - response = WebClient.UploadJson(url, uploadData, method, headers); - - response.Item2.AddResponseInfo(response.Item1); - lastError = response.Item2.Error; - triesLeft -= 1; - - } while (UseAutoRetry && triesLeft > 0 && lastError != null && RetryErrorCodes.Contains(lastError.Status)); - - return response.Item2; - } - - public async Task UploadDataAsync(string url, string uploadData, string method = "POST") where T : BasicModel - { - if (!UseAuth) - throw new InvalidOperationException("Auth is required for all Upload-Actions"); - - int triesLeft = RetryTimes + 1; - Error lastError; - - Tuple response = null; - do - { - Dictionary headers = new Dictionary - { - { "Authorization", TokenType + " " + AccessToken}, - { "Content-Type", "application/json" } - }; - - if (response != null) { await Task.Delay(RetryAfter).ConfigureAwait(false); } - response = await WebClient.UploadJsonAsync(url, uploadData, method, headers).ConfigureAwait(false); - - response.Item2.AddResponseInfo(response.Item1); - lastError = response.Item2.Error; - - triesLeft -= 1; - - } while (UseAutoRetry && triesLeft > 0 && lastError != null && RetryErrorCodes.Contains(lastError.Status)); - - return response.Item2; - } - - public T DownloadData(string url) where T : BasicModel - { - int triesLeft = RetryTimes + 1; - Error lastError; - - Tuple response = null; - do - { - if(response != null) { Thread.Sleep(RetryAfter); } - response = DownloadDataAlt(url); - - response.Item2.AddResponseInfo(response.Item1); - lastError = response.Item2.Error; - - triesLeft -= 1; - - } while (UseAutoRetry && triesLeft > 0 && lastError != null && RetryErrorCodes.Contains(lastError.Status)); - - - return response.Item2; - } - - /// - /// Retrieves whether request had a "TooManyRequests" error, and get the amount Spotify recommends waiting before another request. - /// - /// Info object to analyze. - /// Seconds to wait before making another request. -1 if no error. - /// AUTH NEEDED - private int GetTooManyRequests(ResponseInfo info) - { - // 429 is "TooManyRequests" value specified in Spotify API - if (429 != (int)info.StatusCode) - { - return -1; - } - if (!int.TryParse(info.Headers.Get("Retry-After"), out int secondsToWait)) - { - return -1; - } - return secondsToWait; - } - - public async Task DownloadDataAsync(string url) where T : BasicModel - { - int triesLeft = RetryTimes + 1; - Error lastError; - - Tuple response = null; - do - { - if (response != null) - { - int msToWait = RetryAfter; - int secondsToWait = GetTooManyRequests(response.Item1); - if (secondsToWait > 0) - { - msToWait = secondsToWait * 1000; - } - await Task.Delay(msToWait).ConfigureAwait(false); - } - response = await DownloadDataAltAsync(url).ConfigureAwait(false); - - response.Item2.AddResponseInfo(response.Item1); - lastError = response.Item2.Error; - - if (TooManyRequestsConsumesARetry || GetTooManyRequests(response.Item1) == -1) - { - triesLeft -= 1; - } - - } while (UseAutoRetry - && triesLeft > 0 - && (GetTooManyRequests(response.Item1) != -1 - || lastError != null && RetryErrorCodes.Contains(lastError.Status))); - - - return response.Item2; - } - - private Tuple DownloadDataAlt(string url) - { - Dictionary headers = new Dictionary(); - if (UseAuth) - headers.Add("Authorization", TokenType + " " + AccessToken); - return WebClient.DownloadJson(url, headers); - } - - private Task> DownloadDataAltAsync(string url) - { - Dictionary headers = new Dictionary(); - if (UseAuth) - headers.Add("Authorization", TokenType + " " + AccessToken); - return WebClient.DownloadJsonAsync(url, headers); - } - - #endregion Util - } -} diff --git a/Source Files/SpotifyAPI.Web/SpotifyWebBuilder.cs b/Source Files/SpotifyAPI.Web/SpotifyWebBuilder.cs deleted file mode 100644 index 47e913a..0000000 --- a/Source Files/SpotifyAPI.Web/SpotifyWebBuilder.cs +++ /dev/null @@ -1,1129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Models; - -namespace SpotifyAPI.Web -{ - /// - /// SpotifyAPI URL-Generator - /// - public class SpotifyWebBuilder - { - public const string APIBase = "https://api.spotify.com/v1"; - - #region Search - - /// - /// Get Spotify catalog information about artists, albums, tracks or playlists that match a keyword string. - /// - /// The search query's keywords (and optional field filters and operators), for example q=roadhouse+blues. - /// A list of item types to search across. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first result to return. Default: 0 - /// An ISO 3166-1 alpha-2 country code or the string from_token. - /// - public string SearchItems(string q, SearchType type, int limit = 20, int offset = 0, string market = "") - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder(APIBase + "/search"); - builder.Append("?q=" + q); - builder.Append("&type=" + type.GetStringAttribute(",")); - builder.Append("&limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - #endregion Search - - #region Albums - - /// - /// Get Spotify catalog information about an album’s tracks. Optional parameters can be used to limit the number of - /// tracks returned. - /// - /// The Spotify ID for the album. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first track to return. Default: 0 (the first object). - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetAlbumTracks(string id, int limit = 20, int offset = 0, string market = "") - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/albums/" + id + "/tracks"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Get Spotify catalog information for a single album. - /// - /// The Spotify ID for the album. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetAlbum(string id, string market = "") - { - return string.IsNullOrEmpty(market) ? $"{APIBase}/albums/{id}" : $"{APIBase}/albums/{id}?market={market}"; - } - - /// - /// Get Spotify catalog information for multiple albums identified by their Spotify IDs. - /// - /// A list of the Spotify IDs for the albums. Maximum: 20 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetSeveralAlbums(List ids, string market = "") - { - return string.IsNullOrEmpty(market) - ? $"{APIBase}/albums?ids={string.Join(",", ids.Take(20))}" - : $"{APIBase}/albums?market={market}&ids={string.Join(",", ids.Take(20))}"; - } - - #endregion Albums - - #region Artists - - /// - /// Get Spotify catalog information for a single artist identified by their unique Spotify ID. - /// - /// The Spotify ID for the artist. - /// - public string GetArtist(string id) - { - return $"{APIBase}/artists/{id}"; - } - - /// - /// Get Spotify catalog information about artists similar to a given artist. Similarity is based on analysis of the - /// Spotify community’s listening history. - /// - /// The Spotify ID for the artist. - /// - public string GetRelatedArtists(string id) - { - return $"{APIBase}/artists/{id}/related-artists"; - } - - /// - /// Get Spotify catalog information about an artist’s top tracks by country. - /// - /// The Spotify ID for the artist. - /// The country: an ISO 3166-1 alpha-2 country code. - /// - public string GetArtistsTopTracks(string id, string country) - { - return $"{APIBase}/artists/{id}/top-tracks?country={country}"; - } - - /// - /// Get Spotify catalog information about an artist’s albums. Optional parameters can be specified in the query string - /// to filter and sort the response. - /// - /// The Spotify ID for the artist. - /// - /// A list of keywords that will be used to filter the response. If not supplied, all album types will - /// be returned - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first album to return. Default: 0 - /// - /// An ISO 3166-1 alpha-2 country code. Supply this parameter to limit the response to one particular - /// geographical market - /// - /// - public string GetArtistsAlbums(string id, AlbumType type = AlbumType.All, int limit = 20, int offset = 0, string market = "") - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/artists/" + id + "/albums"); - builder.Append("?album_type=" + type.GetStringAttribute(",")); - builder.Append("&limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Get Spotify catalog information for several artists based on their Spotify IDs. - /// - /// A list of the Spotify IDs for the artists. Maximum: 50 IDs. - /// - public string GetSeveralArtists(List ids) - { - return $"{APIBase}/artists?ids={string.Join(",", ids.Take(50))}"; - } - - #endregion Artists - - #region Browse - - /// - /// Get a list of Spotify featured playlists (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// - /// The desired language, consisting of a lowercase ISO 639 language code and an uppercase ISO 3166-1 - /// alpha-2 country code, joined by an underscore. - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// A timestamp in ISO 8601 format - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// AUTH NEEDED - public string GetFeaturedPlaylists(string locale = "", string country = "", DateTime timestamp = default(DateTime), int limit = 20, int offset = 0) - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/browse/featured-playlists"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(locale)) - builder.Append("&locale=" + locale); - if (!string.IsNullOrEmpty(country)) - builder.Append("&country=" + country); - if (timestamp != default(DateTime)) - builder.Append("×tamp=" + timestamp.ToString("yyyy-MM-ddTHH:mm:ss")); - return builder.ToString(); - } - - /// - /// Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). - /// - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public string GetNewAlbumReleases(string country = "", int limit = 20, int offset = 0) - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/browse/new-releases"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(country)) - builder.Append("&country=" + country); - return builder.ToString(); - } - - /// - /// Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). - /// - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want to narrow the - /// list of returned categories to those relevant to a particular country - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// The maximum number of categories to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 (the first object). - /// - /// AUTH NEEDED - public string GetCategories(string country = "", string locale = "", int limit = 20, int offset = 0) - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder(APIBase + "/browse/categories"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(country)) - builder.Append("&country=" + country); - if (!string.IsNullOrEmpty(locale)) - builder.Append("&locale=" + locale); - return builder.ToString(); - } - - /// - /// Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). - /// - /// The Spotify category ID for the category. - /// - /// A country: an ISO 3166-1 alpha-2 country code. Provide this parameter to ensure that the category - /// exists for a particular country. - /// - /// - /// The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country - /// code, joined by an underscore - /// - /// - /// AUTH NEEDED - public string GetCategory(string categoryId, string country = "", string locale = "") - { - StringBuilder builder = new StringBuilder(APIBase + "/browse/categories/" + categoryId); - if (!string.IsNullOrEmpty(country)) - builder.Append("?country=" + country); - if (!string.IsNullOrEmpty(locale)) - builder.Append((country == "" ? "?locale=" : "&locale=") + locale); - return builder.ToString(); - } - - /// - /// Get a list of Spotify playlists tagged with a particular category. - /// - /// The Spotify category ID for the category. - /// A country: an ISO 3166-1 alpha-2 country code. - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first item to return. Default: 0 - /// - /// AUTH NEEDED - public string GetCategoryPlaylists(string categoryId, string country = "", int limit = 20, int offset = 0) - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder(APIBase + "/browse/categories/" + categoryId + "/playlists"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(country)) - builder.Append("&country=" + country); - return builder.ToString(); - } - - /// - /// Create a playlist-style listening experience based on seed artists, tracks and genres. - /// - /// A comma separated list of Spotify IDs for seed artists. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of any genres in the set of available genre seeds. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// A comma separated list of Spotify IDs for a seed track. - /// Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. - /// - /// Tracks with the attribute values nearest to the target values will be preferred. - /// For each tunable track attribute, a hard floor on the selected track attribute’s value can be provided - /// For each tunable track attribute, a hard ceiling on the selected track attribute’s value can be provided - /// The target size of the list of recommended tracks. Default: 20. Minimum: 1. Maximum: 100. - /// For seeds with unusually small pools or when highly restrictive filtering is applied, it may be impossible to generate the requested number of recommended tracks. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// Because min_*, max_* and target_* are applied to pools before relinking, the generated results may not precisely match the filters applied. - /// - /// AUTH NEEDED - public string GetRecommendations(List artistSeed = null, List genreSeed = null, List trackSeed = null, - TuneableTrack target = null, TuneableTrack min = null, TuneableTrack max = null, int limit = 20, string market = "") - { - limit = Math.Min(100, limit); - StringBuilder builder = new StringBuilder($"{APIBase}/recommendations"); - builder.Append("?limit=" + limit); - if (artistSeed?.Count > 0) - builder.Append("&seed_artists=" + string.Join(",", artistSeed)); - if (genreSeed?.Count > 0) - builder.Append("&seed_genres=" + string.Join(",", genreSeed)); - if (trackSeed?.Count > 0) - builder.Append("&seed_tracks=" + string.Join(",", trackSeed)); - if (target != null) - builder.Append(target.BuildUrlParams("target")); - if (min != null) - builder.Append(min.BuildUrlParams("min")); - if (max != null) - builder.Append(max.BuildUrlParams("max")); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Retrieve a list of available genres seed parameter values for recommendations. - /// - /// - /// AUTH NEEDED - public string GetRecommendationSeedsGenres() - { - return $"{APIBase}/recommendations/available-genre-seeds"; - } - - #endregion Browse - - #region Follow - - /// - /// Get the current user’s followed artists. - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// The last artist ID retrieved from the previous request. - /// - /// AUTH NEEDED - public string GetFollowedArtists(int limit = 20, string after = "") - { - limit = Math.Min(limit, 50); - const FollowType followType = FollowType.Artist; //currently only artist is supported. - StringBuilder builder = new StringBuilder(APIBase + "/me/following?type=" + followType.GetStringAttribute()); - builder.Append("&limit=" + limit); - if (!string.IsNullOrEmpty(after)) - builder.Append("&after=" + after); - return builder.ToString(); - } - - /// - /// Add the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// - /// AUTH NEEDED - public string Follow(FollowType followType) - { - return $"{APIBase}/me/following?type={followType.GetStringAttribute()}"; - } - - /// - /// Remove the current user as a follower of one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// - /// AUTH NEEDED - public string Unfollow(FollowType followType) - { - return $"{APIBase}/me/following?type={followType.GetStringAttribute()}"; - } - - /// - /// Check to see if the current user is following one or more artists or other Spotify users. - /// - /// The ID type: either artist or user. - /// A list of the artist or the user Spotify IDs to check - /// - /// AUTH NEEDED - public string IsFollowing(FollowType followType, List ids) - { - return $"{APIBase}/me/following/contains?type={followType.GetStringAttribute()}&ids={string.Join(",", ids)}"; - } - - /// - /// Add the current user as a follower of a playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// - /// The Spotify ID of the playlist. Any playlist can be followed, regardless of its public/private - /// status, as long as you know its playlist ID. - /// - /// - /// If true the playlist will be included in user's public playlists, if false it will remain - /// private. - /// - /// - /// AUTH NEEDED - public string FollowPlaylist(string ownerId, string playlistId, bool showPublic = true) - { - return $"{APIBase}/users/{ownerId}/playlists/{playlistId}/followers"; - } - - /// - /// Remove the current user as a follower of a playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist that is to be no longer followed. - /// - /// AUTH NEEDED - public string UnfollowPlaylist(string ownerId, string playlistId) - { - return $"{APIBase}/users/{ownerId}/playlists/{playlistId}/followers"; - } - - /// - /// Check to see if one or more Spotify users are following a specified playlist. - /// - /// The Spotify user ID of the person who owns the playlist. - /// The Spotify ID of the playlist. - /// A list of Spotify User IDs - /// - /// AUTH NEEDED - public string IsFollowingPlaylist(string ownerId, string playlistId, List ids) - { - return $"{APIBase}/users/{ownerId}/playlists/{playlistId}/followers/contains?ids={string.Join(",", ids)}"; - } - - #endregion Follow - - #region Library - - /// - /// Save one or more tracks to the current user’s “Your Music” library. - /// - /// - /// AUTH NEEDED - public string SaveTracks() - { - return APIBase + "/me/tracks/"; - } - - /// - /// Get a list of the songs saved in the current Spotify user’s “Your Music” library. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetSavedTracks(int limit = 20, int offset = 0, string market = "") - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/me/tracks"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Remove one or more tracks from the current user’s “Your Music” library. - /// - /// - /// AUTH NEEDED - public string RemoveSavedTracks() - { - return APIBase + "/me/tracks/"; - } - - /// - /// Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public string CheckSavedTracks(List ids) - { - return APIBase + "/me/tracks/contains?ids=" + string.Join(",", ids); - } - - /// - /// Save one or more albums to the current user’s "Your Music" library. - /// - /// - /// AUTH NEEDED - public string SaveAlbums() - { - return $"{APIBase}/me/albums"; - } - - /// - /// Get a list of the albums saved in the current Spotify user’s "Your Music" library. - /// - /// The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetSavedAlbums(int limit = 20, int offset = 0, string market = "") - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/me/albums"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Remove one or more albums from the current user’s "Your Music" library. - /// - /// - /// AUTH NEEDED - public string RemoveSavedAlbums() - { - return APIBase + "/me/albums/"; - } - - /// - /// Check if one or more albums is already saved in the current Spotify user’s "Your Music" library. - /// - /// A list of the Spotify IDs. - /// - /// AUTH NEEDED - public string CheckSavedAlbums(List ids) - { - return APIBase + "/me/albums/contains?ids=" + string.Join(",", ids); - } - - #endregion Library - - #region Personalization - - /// - /// Get the current user’s top tracks based on calculated affinity. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public string GetUsersTopTracks(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder($"{APIBase}/me/top/tracks"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offest); - builder.Append("&time_range=" + timeRange.GetStringAttribute()); - return builder.ToString(); - } - - /// - /// Get the current user’s top artists based on calculated affinity. - /// - /// Over what time frame the affinities are computed. - /// Valid values: long_term (calculated from several years of data and including all new data as it becomes available), - /// medium_term (approximately last 6 months), short_term (approximately last 4 weeks). - /// The number of entities to return. Default: 20. Minimum: 1. Maximum: 50 - /// The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. - /// - /// AUTH NEEDED - public string GetUsersTopArtists(TimeRangeType timeRange = TimeRangeType.MediumTerm, int limit = 20, int offest = 0) - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder($"{APIBase}/me/top/artists"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offest); - builder.Append("&time_range=" + timeRange.GetStringAttribute()); - return builder.ToString(); - } - - /// - /// Get tracks from the current user’s recent play history. - /// - /// The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. - /// A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position. If after is specified, before must not be specified. - /// A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position. If before is specified, after must not be specified. - /// - /// AUTH NEEDED - public string GetUsersRecentlyPlayedTracks(int limit = 20, DateTime? after = null, DateTime? before = null) - { - limit = Math.Min(50, limit); - StringBuilder builder = new StringBuilder($"{APIBase}/me/player/recently-played"); - builder.Append("?limit=" + limit); - if (after.HasValue) - builder.Append("&after=" + after.Value.ToUnixTimeMillisecondsPoly()); - if (before.HasValue) - builder.Append("&before=" + before.Value.ToUnixTimeMillisecondsPoly()); - return builder.ToString(); - } - - #endregion - - #region Playlists - - /// - /// Get a list of the playlists owned or followed by a Spotify user. - /// - /// The user's Spotify user ID. - /// The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. - /// The index of the first playlist to return. Default: 0 (the first object) - /// - /// AUTH NEEDED - public string GetUserPlaylists(string userId, int limit = 20, int offset = 0) - { - limit = Math.Min(limit, 50); - StringBuilder builder = new StringBuilder(APIBase + "/users/" + userId + "/playlists"); - builder.Append("?limit=" + limit); - builder.Append("&offset=" + offset); - return builder.ToString(); - } - - /// - /// Get a playlist owned by a Spotify user. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetPlaylist(string userId, string playlistId, string fields = "", string market = "") - { - StringBuilder builder = new StringBuilder(APIBase + "/users/" + userId + "/playlists/" + playlistId); - builder.Append("?fields=" + fields); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Get a playlist owned by a Spotify user. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetPlaylist(string playlistId, string fields = "", string market = "") - { - StringBuilder builder = new StringBuilder(APIBase + "/playlists/" + playlistId); - builder.Append("?fields=" + fields); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetPlaylistTracks(string userId, string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - limit = Math.Min(limit, 100); - StringBuilder builder = new StringBuilder(APIBase + "/users/" + userId + "/playlists/" + playlistId + "/tracks"); - builder.Append("?fields=" + fields); - builder.Append("&limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Get full details of the tracks of a playlist owned by a Spotify user. - /// - /// The Spotify ID for the playlist. - /// - /// Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are - /// returned. - /// - /// The maximum number of tracks to return. Default: 100. Minimum: 1. Maximum: 100. - /// The index of the first object to return. Default: 0 (i.e., the first object) - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - /// AUTH NEEDED - public string GetPlaylistTracks(string playlistId, string fields = "", int limit = 100, int offset = 0, string market = "") - { - limit = Math.Min(limit, 100); - StringBuilder builder = new StringBuilder(APIBase + "/playlists/" + playlistId + "/tracks"); - builder.Append("?fields=" + fields); - builder.Append("&limit=" + limit); - builder.Append("&offset=" + offset); - if (!string.IsNullOrEmpty(market)) - builder.Append("&market=" + market); - return builder.ToString(); - } - - /// - /// Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) - /// - /// The user's Spotify user ID. - /// - /// The name for the new playlist, for example "Your Coolest Playlist". This name does not need - /// to be unique. - /// - /// - /// default true. If true the playlist will be public, if false it will be private. To be able to - /// create private playlists, the user must have granted the playlist-modify-private scope. - /// - /// - /// AUTH NEEDED - public string CreatePlaylist(string userId, string playlistName, bool isPublic = true) - { - return $"{APIBase}/users/{userId}/playlists"; - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string UpdatePlaylist(string userId, string playlistId) - { - return $"{APIBase}/users/{userId}/playlists/{playlistId}"; - } - - /// - /// Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) - /// - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string UpdatePlaylist(string playlistId) - { - return $"{APIBase}/playlists/{playlistId}"; - } - - /// - /// Replace all the tracks in a playlist, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string ReplacePlaylistTracks(string userId, string playlistId) - { - return $"{APIBase}/users/{userId}/playlists/{playlistId}/tracks"; - } - - /// - /// Replace all the tracks in a playlist, overwriting its existing tracks. This powerful request can be useful for - /// replacing tracks, re-ordering existing tracks, or clearing the playlist. - /// - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string ReplacePlaylistTracks(string playlistId) - { - return $"{APIBase}/playlists/{playlistId}/tracks"; - } - - /// - /// Remove one or more tracks from a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - public string RemovePlaylistTracks(string userId, string playlistId, List uris) - { - return $"{APIBase}/users/{userId}/playlists/{playlistId}/tracks"; - } - - /// - /// Remove one or more tracks from a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// - /// array of objects containing Spotify URI strings (and their position in the playlist). A maximum of - /// 100 objects can be sent at once. - /// - /// - /// AUTH NEEDED - public string RemovePlaylistTracks(string playlistId, List uris) - { - return $"{APIBase}/playlists/{playlistId}/tracks"; - } - - /// - /// Add one or more tracks to a user’s playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public string AddPlaylistTracks(string userId, string playlistId, List uris, int? position = null) - { - return position == null - ? $"{APIBase}/users/{userId}/playlists/{playlistId}/tracks" - : $"{APIBase}/users/{userId}/playlists/{playlistId}/tracks?position={position}"; - } - - /// - /// Add one or more tracks to a user’s playlist. - /// - /// The Spotify ID for the playlist. - /// A list of Spotify track URIs to add - /// The position to insert the tracks, a zero-based index - /// - /// AUTH NEEDED - public string AddPlaylistTracks(string playlistId, List uris, int? position = null) - { - return position == null - ? $"{APIBase}/playlists/{playlistId}/tracks" - : $"{APIBase}/playlists/{playlistId}/tracks?position={position}"; - } - - /// - /// Reorder a track or a group of tracks in a playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string ReorderPlaylist(string userId, string playlistId) - { - return $"{APIBase}/users/{userId}/playlists/{playlistId}/tracks"; - } - - /// - /// Reorder a track or a group of tracks in a playlist. - /// - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string ReorderPlaylist(string playlistId) - { - return $"{APIBase}/playlists/{playlistId}/tracks"; - } - - /// - /// Upload an image for a playlist. - /// - /// The user's Spotify user ID. - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string UploadPlaylistImage(string userId, string playlistId) - { - return $"{APIBase}/users/{userId}/playlists/{playlistId}/images"; - } - - /// - /// Upload an image for a playlist. - /// - /// The Spotify ID for the playlist. - /// - /// AUTH NEEDED - public string UploadPlaylistImage(string playlistId) - { - return $"{APIBase}/playlists/{playlistId}/images"; - } - - #endregion Playlists - - #region Profiles - - /// - /// Get detailed profile information about the current user (including the current user’s username). - /// - /// - /// AUTH NEEDED - public string GetPrivateProfile() - { - return $"{APIBase}/me"; - } - - /// - /// Get public profile information about a Spotify user. - /// - /// The user's Spotify user ID. - /// - public string GetPublicProfile(string userId) - { - return $"{APIBase}/users/{userId}"; - } - - #endregion Profiles - - #region Tracks - - /// - /// Get Spotify catalog information for multiple tracks based on their Spotify IDs. - /// - /// A list of the Spotify IDs for the tracks. Maximum: 50 IDs. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetSeveralTracks(List ids, string market = "") - { - return string.IsNullOrEmpty(market) - ? $"{APIBase}/tracks?ids={string.Join(",", ids.Take(50))}" - : $"{APIBase}/tracks?market={market}&ids={string.Join(",", ids.Take(50))}"; - } - - /// - /// Get Spotify catalog information for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetTrack(string id, string market = "") - { - return string.IsNullOrEmpty(market) ? $"{APIBase}/tracks/{id}" : $"{APIBase}/tracks/{id}?market={market}"; - } - - /// - /// Get a detailed audio analysis for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public string GetAudioAnalysis(string id) - { - return $"{APIBase}/audio-analysis/{id}"; - } - - /// - /// Get audio feature information for a single track identified by its unique Spotify ID. - /// - /// The Spotify ID for the track. - /// - /// AUTH NEEDED - public string GetAudioFeatures(string id) - { - return $"{APIBase}/audio-features/{id}"; - } - - /// - /// Get audio features for multiple tracks based on their Spotify IDs. - /// - /// A list of Spotify Track-IDs. Maximum: 100 IDs. - /// - /// AUTH NEEDED - public string GetSeveralAudioFeatures(List ids) - { - return $"{APIBase}/audio-features?ids={string.Join(",", ids.Take(100))}"; - } - - #endregion Tracks - - #region Player - - /// - /// Get information about a user’s available devices. - /// - /// - public string GetDevices() - { - return $"{APIBase}/me/player/devices"; - } - - /// - /// Get information about the user’s current playback state, including track, track progress, and active device. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetPlayback(string market = "") - { - return string.IsNullOrEmpty(market) ? $"{APIBase}/me/player" : $"{APIBase}/me/player?market={market}"; - } - - /// - /// Get the object currently being played on the user’s Spotify account. - /// - /// An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. - /// - public string GetPlayingTrack(string market = "") - { - return string.IsNullOrEmpty(market) - ? $"{APIBase}/me/player/currently-playing" - : $"{APIBase}/me/player/currently-playing?market={market}"; - } - - /// - /// Transfer playback to a new device and determine if it should start playing. - /// - /// - public string TransferPlayback() - { - return $"{APIBase}/me/player"; - } - - /// - /// Start a new context or resume current playback on the user’s active device. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string ResumePlayback(string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/play" - : $"{APIBase}/me/player/play?device_id={deviceId}"; - } - - /// - /// Pause playback on the user’s account. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string PausePlayback(string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/pause" - : $"{APIBase}/me/player/pause?device_id={deviceId}"; - } - - /// - /// Skips to next track in the user’s queue. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SkipPlaybackToNext(string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/next" - : $"{APIBase}/me/player/next?device_id={deviceId}"; - } - - /// - /// Skips to previous track in the user’s queue. - /// Note that this will ALWAYS skip to the previous track, regardless of the current track’s progress. - /// Returning to the start of the current track should be performed using the https://api.spotify.com/v1/me/player/seek endpoint. - /// - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SkipPlaybackToPrevious(string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/previous" - : $"{APIBase}/me/player/previous?device_id={deviceId}"; - } - - /// - /// Seeks to the given position in the user’s currently playing track. - /// - /// The position in milliseconds to seek to. Must be a positive number. - /// Passing in a position that is greater than the length of the track will cause the player to start playing the next song. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SeekPlayback(int positionMs, string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/seek?position_ms={positionMs}" - : $"{APIBase}/me/player/seek?position_ms={positionMs}&device_id={deviceId}"; - } - - /// - /// Set the repeat mode for the user’s playback. Options are repeat-track, repeat-context, and off. - /// - /// track, context or off. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SetRepeatMode(RepeatState repeatState, string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/repeat?state={repeatState.GetStringAttribute()}" - : $"{APIBase}/me/player/repeat?state={repeatState.GetStringAttribute()}&device_id={deviceId}"; - } - - /// - /// Set the volume for the user’s current playback device. - /// - /// Integer. The volume to set. Must be a value from 0 to 100 inclusive. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SetVolume(int volumePercent, string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/volume?volume_percent={volumePercent}" - : $"{APIBase}/me/player/volume?volume_percent={volumePercent}&device_id={deviceId}"; - } - - /// - /// Toggle shuffle on or off for user’s playback. - /// - /// True of False. - /// The id of the device this command is targeting. If not supplied, the user's currently active device is the target. - /// - public string SetShuffle(bool shuffle, string deviceId = "") - { - return string.IsNullOrEmpty(deviceId) - ? $"{APIBase}/me/player/shuffle?state={shuffle}" - : $"{APIBase}/me/player/shuffle?state={shuffle}&device_id={deviceId}"; - } - #endregion - } -} diff --git a/Source Files/SpotifyAPI.Web/SpotifyWebClient.cs b/Source Files/SpotifyAPI.Web/SpotifyWebClient.cs deleted file mode 100644 index 54b2ff4..0000000 --- a/Source Files/SpotifyAPI.Web/SpotifyWebClient.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using SpotifyAPI.Web.Models; - -namespace SpotifyAPI.Web -{ - internal class SpotifyWebClient : IClient - { - public JsonSerializerSettings JsonSettings { get; set; } - private readonly Encoding _encoding = Encoding.UTF8; - private readonly HttpClient _client; - - private const string UnknownErrorJson = "{\"error\": { \"status\": 0, \"message\": \"SpotifyAPI.Web - Unkown Spotify Error\" }}"; - - public SpotifyWebClient(ProxyConfig proxyConfig = null) - { - HttpClientHandler clientHandler = CreateClientHandler(proxyConfig); - _client = new HttpClient(clientHandler); - } - - public Tuple Download(string url, Dictionary headers = null) - { - Tuple raw = DownloadRaw(url, headers); - return new Tuple(raw.Item1, raw.Item2.Length > 0 ? _encoding.GetString(raw.Item2) : "{}"); - } - - public async Task> DownloadAsync(string url, Dictionary headers = null) - { - Tuple raw = await DownloadRawAsync(url, headers).ConfigureAwait(false); - return new Tuple(raw.Item1, raw.Item2.Length > 0 ? _encoding.GetString(raw.Item2) : "{}"); - } - - public Tuple DownloadRaw(string url, Dictionary headers = null) - { - if (headers != null) - { - AddHeaders(headers); - } - using (HttpResponseMessage response = Task.Run(() => _client.GetAsync(url)).Result) - { - return new Tuple(new ResponseInfo - { - StatusCode = response.StatusCode, - Headers = ConvertHeaders(response.Headers) - }, Task.Run(() => response.Content.ReadAsByteArrayAsync()).Result); - } - } - - public async Task> DownloadRawAsync(string url, Dictionary headers = null) - { - if (headers != null) - { - AddHeaders(headers); - } - using (HttpResponseMessage response = await _client.GetAsync(url).ConfigureAwait(false)) - { - return new Tuple(new ResponseInfo - { - StatusCode = response.StatusCode, - Headers = ConvertHeaders(response.Headers) - }, await response.Content.ReadAsByteArrayAsync()); - } - } - - public Tuple DownloadJson(string url, Dictionary headers = null) - { - Tuple response = Download(url, headers); - try - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(response.Item2, JsonSettings)); - } - catch (JsonException) - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(UnknownErrorJson, JsonSettings)); - } - } - - public async Task> DownloadJsonAsync(string url, Dictionary headers = null) - { - Tuple response = await DownloadAsync(url, headers).ConfigureAwait(false);try - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(response.Item2, JsonSettings)); - } - catch (JsonException) - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(UnknownErrorJson, JsonSettings)); - } - } - - public Tuple Upload(string url, string body, string method, Dictionary headers = null) - { - Tuple data = UploadRaw(url, body, method, headers); - return new Tuple(data.Item1, data.Item2.Length > 0 ? _encoding.GetString(data.Item2) : "{}"); - } - - public async Task> UploadAsync(string url, string body, string method, Dictionary headers = null) - { - Tuple data = await UploadRawAsync(url, body, method, headers).ConfigureAwait(false); - return new Tuple(data.Item1, data.Item2.Length > 0 ? _encoding.GetString(data.Item2) : "{}"); - } - - public Tuple UploadRaw(string url, string body, string method, Dictionary headers = null) - { - if (headers != null) - { - AddHeaders(headers); - } - - HttpRequestMessage message = new HttpRequestMessage(new HttpMethod(method), url) - { - Content = new StringContent(body, _encoding) - }; - using (HttpResponseMessage response = Task.Run(() => _client.SendAsync(message)).Result) - { - return new Tuple(new ResponseInfo - { - StatusCode = response.StatusCode, - Headers = ConvertHeaders(response.Headers) - }, Task.Run(() => response.Content.ReadAsByteArrayAsync()).Result); - } - } - - public async Task> UploadRawAsync(string url, string body, string method, Dictionary headers = null) - { - if (headers != null) - { - AddHeaders(headers); - } - - HttpRequestMessage message = new HttpRequestMessage(new HttpMethod(method), url) - { - Content = new StringContent(body, _encoding) - }; - using (HttpResponseMessage response = await _client.SendAsync(message)) - { - return new Tuple(new ResponseInfo - { - StatusCode = response.StatusCode, - Headers = ConvertHeaders(response.Headers) - }, await response.Content.ReadAsByteArrayAsync()); - } - } - - public Tuple UploadJson(string url, string body, string method, Dictionary headers = null) - { - Tuple response = Upload(url, body, method, headers); - try - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(response.Item2, JsonSettings)); - } - catch (JsonException) - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(UnknownErrorJson, JsonSettings)); - } - } - - public async Task> UploadJsonAsync(string url, string body, string method, Dictionary headers = null) - { - Tuple response = await UploadAsync(url, body, method, headers).ConfigureAwait(false); - try - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(response.Item2, JsonSettings)); - } - catch (JsonException) - { - return new Tuple(response.Item1, JsonConvert.DeserializeObject(UnknownErrorJson, JsonSettings)); - } - } - - public void Dispose() - { - _client.Dispose(); - GC.SuppressFinalize(this); - } - - private static WebHeaderCollection ConvertHeaders(HttpResponseHeaders headers) - { - WebHeaderCollection newHeaders = new WebHeaderCollection(); - foreach (KeyValuePair> headerPair in headers) - { - foreach (string headerValue in headerPair.Value) - { - newHeaders.Add(headerPair.Key, headerValue); - } - } - return newHeaders; - } - - private void AddHeaders(Dictionary headers) - { - _client.DefaultRequestHeaders.Clear(); - foreach (KeyValuePair headerPair in headers) - { - _client.DefaultRequestHeaders.TryAddWithoutValidation(headerPair.Key, headerPair.Value); - } - } - - private static HttpClientHandler CreateClientHandler(ProxyConfig proxyConfig = null) - { - HttpClientHandler clientHandler = new HttpClientHandler - { - PreAuthenticate = false, - UseDefaultCredentials = true, - UseProxy = false - }; - - if (string.IsNullOrWhiteSpace(proxyConfig?.Host)) return clientHandler; - WebProxy proxy = proxyConfig.CreateWebProxy(); - clientHandler.UseProxy = true; - clientHandler.Proxy = proxy; - clientHandler.UseDefaultCredentials = proxy.UseDefaultCredentials; - clientHandler.PreAuthenticate = proxy.UseDefaultCredentials; - - return clientHandler; - } - } -} \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web/Util.cs b/Source Files/SpotifyAPI.Web/Util.cs deleted file mode 100644 index 5d6df3c..0000000 --- a/Source Files/SpotifyAPI.Web/Util.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace SpotifyAPI.Web -{ - public static class Util - { - public static string GetStringAttribute(this T en, string separator = "") where T : struct, IConvertible - { - Enum e = (Enum)(object)en; - IEnumerable attributes = - Enum.GetValues(typeof(T)) - .Cast() - .Where(v => e.HasFlag((Enum)(object)v)) - .Select(v => typeof(T).GetField(v.ToString(CultureInfo.InvariantCulture))) - .Select(f => f.GetCustomAttributes(typeof(StringAttribute), false)[0]) - .Cast(); - - List list = new List(); - attributes.ToList().ForEach(element => list.Add(element.Text)); - return string.Join(separator, list); - } - - public static long ToUnixTimeMillisecondsPoly(this DateTime time) - { - return (long)time.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds; - } - } - - public sealed class StringAttribute : Attribute - { - public string Text { get; set; } - - public StringAttribute(string text) - { - Text = text; - } - } -} diff --git a/Source Files/SpotifyAPI.Web/Util/Base64Util.cs b/Source Files/SpotifyAPI.Web/Util/Base64Util.cs new file mode 100644 index 0000000..e1e91e1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/Base64Util.cs @@ -0,0 +1,132 @@ + +using System; +using System.Globalization; + +namespace SpotifyAPI.Web +{ + internal class Base64Util + { + internal const string WebEncoders_InvalidCountOffsetOrLength = "Invalid {0}, {1} or {2} length."; + internal const string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length."; + + public static string UrlEncode(byte[] input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // Special-case empty input + if (input.Length == 0) + { + return string.Empty; + } + + var buffer = new char[GetArraySizeRequiredToEncode(input.Length)]; + var numBase64Chars = Convert.ToBase64CharArray(input, 0, input.Length, buffer, 0); + + // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. + for (var i = 0; i < numBase64Chars; i++) + { + var ch = buffer[i]; + if (ch == '+') + { + buffer[i] = '-'; + } + else if (ch == '/') + { + buffer[i] = '_'; + } + else if (ch == '=') + { + return new string(buffer, startIndex: 0, length: i); + } + } + + return new string(buffer, startIndex: 0, length: numBase64Chars); + } + + public static byte[] UrlDecode(string input) + { + var buffer = new char[GetArraySizeRequiredToDecode(input.Length)]; + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // Assumption: input is base64url encoded without padding and contains no whitespace. + + var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(input.Length); + var arraySizeRequired = checked(input.Length + paddingCharsToAdd); + + // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'. + var i = 0; + for (var j = 0; i < input.Length; i++, j++) + { + var ch = input[j]; + if (ch == '-') + { + buffer[i] = '+'; + } + else if (ch == '_') + { + buffer[i] = '/'; + } + else + { + buffer[i] = ch; + } + } + + // Add the padding characters back. + for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--) + { + buffer[i] = '='; + } + + // Decode. + // If the caller provided invalid base64 chars, they'll be caught here. + return Convert.FromBase64CharArray(buffer, 0, arraySizeRequired); + } + + private static int GetArraySizeRequiredToEncode(int count) + { + var numWholeOrPartialInputBlocks = checked(count + 2) / 3; + return checked(numWholeOrPartialInputBlocks * 4); + } + + private static int GetArraySizeRequiredToDecode(int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (count == 0) + { + return 0; + } + + var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); + + return checked(count + numPaddingCharsToAdd); + } + + private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) + { + return (inputLength % 4) switch + { + 0 => 0, + 2 => 2, + 3 => 1, + _ => throw new FormatException( + string.Format( + CultureInfo.CurrentCulture, + WebEncoders_MalformedInput, + inputLength)), + }; + } + } + + +} diff --git a/Source Files/SpotifyAPI.Web/Util/Ensure.cs b/Source Files/SpotifyAPI.Web/Util/Ensure.cs new file mode 100644 index 0000000..5b8e46b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/Ensure.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SpotifyAPI.Web +{ + /// + /// Ensure input parameters + /// + internal static class Ensure + { + /// + /// Checks an argument to ensure it isn't null. + /// + /// The argument value to check + /// The name of the argument + public static void ArgumentNotNull(object value, string name) + { + if (value != null) + { + return; + } + + throw new ArgumentNullException(name); + } + + /// + /// Checks an argument to ensure it isn't null or an empty string + /// + /// The argument value to check + /// The name of the argument + public static void ArgumentNotNullOrEmptyString(string value, string name) + { + if (!string.IsNullOrEmpty(value)) + { + return; + } + + throw new ArgumentException("String is empty or null", name); + } + + public static void ArgumentNotNullOrEmptyList(IEnumerable value, string name) + { + if (value != null && value.Any()) + { + return; + } + + throw new ArgumentException("List is empty or null", name); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Util/PKCEUtil.cs b/Source Files/SpotifyAPI.Web/Util/PKCEUtil.cs new file mode 100644 index 0000000..55157a8 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/PKCEUtil.cs @@ -0,0 +1,69 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SpotifyAPI.Web +{ + public static class PKCEUtil + { + private const int VERIFY_MIN_LENGTH = 43; + private const int VERIFY_MAX_LENGTH = 128; + private const int VERIFY_DEFAULT_LENGTH = 100; + + /// + /// Generate a verifier and challenge pair using RNGCryptoServiceProvider + /// + /// The length of the generated verifier + /// + public static (string verifier, string challenge) GenerateCodes(int length = VERIFY_DEFAULT_LENGTH) + { + if (length < VERIFY_MIN_LENGTH || length > VERIFY_MAX_LENGTH) + { + throw new ArgumentException( + $"length must be between {VERIFY_MIN_LENGTH} and {VERIFY_MAX_LENGTH}", + nameof(length) + ); + } + + var verifier = GenerateRandomURLSafeString(length); + return GenerateCodes(verifier); + } + + /// + /// Return the paseed verifier and its challenge + /// + /// A secure random generated verifier + /// + public static (string verifier, string challenge) GenerateCodes(string verifier) + { + Ensure.ArgumentNotNull(verifier, nameof(verifier)); + + if (verifier.Length < VERIFY_MIN_LENGTH || verifier.Length > VERIFY_MAX_LENGTH) + { + throw new ArgumentException( + $"length must be between {VERIFY_MIN_LENGTH} and {VERIFY_MAX_LENGTH}", + nameof(verifier) + ); + } + + var challenge = Base64Util.UrlEncode(ComputeSHA256(verifier)); + return (verifier, challenge); + } + + private static string GenerateRandomURLSafeString(int length) + { + using var rng = new RNGCryptoServiceProvider(); + var bit_count = length * 6; + var byte_count = (bit_count + 7) / 8; // rounded up + var bytes = new byte[byte_count]; + rng.GetBytes(bytes); + return Base64Util.UrlEncode(bytes); + } + + private static byte[] ComputeSHA256(string value) + { + using var hash = SHA256.Create(); + return hash.ComputeHash(Encoding.UTF8.GetBytes(value)); + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Util/StringAttribute.cs b/Source Files/SpotifyAPI.Web/Util/StringAttribute.cs new file mode 100644 index 0000000..bc13301 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/StringAttribute.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; +using System.Linq; +using System.Diagnostics.CodeAnalysis; + +namespace SpotifyAPI.Web +{ + [AttributeUsage(AttributeTargets.Field)] + public sealed class StringAttribute : Attribute + { + public StringAttribute(string value) + { + Value = value; + } + + public string Value { get; } + +#if NETSTANDARD2_0 + public static bool GetValue(Type enumType, Enum enumValue, out string? result) +#else + public static bool GetValue(Type enumType, Enum enumValue, [NotNullWhen(true)] out string? result) +#endif + { + Ensure.ArgumentNotNull(enumType, nameof(enumType)); + Ensure.ArgumentNotNull(enumValue, nameof(enumValue)); + + if (enumType + .GetMember(enumValue.ToString())[0] + .GetCustomAttributes(typeof(StringAttribute)) + .FirstOrDefault() is StringAttribute stringAttr) + { + result = stringAttr.Value; + return true; + } + result = null; + return false; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Util/URIExtension.cs b/Source Files/SpotifyAPI.Web/Util/URIExtension.cs new file mode 100644 index 0000000..eae2e87 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/URIExtension.cs @@ -0,0 +1,42 @@ +using System.Web; +using System.Linq; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace SpotifyAPI.Web +{ + public static class URIExtensions + { + public static Uri ApplyParameters(this Uri uri, IDictionary parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + if (parameters == null || !parameters.Any()) + { + return uri; + } + + var newParameters = new Dictionary(); + NameValueCollection existingParameters = HttpUtility.ParseQueryString(uri.Query); + foreach (string key in existingParameters) + { + newParameters.Add(key, existingParameters[key]!); + } + foreach (KeyValuePair parameter in parameters) + { + newParameters.Add(parameter.Key, HttpUtility.UrlEncode(parameter.Value)); + } + + var queryString = string.Join("&", newParameters.Select((parameter) => $"{parameter.Key}={parameter.Value}")); + var query = string.IsNullOrEmpty(queryString) ? null : queryString; + + var uriBuilder = new UriBuilder(uri) + { + Query = query + }; + + return uriBuilder.Uri; + } + } +} diff --git a/Source Files/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs b/Source Files/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs new file mode 100644 index 0000000..3fb91a1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util/URIParameterFormatProvider.cs @@ -0,0 +1,27 @@ +using System.Web; +using System; +namespace SpotifyAPI.Web +{ + public class URIParameterFormatProvider : IFormatProvider + { + private readonly URIParameterFormatter _formatter; + + public URIParameterFormatProvider() + { + _formatter = new URIParameterFormatter(); + } + + public object? GetFormat(Type? formatType) + { + return formatType == typeof(ICustomFormatter) ? _formatter : null; + } + + private class URIParameterFormatter : ICustomFormatter + { + public string Format(string? format, object? arg, IFormatProvider? formatProvider) + { + return HttpUtility.UrlEncode(arg?.ToString()) ?? string.Empty; + } + } + } +} diff --git a/Source Files/SpotifyIntegration.cs b/Source Files/SpotifyIntegration.cs index 0430fe5..0a59851 100644 --- a/Source Files/SpotifyIntegration.cs +++ b/Source Files/SpotifyIntegration.cs @@ -1,12 +1,13 @@ using SpotifyAPI.Web; using SpotifyAPI.Web.Auth; -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows.Forms; +using System.Threading.Tasks; +using System.Xml.Serialization; +using System.Xml; +using System.Security.Cryptography; namespace MusicBeePlugin { @@ -14,51 +15,180 @@ namespace MusicBeePlugin public partial class Plugin { - private static SpotifyWebAPI _spotify; + private static SpotifyClient _spotify; private static int _auth, _num, _trackMissing = 0; - private static bool _trackLIB, _albumLIB, _artistLIB, _runOnce = false; - private static string _title, _album, _artist, _trackID, _albumID, _artistID, _imageURL; + private static bool _trackLIB, _albumLIB, _artistLIB = false; + private static string _title, _album, _artist, _trackID, _albumID, _artistID, _imageURL, _path; + private static string _clientID = "9076681768d94feda885a7b5eced926d"; - - - static async void SpotifyWebAuth(bool autoRefresh) + public void SerializeConfig(PKCETokenResponse data, string path, RSACryptoServiceProvider rsaKey) { - ImplicitGrantAuth auth = - new ImplicitGrantAuth("777824a07eeb4312972ff5fcec54c565", "http://localhost:4002", "http://localhost:4002", Scope.UserLibraryModify | Scope.UserFollowModify | Scope.UserFollowRead | Scope.UserLibraryRead); - auth.AuthReceived += async (sender, payload) => + using (StreamWriter file = new StreamWriter(path, false)) { + XmlSerializer controlsDefaultsSerializer = new XmlSerializer(typeof(PKCETokenResponse)); + controlsDefaultsSerializer.Serialize(file, data); + file.Close(); + } - auth.Stop(); // `sender` is also the auth instance - _spotify = new SpotifyWebAPI() { TokenType = payload.TokenType, AccessToken = payload.AccessToken }; + try + { + // Encrypt + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.PreserveWhitespace = true; + xmlDoc.Load(path); + Encrypt(xmlDoc, "AccessToken", "AccessToken", rsaKey, "rsaKey"); + Encrypt(xmlDoc, "RefreshToken", "RefreshToken", _rsaKey, "rsaKey"); + xmlDoc.Save(path); - - }; - auth.Start(); // Starts an internal HTTP Server + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } - auth.OpenBrowser(autoRefresh); + } + + public PKCETokenResponse DeserializeConfig(string path, RSACryptoServiceProvider rsaKey) + { - _auth = 1; - _runOnce = true; + try + { + // Decrypt + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.PreserveWhitespace = true; + xmlDoc.Load(path); + Decrypt(xmlDoc, rsaKey, "rsaKey"); + xmlDoc.Save(path); + + // Deserialize + StreamReader file = new StreamReader(path); + XmlSerializer xSerial = new XmlSerializer(typeof(PKCETokenResponse)); + object oData = xSerial.Deserialize(file); + var thisConfig = (PKCETokenResponse)oData; + file.Close(); + return thisConfig; + } + catch (Exception e) + { + Console.Write(e.Message); + return null; + } + finally + { + // Encrypt + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.PreserveWhitespace = true; + xmlDoc.Load(path); + Encrypt(xmlDoc, "AccessToken", "AccessToken", rsaKey, "rsaKey"); + Encrypt(xmlDoc, "RefreshToken", "RefreshToken", rsaKey, "rsaKey"); + xmlDoc.Save(path); + } } + //static void WriteOutput(PKCETokenResponse InitialToken) + //{ + // MessageBox.Show( + // InitialToken.AccessToken + '\n' + + // InitialToken.CreatedAt.ToString() + '\n' + + // InitialToken.ExpiresIn + '\n' + + // InitialToken.Scope + '\n' + + // InitialToken.TokenType + '\n' + + // InitialToken.RefreshToken + '\n' + // ); + //} - public FullTrack TrackSearch() + async void SpotifyWebAuth() { - - - SearchItem track = _spotify.SearchItems(_searchTerm, SearchType.Track, 10); + try + { + if(File.Exists(_path)) + { + var token_response = DeserializeConfig(_path, _rsaKey); + + //WriteOutput(token_response); + + var authenticator = new PKCEAuthenticator(_clientID, token_response, _path); + + var config = SpotifyClientConfig.CreateDefault() + .WithAuthenticator(authenticator); + _spotify = new SpotifyClient(config); + + SerializeConfig(token_response, _path, _rsaKey); + + //WriteOutput(token_response); + + // This appears to be the easiest way to check if the Spotify client works, but it's not great: + try + { + await _spotify.Search.Item(new SearchRequest(SearchRequest.Types.Track, "fasdofimasdofiasdnfaosnf")); + _auth = 1; + } + catch(APIException) + { + Console.WriteLine("Spotify agent dead."); + throw new System.NullReferenceException(); + } - if (track.HasError()) + } + else { throw new System.NullReferenceException(); } + } + catch (System.NullReferenceException) { + var (verifier, challenge) = PKCEUtil.GenerateCodes(120); - _trackMissing = 1; - Console.WriteLine("Error Status: " + track.Error.Status); - Console.WriteLine("Error Msg: " + track.Error.Message); + var loginRequest = new LoginRequest( + new Uri("http://localhost:5000/callback"), _clientID, LoginRequest.ResponseType.Code) + { + CodeChallengeMethod = "S256", + CodeChallenge = challenge, + Scope = new[] { Scopes.UserLibraryModify, Scopes.UserFollowModify, Scopes.UserFollowRead, Scopes.UserLibraryRead } + }; + var uri = loginRequest.ToUri(); + + var server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000); + + server.PkceReceived += async (sender, response) => + { + await server.Stop(); + + var initialResponse = await new OAuthClient().RequestToken( + new PKCETokenRequest(_clientID, response.Code, server.BaseUri, verifier) + ); + + //WriteOutput(initialResponse); + + var authenticator = new PKCEAuthenticator(_clientID, initialResponse, _path); + var config = SpotifyClientConfig.CreateDefault() + .WithAuthenticator(authenticator); + _spotify = new SpotifyClient(config); + + //WriteOutput(initialResponse); + SerializeConfig(initialResponse, _path, _rsaKey); + }; + await server.Start(); + + try + { + BrowserUtil.Open(uri); + } + catch (Exception) + { + Console.WriteLine("Unable to open URL, manually open: {0}", uri); + } + + _auth = 1; } - else if (track.Tracks.Total >= 1) + + } + + public async Task TrackSearch() + { + + try { + var track = await _spotify.Search.Item(new SearchRequest(SearchRequest.Types.Track, _searchTerm)); _title = Truncate(track.Tracks.Items[_num].Name, largeBold); _artist = Truncate(string.Join(", ", from item in track.Tracks.Items[_num].Artists select item.Name), smallRegular); _album = Truncate(track.Tracks.Items[_num].Album.Name, smallRegular); @@ -66,91 +196,190 @@ public FullTrack TrackSearch() _albumID = track.Tracks.Items[_num].Album.Id; _artistID = track.Tracks.Items[_num].Artists[0].Id; _imageURL = track.Tracks.Items[_num].Album.Images[0].Url; - + _trackMissing = 0; + return null; } - else + catch (APIUnauthorizedException e) { - + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + return null; + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + return null; + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); _trackMissing = 1; - + return null; } - - return null; - } - + } public void SaveTrack() { - - ErrorResponse response = _spotify.SaveTrack(_trackID); - if (!response.HasError()) - MessageBox.Show("Track Saved."); - else - MessageBox.Show(response.Error.Message); + try + { + var track = new LibrarySaveTracksRequest(new List { _trackID }); + _spotify.Library.SaveTracks(track); + Console.WriteLine("Track Saved."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } } public void SaveAlbum() { - - ErrorResponse response = _spotify.SaveAlbum(_albumID); - if (!response.HasError()) - MessageBox.Show("Album Saved."); - else - MessageBox.Show(response.Error.Message); - + try + { + var album = new LibrarySaveAlbumsRequest(new List { _albumID }); + _spotify.Library.SaveAlbums(album); + Console.WriteLine("Album Saved."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } + } public void FollowArtist() { - ErrorResponse response = _spotify.Follow(FollowType.Artist, _artistID); - if (!response.HasError()) - MessageBox.Show("Artist Followed."); - else - MessageBox.Show(response.Error.Message); - + try + { + var artist = new FollowRequest(FollowRequest.Type.Artist, new List { _artistID }); + _spotify.Follow.Follow(artist); + Console.WriteLine("Artist Followed."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } + } public void RemoveTrack() { - - ErrorResponse response = _spotify.RemoveSavedTracks(new List { _trackID }); - if (!response.HasError()) - MessageBox.Show("Track Unsaved."); - else - MessageBox.Show(response.Error.Message); - + try + { + var track = new LibraryRemoveTracksRequest(new List { _trackID }); + _spotify.Library.RemoveTracks(track); + Console.WriteLine("Track Unsaved."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } + } public void RemoveAlbum() { - ErrorResponse response = _spotify.RemoveSavedAlbums(new List { _albumID}); - if (!response.HasError()) - MessageBox.Show("Album Unsaved."); - else - MessageBox.Show(response.Error.Message); - + try + { + var album = new LibraryRemoveAlbumsRequest(new List { _albumID }); + _spotify.Library.RemoveAlbums(album); + Console.WriteLine("Album Unsaved."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } + } public void UnfollowArtist() { - ErrorResponse response = _spotify.Unfollow(FollowType.Artist, _artistID); - if (!response.HasError()) - MessageBox.Show("Artist Unfollowed."); - else - MessageBox.Show(response.Error.Message); - + try + { + var artist = new UnfollowRequest(UnfollowRequest.Type.Artist, new List { _artistID }); + _spotify.Follow.Unfollow(artist); + Console.WriteLine("Artist Unfollowed."); + } + catch (APIUnauthorizedException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (APIException e) + { + Console.WriteLine("Error Status: " + e.Response); + Console.WriteLine("Error Msg: " + e.Message); + } + catch (System.ArgumentOutOfRangeException e) + { + Console.WriteLine("Song not found!"); + } + } public Boolean CheckTrack(string id) { - ListResponse tracksSaved = _spotify.CheckSavedTracks(new List { id }); - if (tracksSaved.List[0]) + var tracks = new LibraryCheckTracksRequest(new List { id }); + + List tracksSaved = _spotify.Library.CheckTracks(tracks).Result; + if (tracksSaved.ElementAt(0)) { _trackLIB = true; return true; @@ -164,38 +393,31 @@ public Boolean CheckTrack(string id) public Boolean CheckAlbum(string id) { - //API Code which doesn't currently work correctly. - //ListResponse albumsSaved = _spotify.CheckSavedAlbums(new List { id }); - //if (albumsSaved.List[0]) - - foreach (string line in File.ReadLines(_savedAlbumsPath)) + var albums = new LibraryCheckAlbumsRequest(new List { id }); + + List albumsSaved = _spotify.Library.CheckAlbums(albums).Result; + if (albumsSaved.ElementAt(0)) { - if (line.Contains(_albumID)) - { - _albumLIB = true; - return true; - } - else - { - _albumLIB = false; - } + _albumLIB = true; + return true; } - - - if (_albumLIB) - { return true; } else - { return false; } - + { + _albumLIB = false; + return false; + } - } + } public Boolean CheckArtist(string id) { - ListResponse response = _spotify.IsFollowing(FollowType.Artist, id); - if (response.List[0] == true) + + var artist = new FollowCheckCurrentUserRequest(FollowCheckCurrentUserRequest.Type.Artist, new List { id }); + + List artistFollowed = _spotify.Follow.CheckCurrentUser(artist).Result; + if (artistFollowed.ElementAt(0)) { _artistLIB = true; return true; @@ -207,38 +429,6 @@ public Boolean CheckArtist(string id) } } - // Workaround for Spotify API "Check-Users-Saved-Albums" Endpoint bug. - public void GenerateAlbumList() - { - - int offset = 0; - - using (System.IO.StreamWriter file = new System.IO.StreamWriter(_savedAlbumsPath)) - { - - - while (offset != -1) - { - - Paging savedAlbums = _spotify.GetSavedAlbums(50, offset); - savedAlbums.Items.ForEach(album => file.WriteLine(album.Album.Id)); - - if (savedAlbums.Next == null) - { - offset += -1; - break; - } - else - { - offset += 50; - } - - } - file.Close(); - } - - } - } } diff --git a/Source Files/mb_Spotify-Plugin.csproj b/Source Files/mb_Spotify-Plugin.csproj index a53f1c6..8ca000a 100644 --- a/Source Files/mb_Spotify-Plugin.csproj +++ b/Source Files/mb_Spotify-Plugin.csproj @@ -32,17 +32,16 @@ 4 - - False - SpotifyAPI.Web\bin\Debug\net46\SpotifyAPI.Web.dll + + SpotifyAPI.Web.Auth\bin\Debug\netstandard2.0\SpotifyAPI.Web.dll - - False - SpotifyAPI.Web.Auth\bin\Debug\net46\SpotifyAPI.Web.Auth.dll + + SpotifyAPI.Web.Auth\bin\Debug\netstandard2.0\SpotifyAPI.Web.Auth.dll + @@ -52,6 +51,7 @@ + @@ -59,6 +59,6 @@ - copy /Y "C:\Users\Zachary\Desktop\Core File\Other\Installers and Important Stuff\MusicBee Plugin Development\DEVELOPMENT\mb_Spotify-Plugin\Source Files\bin\Debug\mb_Spotify-Plugin.dll" "C:\Program Files (x86)\MusicBee\Plugins\mb_Spotify-Plugin.dll" + copy /Y "C:\Users\zacha\Desktop\Local Files\MusicBee Plugin Development\DEVELOPMENT\mb_Spotify-Plugin\Source Files\bin\Debug\mb_Spotify-Plugin.dll" "C:\Users\zacha\Desktop\Local Files\MusicBee\Plugins\mb_Spotify-Plugin.dll" \ No newline at end of file diff --git a/Source Files/mb_Spotify-Plugin.sln b/Source Files/mb_Spotify-Plugin.sln index e35b199..f7055dc 100644 --- a/Source Files/mb_Spotify-Plugin.sln +++ b/Source Files/mb_Spotify-Plugin.sln @@ -1,15 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.421 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31112.23 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mb_Spotify-Plugin", "mb_Spotify-Plugin.csproj", "{EA4E342C-C766-4C68-A28B-7ADBE7C3667D}" + ProjectSection(ProjectDependencies) = postProject + {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0} = {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0} + {C222F9E6-9053-49E1-A1A0-F5A409159847} = {C222F9E6-9053-49E1-A1A0-F5A409159847} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86A2B1EE-465E-449F-ADD3-F8FD52FE7F11}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web", "..\Source Files\SpotifyAPI.Web\SpotifyAPI.Web.csproj", "{C222F9E6-9053-49E1-A1A0-F5A409159847}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web", "SpotifyAPI.Web\SpotifyAPI.Web.csproj", "{C222F9E6-9053-49E1-A1A0-F5A409159847}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web.Auth", "..\Source Files\SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web.Auth", "SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution