diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8debf01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,351 @@ + +# Created by https://www.gitignore.io/api/visualstudio +# Edit at https://www.gitignore.io/?templates=visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/visualstudio \ No newline at end of file diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..df7a88f --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,4 @@ +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 diff --git a/PluginPanel.png b/PluginPanel.png new file mode 100644 index 0000000..77942e8 Binary files /dev/null and b/PluginPanel.png differ diff --git a/Plugins/Newtonsoft.Json.dll b/Plugins/Newtonsoft.Json.dll new file mode 100644 index 0000000..d0aaed9 Binary files /dev/null and b/Plugins/Newtonsoft.Json.dll differ diff --git a/Plugins/SpotifyAPI.Web.Auth.dll b/Plugins/SpotifyAPI.Web.Auth.dll new file mode 100644 index 0000000..129f936 Binary files /dev/null and b/Plugins/SpotifyAPI.Web.Auth.dll differ diff --git a/Plugins/SpotifyAPI.Web.dll b/Plugins/SpotifyAPI.Web.dll new file mode 100644 index 0000000..d1830fc Binary files /dev/null and b/Plugins/SpotifyAPI.Web.dll differ diff --git a/Plugins/Unosquare.Labs.EmbedIO.dll b/Plugins/Unosquare.Labs.EmbedIO.dll new file mode 100644 index 0000000..89c106e Binary files /dev/null and b/Plugins/Unosquare.Labs.EmbedIO.dll differ diff --git a/Plugins/Unosquare.Swan.Lite.dll b/Plugins/Unosquare.Swan.Lite.dll new file mode 100644 index 0000000..882dafd Binary files /dev/null and b/Plugins/Unosquare.Swan.Lite.dll differ diff --git a/Plugins/mb_Spotify-Plugin.dll b/Plugins/mb_Spotify-Plugin.dll new file mode 100644 index 0000000..b79f436 Binary files /dev/null and b/Plugins/mb_Spotify-Plugin.dll differ diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..f57a434 --- /dev/null +++ b/README.txt @@ -0,0 +1,16 @@ +DESCRIPTION: + +This plugin integrates Spotify with your MusicBee library. You can add albums and tracks and follow artists directly from MusicBee, through the plugin interface. + + +INSTALLATION: + +Just extract the contents of the "plugins" folder to your MusicBee Plugins directory. + +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 diff --git a/Source Files/MusicBeeInterface.cs b/Source Files/MusicBeeInterface.cs new file mode 100644 index 0000000..2a672b3 --- /dev/null +++ b/Source Files/MusicBeeInterface.cs @@ -0,0 +1,825 @@ +using System; +using System.Runtime.InteropServices; + +namespace MusicBeePlugin +{ + public partial class Plugin + { + public const short PluginInfoVersion = 1; + public const short MinInterfaceVersion = 36; + public const short MinApiRevision = 48; + + [StructLayout(LayoutKind.Sequential)] + public struct MusicBeeApiInterface + { + public void Initialise(IntPtr apiInterfacePtr) + { + CopyMemory(ref this, apiInterfacePtr, 4); + if (MusicBeeVersion == MusicBeeVersion.v2_0) + // MusicBee version 2.0 - Api methods > revision 25 are not available + CopyMemory(ref this, apiInterfacePtr, 456); + else if (MusicBeeVersion == MusicBeeVersion.v2_1) + CopyMemory(ref this, apiInterfacePtr, 516); + else if (MusicBeeVersion == MusicBeeVersion.v2_2) + CopyMemory(ref this, apiInterfacePtr, 584); + else if (MusicBeeVersion == MusicBeeVersion.v2_3) + CopyMemory(ref this, apiInterfacePtr, 596); + else if (MusicBeeVersion == MusicBeeVersion.v2_4) + CopyMemory(ref this, apiInterfacePtr, 604); + else if (MusicBeeVersion == MusicBeeVersion.v2_5) + CopyMemory(ref this, apiInterfacePtr, 648); + else + CopyMemory(ref this, apiInterfacePtr, Marshal.SizeOf(this)); + } + public MusicBeeVersion MusicBeeVersion + { + get { + if (ApiRevision <= 25) + return MusicBeeVersion.v2_0; + else if (ApiRevision <= 31) + return MusicBeeVersion.v2_1; + else if (ApiRevision <= 33) + return MusicBeeVersion.v2_2; + else if (ApiRevision <= 38) + return MusicBeeVersion.v2_3; + else if (ApiRevision <= 43) + return MusicBeeVersion.v2_4; + else if (ApiRevision <= 47) + return MusicBeeVersion.v2_5; + else + return MusicBeeVersion.v3_0; + } + } + public short InterfaceVersion; + public short ApiRevision; + public MB_ReleaseStringDelegate MB_ReleaseString; + public MB_TraceDelegate MB_Trace; + public Setting_GetPersistentStoragePathDelegate Setting_GetPersistentStoragePath; + public Setting_GetSkinDelegate Setting_GetSkin; + public Setting_GetSkinElementColourDelegate Setting_GetSkinElementColour; + public Setting_IsWindowBordersSkinnedDelegate Setting_IsWindowBordersSkinned; + public Library_GetFilePropertyDelegate Library_GetFileProperty; + public Library_GetFileTagDelegate Library_GetFileTag; + public Library_SetFileTagDelegate Library_SetFileTag; + public Library_CommitTagsToFileDelegate Library_CommitTagsToFile; + public Library_GetLyricsDelegate Library_GetLyrics; + [Obsolete("Use Library_GetArtworkEx")] + public Library_GetArtworkDelegate Library_GetArtwork; + public Library_QueryFilesDelegate Library_QueryFiles; + public Library_QueryGetNextFileDelegate Library_QueryGetNextFile; + public Player_GetPositionDelegate Player_GetPosition; + public Player_SetPositionDelegate Player_SetPosition; + public Player_GetPlayStateDelegate Player_GetPlayState; + public Player_ActionDelegate Player_PlayPause; + public Player_ActionDelegate Player_Stop; + public Player_ActionDelegate Player_StopAfterCurrent; + public Player_ActionDelegate Player_PlayPreviousTrack; + public Player_ActionDelegate Player_PlayNextTrack; + public Player_ActionDelegate Player_StartAutoDj; + public Player_ActionDelegate Player_EndAutoDj; + public Player_GetVolumeDelegate Player_GetVolume; + public Player_SetVolumeDelegate Player_SetVolume; + public Player_GetMuteDelegate Player_GetMute; + public Player_SetMuteDelegate Player_SetMute; + public Player_GetShuffleDelegate Player_GetShuffle; + public Player_SetShuffleDelegate Player_SetShuffle; + public Player_GetRepeatDelegate Player_GetRepeat; + public Player_SetRepeatDelegate Player_SetRepeat; + public Player_GetEqualiserEnabledDelegate Player_GetEqualiserEnabled; + public Player_SetEqualiserEnabledDelegate Player_SetEqualiserEnabled; + public Player_GetDspEnabledDelegate Player_GetDspEnabled; + public Player_SetDspEnabledDelegate Player_SetDspEnabled; + public Player_GetScrobbleEnabledDelegate Player_GetScrobbleEnabled; + public Player_SetScrobbleEnabledDelegate Player_SetScrobbleEnabled; + public NowPlaying_GetFileUrlDelegate NowPlaying_GetFileUrl; + public NowPlaying_GetDurationDelegate NowPlaying_GetDuration; + public NowPlaying_GetFilePropertyDelegate NowPlaying_GetFileProperty; + public NowPlaying_GetFileTagDelegate NowPlaying_GetFileTag; + public NowPlaying_GetLyricsDelegate NowPlaying_GetLyrics; + public NowPlaying_GetArtworkDelegate NowPlaying_GetArtwork; + public NowPlayingList_ActionDelegate NowPlayingList_Clear; + public Library_QueryFilesDelegate NowPlayingList_QueryFiles; + public Library_QueryGetNextFileDelegate NowPlayingList_QueryGetNextFile; + public NowPlayingList_FileActionDelegate NowPlayingList_PlayNow; + public NowPlayingList_FileActionDelegate NowPlayingList_QueueNext; + public NowPlayingList_FileActionDelegate NowPlayingList_QueueLast; + public NowPlayingList_ActionDelegate NowPlayingList_PlayLibraryShuffled; + public Playlist_QueryPlaylistsDelegate Playlist_QueryPlaylists; + public Playlist_QueryGetNextPlaylistDelegate Playlist_QueryGetNextPlaylist; + public Playlist_GetTypeDelegate Playlist_GetType; + public Playlist_QueryFilesDelegate Playlist_QueryFiles; + public Library_QueryGetNextFileDelegate Playlist_QueryGetNextFile; + public MB_WindowHandleDelegate MB_GetWindowHandle; + public MB_RefreshPanelsDelegate MB_RefreshPanels; + public MB_SendNotificationDelegate MB_SendNotification; + public MB_AddMenuItemDelegate MB_AddMenuItem; + public Setting_GetFieldNameDelegate Setting_GetFieldName; + [Obsolete("Use Library_QueryFilesEx", true)] + public Library_QueryGetAllFilesDelegate Library_QueryGetAllFiles; + [Obsolete("Use NowPlayingList_QueryFilesEx", true)] + public Library_QueryGetAllFilesDelegate NowPlayingList_QueryGetAllFiles; + [Obsolete("Use Playlist_QueryFilesEx", true)] + public Library_QueryGetAllFilesDelegate Playlist_QueryGetAllFiles; + public MB_CreateBackgroundTaskDelegate MB_CreateBackgroundTask; + public MB_SetBackgroundTaskMessageDelegate MB_SetBackgroundTaskMessage; + public MB_RegisterCommandDelegate MB_RegisterCommand; + public Setting_GetDefaultFontDelegate Setting_GetDefaultFont; + public Player_GetShowTimeRemainingDelegate Player_GetShowTimeRemaining; + public NowPlayingList_GetCurrentIndexDelegate NowPlayingList_GetCurrentIndex; + public NowPlayingList_GetFileUrlDelegate NowPlayingList_GetListFileUrl; + public NowPlayingList_GetFilePropertyDelegate NowPlayingList_GetFileProperty; + public NowPlayingList_GetFileTagDelegate NowPlayingList_GetFileTag; + public NowPlaying_GetSpectrumDataDelegate NowPlaying_GetSpectrumData; + public NowPlaying_GetSoundGraphDelegate NowPlaying_GetSoundGraph; + public MB_GetPanelBoundsDelegate MB_GetPanelBounds; + public MB_AddPanelDelegate MB_AddPanel; + public MB_RemovePanelDelegate MB_RemovePanel; + public MB_GetLocalisationDelegate MB_GetLocalisation; + public NowPlayingList_IsAnyPriorTracksDelegate NowPlayingList_IsAnyPriorTracks; + public NowPlayingList_IsAnyFollowingTracksDelegate NowPlayingList_IsAnyFollowingTracks; + public Player_ShowEqualiserDelegate Player_ShowEqualiser; + public Player_GetAutoDjEnabledDelegate Player_GetAutoDjEnabled; + public Player_GetStopAfterCurrentEnabledDelegate Player_GetStopAfterCurrentEnabled; + public Player_GetCrossfadeDelegate Player_GetCrossfade; + public Player_SetCrossfadeDelegate Player_SetCrossfade; + public Player_GetReplayGainModeDelegate Player_GetReplayGainMode; + public Player_SetReplayGainModeDelegate Player_SetReplayGainMode; + public Player_QueueRandomTracksDelegate Player_QueueRandomTracks; + public Setting_GetDataTypeDelegate Setting_GetDataType; + public NowPlayingList_GetNextIndexDelegate NowPlayingList_GetNextIndex; + public NowPlaying_GetArtistPictureDelegate NowPlaying_GetArtistPicture; + public NowPlaying_GetArtworkDelegate NowPlaying_GetDownloadedArtwork; + // api version 16 + public MB_ShowNowPlayingAssistantDelegate MB_ShowNowPlayingAssistant; + // api version 17 + public NowPlaying_GetLyricsDelegate NowPlaying_GetDownloadedLyrics; + // api version 18 + public Player_GetShowRatingTrackDelegate Player_GetShowRatingTrack; + public Player_GetShowRatingLoveDelegate Player_GetShowRatingLove; + // api version 19 + public MB_CreateParameterisedBackgroundTaskDelegate MB_CreateParameterisedBackgroundTask; + public Setting_GetLastFmUserIdDelegate Setting_GetLastFmUserId; + public Playlist_GetNameDelegate Playlist_GetName; + public Playlist_CreatePlaylistDelegate Playlist_CreatePlaylist; + public Playlist_SetFilesDelegate Playlist_SetFiles; + public Library_QuerySimilarArtistsDelegate Library_QuerySimilarArtists; + public Library_QueryLookupTableDelegate Library_QueryLookupTable; + public Library_QueryGetLookupTableValueDelegate Library_QueryGetLookupTableValue; + public NowPlayingList_FilesActionDelegate NowPlayingList_QueueFilesNext; + public NowPlayingList_FilesActionDelegate NowPlayingList_QueueFilesLast; + // api version 20 + public Setting_GetWebProxyDelegate Setting_GetWebProxy; + // api version 21 + public NowPlayingList_RemoveAtDelegate NowPlayingList_RemoveAt; + // api version 22 + public Playlist_RemoveAtDelegate Playlist_RemoveAt; + // api version 23 + public MB_SetPanelScrollableAreaDelegate MB_SetPanelScrollableArea; + // api version 24 + public MB_InvokeCommandDelegate MB_InvokeCommand; + public MB_OpenFilterInTabDelegate MB_OpenFilterInTab; + // api version 25 + public MB_SetWindowSizeDelegate MB_SetWindowSize; + public Library_GetArtistPictureDelegate Library_GetArtistPicture; + public Pending_GetFileUrlDelegate Pending_GetFileUrl; + public Pending_GetFilePropertyDelegate Pending_GetFileProperty; + public Pending_GetFileTagDelegate Pending_GetFileTag; + // api version 26 + public Player_GetButtonEnabledDelegate Player_GetButtonEnabled; + // api version 27 + public NowPlayingList_MoveFilesDelegate NowPlayingList_MoveFiles; + // api version 28 + public Library_GetArtworkDelegate Library_GetArtworkUrl; + public Library_GetArtistPictureThumbDelegate Library_GetArtistPictureThumb; + public NowPlaying_GetArtworkDelegate NowPlaying_GetArtworkUrl; + public NowPlaying_GetArtworkDelegate NowPlaying_GetDownloadedArtworkUrl; + public NowPlaying_GetArtistPictureThumbDelegate NowPlaying_GetArtistPictureThumb; + // api version 29 + public Playlist_IsInListDelegate Playlist_IsInList; + // api version 30 + public Library_GetArtistPictureUrlsDelegate Library_GetArtistPictureUrls; + public NowPlaying_GetArtistPictureUrlsDelegate NowPlaying_GetArtistPictureUrls; + // api version 31 + public Playlist_AddFilesDelegate Playlist_AppendFiles; + // api version 32 + public Sync_FileStartDelegate Sync_FileStart; + public Sync_FileEndDelegate Sync_FileEnd; + // api version 33 + public Library_QueryFilesExDelegate Library_QueryFilesEx; + public Library_QueryFilesExDelegate NowPlayingList_QueryFilesEx; + public Playlist_QueryFilesExDelegate Playlist_QueryFilesEx; + public Playlist_MoveFilesDelegate Playlist_MoveFiles; + public Playlist_PlayNowDelegate Playlist_PlayNow; + public NowPlaying_IsSoundtrackDelegate NowPlaying_IsSoundtrack; + public NowPlaying_GetArtistPictureUrlsDelegate NowPlaying_GetSoundtrackPictureUrls; + public Library_GetDevicePersistentIdDelegate Library_GetDevicePersistentId; + public Library_SetDevicePersistentIdDelegate Library_SetDevicePersistentId; + public Library_FindDevicePersistentIdDelegate Library_FindDevicePersistentId; + public Setting_GetValueDelegate Setting_GetValue; + public Library_AddFileToLibraryDelegate Library_AddFileToLibrary; + public Playlist_DeletePlaylistDelegate Playlist_DeletePlaylist; + public Library_GetSyncDeltaDelegate Library_GetSyncDelta; + // api version 35 + public Library_GetFileTagsDelegate Library_GetFileTags; + public NowPlaying_GetFileTagsDelegate NowPlaying_GetFileTags; + public NowPlayingList_GetFileTagsDelegate NowPlayingList_GetFileTags; + // api version 43 + public MB_AddTreeNodeDelegate MB_AddTreeNode; + public MB_DownloadFileDelegate MB_DownloadFile; + // api version 47 + public Setting_GetFileConvertCommandLineDelegate Setting_GetFileConvertCommandLine; + public Player_OpenStreamHandleDelegate Player_OpenStreamHandle; + public Player_UpdatePlayStatisticsDelegate Player_UpdatePlayStatistics; + public Library_GetArtworkExDelegate Library_GetArtworkEx; + public Library_SetArtworkExDelegate Library_SetArtworkEx; + public MB_GetVisualiserInformationDelegate MB_GetVisualiserInformation; + public MB_ShowVisualiserDelegate MB_ShowVisualiser; + public MB_GetPluginViewInformationDelegate MB_GetPluginViewInformation; + public MB_ShowPluginViewDelegate MB_ShowPluginView; + public Player_GetOutputDevicesDelegate Player_GetOutputDevices; + public Player_SetOutputDeviceDelegate Player_SetOutputDevice; + // api version 48 + public MB_UninistallPluginDelegate MB_UninstallPlugin; + } + + public enum MusicBeeVersion + { + v2_0 = 0, + v2_1 = 1, + v2_2 = 2, + v2_3 = 3, + v2_4 = 4, + v2_5 = 5, + v3_0 = 6 + } + + public enum PluginType + { + Unknown = 0, + General = 1, + LyricsRetrieval = 2, + ArtworkRetrieval = 3, + PanelView = 4, + DataStream = 5, + InstantMessenger = 6, + Storage = 7, + VideoPlayer = 8, + DSP = 9, + TagRetrieval = 10, + TagOrArtworkRetrieval = 11, + Upnp = 12 + } + + [StructLayout(LayoutKind.Sequential)] + public class PluginInfo + { + public short PluginInfoVersion; + public PluginType Type; + public string Name; + public string Description; + public string Author; + public string TargetApplication; + public short VersionMajor; + public short VersionMinor; + public short Revision; + public short MinInterfaceVersion; + public short MinApiRevision; + public ReceiveNotificationFlags ReceiveNotifications; + public int ConfigurationPanelHeight; + } + + [Flags()] + public enum ReceiveNotificationFlags + { + StartupOnly = 0x0, + PlayerEvents = 0x1, + DataStreamEvents = 0x2, + TagEvents = 0x04, + DownloadEvents = 0x08 + } + + public enum NotificationType + { + PluginStartup = 0, // notification sent after successful initialisation for an enabled plugin + TrackChanging = 16, + TrackChanged = 1, + PlayStateChanged = 2, + AutoDjStarted = 3, + AutoDjStopped = 4, + VolumeMuteChanged = 5, + VolumeLevelChanged = 6, + NowPlayingListChanged = 7, + NowPlayingListEnded = 18, + NowPlayingArtworkReady = 8, + NowPlayingLyricsReady = 9, + TagsChanging = 10, + TagsChanged = 11, + RatingChanging = 15, + RatingChanged = 12, + PlayCountersChanged = 13, + ScreenSaverActivating = 14, + ShutdownStarted = 17, + EmbedInPanel = 19, + PlayerRepeatChanged = 20, + PlayerShuffleChanged = 21, + PlayerEqualiserOnOffChanged = 22, + PlayerScrobbleChanged = 23, + ReplayGainChanged = 24, + FileDeleting = 25, + FileDeleted = 26, + ApplicationWindowChanged = 27, + StopAfterCurrentChanged = 28, + LibrarySwitched = 29, + FileAddedToLibrary = 30, + FileAddedToInbox = 31, + SynchCompleted = 32, + DownloadCompleted = 33, + MusicBeeStarted = 34 + } + + public enum PluginCloseReason + { + MusicBeeClosing = 1, + UserDisabled = 2, + StopNoUnload = 3 + } + + public enum CallbackType + { + SettingsUpdated = 1, + StorageReady = 2, + StorageFailed = 3, + FilesRetrievedChanged = 4, + FilesRetrievedNoChange = 5, + FilesRetrievedFail = 6, + LyricsDownloaded = 7, + StorageEject = 8, + SuspendPlayCounters = 9, + ResumePlayCounters = 10, + EnablePlugin = 11, + DisablePlugin = 12, + RenderingDevicesChanged = 13, + FullscreenOn = 14, + FullscreenOff = 15 + } + + public enum FilePropertyType + { + Url = 2, + Kind = 4, + Format = 5, + Size = 7, + Channels = 8, + SampleRate = 9, + Bitrate = 10, + DateModified = 11, + DateAdded = 12, + LastPlayed = 13, + PlayCount = 14, + SkipCount = 15, + Duration = 16, + Status = 21, + NowPlayingListIndex = 78, // only has meaning when called from NowPlayingList_* commands + ReplayGainTrack = 94, + ReplayGainAlbum = 95 + } + + public enum MetaDataType + { + TrackTitle = 65, + Album = 30, + AlbumArtist = 31, // displayed album artist + AlbumArtistRaw = 34, // stored album artist + Artist = 32, // displayed artist + MultiArtist = 33, // individual artists, separated by a null char + PrimaryArtist = 19, // first artist from multi-artist tagged file, otherwise displayed artist + Artists = 144, + ArtistsWithArtistRole = 145, + ArtistsWithPerformerRole = 146, + ArtistsWithGuestRole = 147, + ArtistsWithRemixerRole = 148, + Artwork = 40, + BeatsPerMin = 41, + Composer = 43, // displayed composer + MultiComposer = 89, // individual composers, separated by a null char + Comment = 44, + Conductor = 45, + Custom1 = 46, + Custom2 = 47, + Custom3 = 48, + Custom4 = 49, + Custom5 = 50, + Custom6 = 96, + Custom7 = 97, + Custom8 = 98, + Custom9 = 99, + Custom10 = 128, + Custom11 = 129, + Custom12 = 130, + Custom13 = 131, + Custom14 = 132, + Custom15 = 133, + Custom16 = 134, + DiscNo = 52, + DiscCount = 54, + Encoder = 55, + Genre = 59, + Genres = 143, + GenreCategory = 60, + Grouping = 61, + Keywords = 84, + HasLyrics = 63, + Lyricist = 62, + Lyrics = 114, + Mood = 64, + Occasion = 66, + Origin = 67, + Publisher = 73, + Quality = 74, + Rating = 75, + RatingLove = 76, + RatingAlbum = 104, + Tempo = 85, + TrackNo = 86, + TrackCount = 87, + Virtual1 = 109, + Virtual2 = 110, + Virtual3 = 111, + Virtual4 = 112, + Virtual5 = 113, + Virtual6 = 122, + Virtual7 = 123, + Virtual8 = 124, + Virtual9 = 125, + Virtual10 = 135, + Virtual11 = 136, + Virtual12 = 137, + Virtual13 = 138, + Virtual14 = 139, + Virtual15 = 140, + Virtual16 = 141, + Year = 88 + } + + public enum FileCodec + { + Unknown = -1, + Mp3 = 1, + Aac = 2, + Flac = 3, + Ogg = 4, + WavPack = 5, + Wma = 6, + Tak = 7, + Mpc = 8, + Wave = 9, + Asx = 10, + Alac = 11, + Aiff = 12, + Pcm = 13, + Opus = 15, + Spx = 16, + Dsd = 17, + AacNoContainer = 18 + } + + public enum EncodeQuality + { + SmallSize = 1, + Portable = 2, + HighQuality = 3, + Archiving = 4 + } + + [Flags()] + public enum LibraryCategory + { + Music = 0, + Audiobook = 1, + Video = 2, + Inbox = 4 + } + + public enum DeviceIdType + { + GooglePlay = 1, + AppleDevice = 2, + GooglePlay2 = 3, + AppleDevice2 = 4 + } + + public enum DataType + { + String = 0, + Number = 1, + DateTime = 2, + Rating = 3 + } + + public enum SettingId + { + CompactPlayerFlickrEnabled = 1, + FileTaggingPreserveModificationTime = 2, + LastDownloadFolder = 3, + ArtistGenresOnly = 4, + IgnoreNamePrefixes = 5, + IgnoreNameChars = 6, + PlayCountTriggerPercent = 7, + PlayCountTriggerSeconds = 8, + SkipCountTriggerPercent = 9, + SkipCountTriggerSeconds = 10, + CustomWebLinkName1 = 11, + CustomWebLinkName2 = 12, + CustomWebLinkName3 = 13, + CustomWebLinkName4 = 14, + CustomWebLinkName5 = 15, + CustomWebLinkName6 = 16, + CustomWebLink1 = 17, + CustomWebLink2 = 18, + CustomWebLink3 = 19, + CustomWebLink4 = 20, + CustomWebLink5 = 21, + CustomWebLink6 = 22, + CustomWebLinkNowPlaying1 = 23, + CustomWebLinkNowPlaying2 = 24, + CustomWebLinkNowPlaying3 = 25, + CustomWebLinkNowPlaying4 = 26, + CustomWebLinkNowPlaying5 = 27, + CustomWebLinkNowPlaying6 = 28 + } + + public enum ComparisonType + { + Is = 0, + IsSimilar = 20 + } + + public enum LyricsType + { + NotSpecified = 0, + Synchronised = 1, + UnSynchronised = 2 + } + + public enum PlayState + { + Undefined = 0, + Loading = 1, + Playing = 3, + Paused = 6, + Stopped = 7 + } + + public enum RepeatMode + { + None = 0, + All = 1, + One = 2 + } + + public enum PlayButtonType + { + PreviousTrack = 0, + PlayPause = 1, + NextTrack = 2, + Stop = 3 + } + + public enum PlaylistFormat + { + Unknown = 0, + M3u = 1, + Xspf = 2, + Asx = 3, + Wpl = 4, + Pls = 5, + Auto = 7, + M3uAscii = 8, + AsxFile = 9, + Radio = 10, + M3uExtended = 11, + Mbp = 12 + } + + public enum SkinElement + { + SkinInputControl = 7, + SkinInputPanel = 10, + SkinInputPanelLabel = 14, + SkinTrackAndArtistPanel = -1 + } + + public enum ElementState + { + ElementStateDefault = 0, + ElementStateModified = 6 + } + + public enum ElementComponent + { + ComponentBorder = 0, + ComponentBackground = 1, + ComponentForeground = 3 + } + + public enum PluginPanelDock + { + ApplicationWindow = 0, + TrackAndArtistPanel = 1, + TextBox = 3, + ComboBox = 4, + MainPanel = 5 + } + + + public enum ReplayGainMode + { + Off = 0, + Track = 1, + Album = 2, + Smart = 3 + } + + public enum PlayStatisticType + { + NoChange = 0, + IncreasePlayCount = 1, + IncreaseSkipCount = 2 + } + + public enum Command + { + NavigateTo = 1 + } + + public enum DownloadTarget + { + Inbox = 0, + MusicLibrary = 1, + SpecificFolder = 3 + } + + [Flags()] + public enum PictureLocations: byte + { + None = 0, + EmbedInFile = 1, + LinkToOrganisedCopy = 2, + LinkToSource = 4, + FolderThumb = 8 + } + + public enum WindowState + { + Off = -1, + Normal = 0, + Fullscreen = 1, + Desktop = 2 + } + + public delegate void MB_ReleaseStringDelegate(string p1); + public delegate void MB_TraceDelegate(string p1); + public delegate IntPtr MB_WindowHandleDelegate(); + public delegate void MB_RefreshPanelsDelegate(); + public delegate void MB_SendNotificationDelegate(CallbackType type); + public delegate System.Windows.Forms.ToolStripItem MB_AddMenuItemDelegate(string menuPath, string hotkeyDescription, EventHandler handler); + public delegate bool MB_AddTreeNodeDelegate(string treePath, string name, System.Drawing.Bitmap icon, EventHandler openHandler, EventHandler closeHandler); + public delegate void MB_RegisterCommandDelegate(string command, EventHandler handler); + public delegate void MB_CreateBackgroundTaskDelegate(System.Threading.ThreadStart taskCallback, System.Windows.Forms.Form owner); + public delegate void MB_CreateParameterisedBackgroundTaskDelegate(System.Threading.ParameterizedThreadStart taskCallback, object parameters, System.Windows.Forms.Form owner); + public delegate void MB_SetBackgroundTaskMessageDelegate(string message); + public delegate System.Drawing.Rectangle MB_GetPanelBoundsDelegate(PluginPanelDock dock); + public delegate bool MB_SetPanelScrollableAreaDelegate(System.Windows.Forms.Control panel, System.Drawing.Size scrollArea, bool alwaysShowScrollBar); + public delegate System.Windows.Forms.Control MB_AddPanelDelegate(System.Windows.Forms.Control panel, PluginPanelDock dock); + public delegate void MB_RemovePanelDelegate(System.Windows.Forms.Control panel); + public delegate string MB_GetLocalisationDelegate(string id, string defaultText); + public delegate bool MB_ShowNowPlayingAssistantDelegate(); + public delegate bool MB_InvokeCommandDelegate(Command command, object parameter); + public delegate bool MB_OpenFilterInTabDelegate(MetaDataType field1, ComparisonType comparison1, string value1, MetaDataType field2, ComparisonType comparison2, string value2); + public delegate bool MB_SetWindowSizeDelegate(int width, int height); + public delegate bool MB_DownloadFileDelegate(string url, DownloadTarget target, string targetFolder, bool cancelDownload); + public delegate bool MB_GetVisualiserInformationDelegate(out string[] visualiserNames, out string defaultVisualiserName, out WindowState defaultState, out WindowState currentState); + public delegate bool MB_ShowVisualiserDelegate(string visualiserName, WindowState state); + public delegate bool MB_GetPluginViewInformationDelegate(string pluginFilename, out string[] viewNames, out string defaultViewName, out WindowState defaultState, out WindowState currentState); + public delegate bool MB_ShowPluginViewDelegate(string pluginFilename, string viewName, WindowState state); + public delegate bool MB_UninistallPluginDelegate(string pluginFilename, string password); + public delegate string Setting_GetFieldNameDelegate(MetaDataType field); + public delegate string Setting_GetPersistentStoragePathDelegate(); + public delegate string Setting_GetSkinDelegate(); + public delegate int Setting_GetSkinElementColourDelegate(SkinElement element, ElementState state, ElementComponent component); + public delegate bool Setting_IsWindowBordersSkinnedDelegate(); + public delegate System.Drawing.Font Setting_GetDefaultFontDelegate(); + public delegate DataType Setting_GetDataTypeDelegate(MetaDataType field); + public delegate string Setting_GetLastFmUserIdDelegate(); + public delegate string Setting_GetWebProxyDelegate(); + public delegate bool Setting_GetValueDelegate(SettingId settingId, ref object value); + public delegate string Setting_GetFileConvertCommandLineDelegate(FileCodec codec, EncodeQuality encodeQuality); + public delegate string Library_GetFilePropertyDelegate(string sourceFileUrl, FilePropertyType type); + public delegate string Library_GetFileTagDelegate(string sourceFileUrl, MetaDataType field); + public delegate bool Library_GetFileTagsDelegate(string sourceFileUrl, MetaDataType[] fields, ref string[] results); + public delegate bool Library_SetFileTagDelegate(string sourceFileUrl, MetaDataType field, string value); + public delegate string Library_GetDevicePersistentIdDelegate(string sourceFileUrl, DeviceIdType idType); + public delegate bool Library_SetDevicePersistentIdDelegate(string sourceFileUrl, DeviceIdType idType, string value); + public delegate bool Library_FindDevicePersistentIdDelegate(DeviceIdType idType, string[] ids, ref string[] values); + public delegate bool Library_CommitTagsToFileDelegate(string sourceFileUrl); + public delegate string Library_AddFileToLibraryDelegate(string sourceFileUrl, LibraryCategory category); + public delegate bool Library_GetSyncDeltaDelegate(string[] cachedFiles, DateTime updatedSince, LibraryCategory categories, ref string[] newFiles, ref string[] updatedFiles, ref string[] deletedFiles); + public delegate string Library_GetLyricsDelegate(string sourceFileUrl, LyricsType type); + public delegate string Library_GetArtworkDelegate(string sourceFileUrl, int index); + public delegate bool Library_GetArtworkExDelegate(string sourceFileUrl, int index, bool retrievePictureData, ref PictureLocations pictureLocations, ref string pictureUrl, ref byte[] imageData); + public delegate bool Library_SetArtworkExDelegate(string sourceFileUrl, int index, byte[] imageData); + public delegate string Library_GetArtistPictureDelegate(string artistName, int fadingPercent, int fadingColor); + public delegate bool Library_GetArtistPictureUrlsDelegate(string artistName, bool localOnly, ref string[] urls); + public delegate string Library_GetArtistPictureThumbDelegate(string artistName); + public delegate bool Library_QueryFilesDelegate(string query); + public delegate string Library_QueryGetNextFileDelegate(); + public delegate string Library_QueryGetAllFilesDelegate(); + public delegate bool Library_QueryFilesExDelegate(string query, ref string[] files); + public delegate string Library_QuerySimilarArtistsDelegate(string artistName, double minimumArtistSimilarityRating); + public delegate bool Library_QueryLookupTableDelegate(string keyTags, string valueTags, string query); + public delegate string Library_QueryGetLookupTableValueDelegate(string key); + public delegate int Player_GetPositionDelegate(); + public delegate bool Player_SetPositionDelegate(int position); + public delegate PlayState Player_GetPlayStateDelegate(); + public delegate bool Player_GetButtonEnabledDelegate(PlayButtonType button); + public delegate bool Player_ActionDelegate(); + public delegate int Player_QueueRandomTracksDelegate(int count); + public delegate float Player_GetVolumeDelegate(); + public delegate bool Player_SetVolumeDelegate(float volume); + public delegate bool Player_GetMuteDelegate(); + public delegate bool Player_SetMuteDelegate(bool mute); + public delegate bool Player_GetShuffleDelegate(); + public delegate bool Player_SetShuffleDelegate(bool shuffle); + public delegate RepeatMode Player_GetRepeatDelegate(); + public delegate bool Player_SetRepeatDelegate(RepeatMode repeat); + public delegate bool Player_GetEqualiserEnabledDelegate(); + public delegate bool Player_SetEqualiserEnabledDelegate(bool enabled); + public delegate bool Player_GetDspEnabledDelegate(); + public delegate bool Player_SetDspEnabledDelegate(bool enabled); + public delegate bool Player_GetScrobbleEnabledDelegate(); + public delegate bool Player_SetScrobbleEnabledDelegate(bool enabled); + public delegate bool Player_GetShowTimeRemainingDelegate(); + public delegate bool Player_GetShowRatingTrackDelegate(); + public delegate bool Player_GetShowRatingLoveDelegate(); + public delegate bool Player_ShowEqualiserDelegate(); + public delegate bool Player_GetAutoDjEnabledDelegate(); + public delegate bool Player_GetStopAfterCurrentEnabledDelegate(); + public delegate bool Player_GetCrossfadeDelegate(); + public delegate bool Player_SetCrossfadeDelegate(bool crossfade); + public delegate ReplayGainMode Player_GetReplayGainModeDelegate(); + public delegate bool Player_SetReplayGainModeDelegate(ReplayGainMode mode); + public delegate int Player_OpenStreamHandleDelegate(string url, bool useMusicBeeSettings, bool enableDsp, ReplayGainMode gainType); + public delegate bool Player_UpdatePlayStatisticsDelegate(string url, PlayStatisticType countType, bool disableScrobble); + public delegate bool Player_GetOutputDevicesDelegate(out string[] deviceNames, out string activeDeviceName); + public delegate bool Player_SetOutputDeviceDelegate(string deviceName); + public delegate string NowPlaying_GetFileUrlDelegate(); + public delegate int NowPlaying_GetDurationDelegate(); + public delegate string NowPlaying_GetFilePropertyDelegate(FilePropertyType type); + public delegate string NowPlaying_GetFileTagDelegate(MetaDataType field); + public delegate bool NowPlaying_GetFileTagsDelegate(MetaDataType[] fields, ref string[] results); + public delegate string NowPlaying_GetLyricsDelegate(); + public delegate string NowPlaying_GetArtworkDelegate(); + public delegate string NowPlaying_GetArtistPictureDelegate(int fadingPercent); + public delegate bool NowPlaying_GetArtistPictureUrlsDelegate(bool localOnly, ref string[] urls); + public delegate string NowPlaying_GetArtistPictureThumbDelegate(); + public delegate bool NowPlaying_IsSoundtrackDelegate(); + public delegate int NowPlaying_GetSpectrumDataDelegate(float[] fftData); + public delegate bool NowPlaying_GetSoundGraphDelegate(float[] graphData); + public delegate int NowPlayingList_GetCurrentIndexDelegate(); + public delegate int NowPlayingList_GetNextIndexDelegate(int offset); + public delegate bool NowPlayingList_IsAnyPriorTracksDelegate(); + public delegate bool NowPlayingList_IsAnyFollowingTracksDelegate(); + public delegate string NowPlayingList_GetFileUrlDelegate(int index); + public delegate string NowPlayingList_GetFilePropertyDelegate(int index, FilePropertyType type); + public delegate string NowPlayingList_GetFileTagDelegate(int index, MetaDataType field); + public delegate bool NowPlayingList_GetFileTagsDelegate(int index, MetaDataType[] fields, ref string[] results); + public delegate bool NowPlayingList_ActionDelegate(); + public delegate bool NowPlayingList_FileActionDelegate(string sourceFileUrl); + public delegate bool NowPlayingList_FilesActionDelegate(string[] sourceFileUrl); + public delegate bool NowPlayingList_RemoveAtDelegate(int index); + public delegate bool NowPlayingList_MoveFilesDelegate(int[] fromIndices, int toIndex); + public delegate string Playlist_GetNameDelegate(string playlistUrl); + public delegate PlaylistFormat Playlist_GetTypeDelegate(string playlistUrl); + public delegate bool Playlist_QueryPlaylistsDelegate(); + public delegate string Playlist_QueryGetNextPlaylistDelegate(); + public delegate bool Playlist_IsInListDelegate(string playlistUrl, string filename); + public delegate bool Playlist_QueryFilesDelegate(string playlistUrl); + public delegate bool Playlist_QueryFilesExDelegate(string playlistUrl, ref string[] filenames); + public delegate string Playlist_CreatePlaylistDelegate(string folderName, string playlistName, string[] filenames); + public delegate bool Playlist_DeletePlaylistDelegate(string playlistUrl); + public delegate bool Playlist_SetFilesDelegate(string playlistUrl, string[] filenames); + public delegate bool Playlist_AddFilesDelegate(string playlistUrl, string[] filenames); + public delegate bool Playlist_RemoveAtDelegate(string playlistUrl, int index); + public delegate bool Playlist_MoveFilesDelegate(string playlistUrl, int[] fromIndices, int toIndex); + public delegate bool Playlist_PlayNowDelegate(string playlistUrl); + public delegate string Pending_GetFileUrlDelegate(); + public delegate string Pending_GetFilePropertyDelegate(FilePropertyType field); + public delegate string Pending_GetFileTagDelegate(MetaDataType field); + public delegate string Sync_FileStartDelegate(string filename); + public delegate void Sync_FileEndDelegate(string filename, bool success, string errorMessage); + + [System.Security.SuppressUnmanagedCodeSecurity()] + [DllImport("kernel32.dll")] + private static extern void CopyMemory(ref MusicBeeApiInterface mbApiInterface, IntPtr src, int length); + } +} \ No newline at end of file diff --git a/Source Files/PanelInterface.cs b/Source Files/PanelInterface.cs new file mode 100644 index 0000000..c30c07c --- /dev/null +++ b/Source Files/PanelInterface.cs @@ -0,0 +1,362 @@ +using System; +using System.IO; +using System.Drawing; +using System.Windows.Forms; +using System.Net; +using System.Threading; +using System.Collections.Generic; +using Microsoft.Win32; + +namespace MusicBeePlugin +{ + + public partial class Plugin + { + private MusicBeeApiInterface mbApiInterface; + private PluginInfo about = new PluginInfo(); + private Control panel; + public int panelHeight; + private static string _searchTerm, _savedAlbumsPath; + Font largeBold, smallRegular, smallBold; + static System.Threading.Timer authTimer; + + static void TickTimer(object state) + { + _auth = 0; + } + + 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.TargetApplication = "Spotify Plugin"; + about.Type = PluginType.PanelView; + about.VersionMajor = 2; + about.VersionMinor = 0; + about.Revision = 2; + about.MinInterfaceVersion = MinInterfaceVersion; + about.MinApiRevision = MinApiRevision; + about.ReceiveNotifications = (ReceiveNotificationFlags.PlayerEvents | ReceiveNotificationFlags.TagEvents); + about.ConfigurationPanelHeight = 0; + + _savedAlbumsPath = mbApiInterface.Setting_GetPersistentStoragePath() + @"spotify.txt"; + + SystemEvents.PowerModeChanged += OnPowerChange; + + return about; + } + + + public int OnDockablePanelCreated(Control panel) + { + + float dpiScaling = 0; + + + largeBold = new Font(panel.Font.FontFamily, 9, FontStyle.Bold); + smallRegular = new Font(panel.Font.FontFamily, 8); + smallBold = new Font(panel.Font.FontFamily, 8, FontStyle.Bold); + + panel.Paint += DrawPanel; + panel.Click += PanelClick; + + this.panel = panel; + panelHeight = Convert.ToInt32(145 * dpiScaling); + return panelHeight; + } + + public string Truncate(string text, Font font) + { + + if (TextRenderer.MeasureText(text + "...", font).Width < panel.Width) + { + + return text; + + } + else + { + + int i = text.Length; + while (TextRenderer.MeasureText(text + "...", font).Width > panel.Width) + { + text = text.Substring(0, --i); + if (i == 0) break; + } + + return text = text + "..."; + } + + } + + private void DrawPanel(object sender, PaintEventArgs e) + { + + var bg = panel.BackColor; + var text1 = panel.ForeColor; + var text2 = text1; + var highlight = Color.FromArgb(2021216); + e.Graphics.Clear(bg); + panel.Cursor = Cursors.Hand; + + // re-draws when file is found? + if (_auth == 1 && _trackMissing != 1) + { + //MessageBox.Show("Drawing."); + + TextRenderer.DrawText(e.Graphics, _title, largeBold, new Point(5, 10), text1); + TextRenderer.DrawText(e.Graphics, _artist, smallRegular, new Point(5, 30), text1); + TextRenderer.DrawText(e.Graphics, _album, smallRegular, new Point(5, 50), text1); + + WebClient webClient = new WebClient(); + byte[] data = webClient.DownloadData(_imageURL); + System.Drawing.Image image = System.Drawing.Image.FromStream(new MemoryStream(data)); + image = new Bitmap(image, new Size(65, 65)); + 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); + } + + + } + else if (_auth == 1 && _trackMissing == 1) + { + + //MessageBox.Show("Drawing."); + TextRenderer.DrawText(e.Graphics, "No Track Found!", new Font(panel.Font.FontFamily, 12), new Point(5, 70), text1); + + } + else if (_auth == 0) + { + //MessageBox.Show("Drawing."); + TextRenderer.DrawText(e.Graphics, "Please Click Here to \nAuthenticate Spotify.", new Font(panel.Font.FontFamily, 14), new Point(4, 50), text1); + + } + + + } + + public List GetMenuItems() + { + List list = new List(); + ToolStripMenuItem reAuth = new ToolStripMenuItem("Re-authenticate"); + + reAuth.Click += reAuthSpotify; + + list.Add(reAuth); + + return list; + } + + + + private void OnPowerChange(object s, PowerModeChangedEventArgs e) + { + switch (e.Mode) + { + case PowerModes.Resume: + + _auth = 0; + + break; + } + } + + public void reAuthSpotify(object sender, EventArgs e) + { + SpotifyWebAuth(false); + _trackMissing = 1; + panel.Invalidate(); + } + + private void PanelClick(object sender, EventArgs e) + { + + + MouseEventArgs me = (MouseEventArgs)e; + if (_auth == 0 && me.Button == System.Windows.Forms.MouseButtons.Left) + { + + SpotifyWebAuth(false); + _trackMissing = 1; + + panel.Invalidate(); + //panel.Paint += DrawPanel; + + } + else if (_auth == 1 && me.Button == System.Windows.Forms.MouseButtons.Left) + { + + Point point = panel.PointToClient(Cursor.Position); + float currentPosX = point.X; + float currentPosY = point.Y; + + + if (point.X > 80 && point.X < this.panel.Width && point.Y < 140 && point.Y > 130) + { + + if (_artistLIB) + { + UnfollowArtist(); + panel.Invalidate(); + //panel.Paint += DrawPanel; + } + else + { + FollowArtist(); + 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) + { + + if (_trackLIB) + { + RemoveTrack(); + panel.Invalidate(); + //panel.Paint += DrawPanel; + } + else + { + SaveTrack(); + panel.Invalidate(); + //panel.Paint += DrawPanel; + } + + } + + + } + + + } + + + public 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(); + } + + + panel.Invalidate(); + //panel.Paint += DrawPanel; + + break; + + } + } + + + + public bool Configure(IntPtr panelHandle) + { + + + return true; + } + + public void SaveSettings() + { + + } + + public void Close(PluginCloseReason reason) + { + SystemEvents.PowerModeChanged -= OnPowerChange; + } + + public void Uninstall() + { + } + + } + +} diff --git a/Source Files/Properties/AssemblyInfo.cs b/Source Files/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9754288 --- /dev/null +++ b/Source Files/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("mb_Spotify_Plugin")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Zachary Cohen")] +[assembly: AssemblyProduct("mb_Spotify_Plugin")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("D2937FD0-36F4-40AB-ABF2-60B2CF07ADEE")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// 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")] diff --git a/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs b/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs new file mode 100644 index 0000000..84719e6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/AuthUtil.cs @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..b8687c3 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/AuthorizationCodeAuth.cs @@ -0,0 +1,134 @@ +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/CredentialsAuth.cs b/Source Files/SpotifyAPI.Web.Auth/CredentialsAuth.cs new file mode 100644 index 0000000..1da062b --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/CredentialsAuth.cs @@ -0,0 +1,42 @@ +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/ImplicitGrantAuth.cs b/Source Files/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs new file mode 100644 index 0000000..c99809f --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/ImplicitGrantAuth.cs @@ -0,0 +1,65 @@ +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/Resources/AuthorizationCodeAuth/css/bulma.min.css b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css new file mode 100644 index 0000000..59825f7 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css @@ -0,0 +1 @@ +/*! 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 new file mode 100644 index 0000000..4bb428a Binary files /dev/null and b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/1.png 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 new file mode 100644 index 0000000..a01a53a Binary files /dev/null and b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/images/2.png differ diff --git a/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html new file mode 100644 index 0000000..9b883d4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/start.html @@ -0,0 +1,77 @@ + + + + + + + + + + + +
+
+

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 new file mode 100644 index 0000000..6197f88 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/Resources/ImplicitGrantAuth/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj new file mode 100644 index 0000000..6f40b62 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.csproj @@ -0,0 +1,51 @@ + + + + net46;netstandard2.0 + + + + bin\Debug\netstandard2.0\SpotifyAPI.Web.Auth.xml + 1701;1702;1705;1591 + + + + bin\Release\netstandard2.0\SpotifyAPI.Web.Auth.xml + 1701;1702;1705;1591 + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\..\..\..\..\..\..\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 new file mode 100644 index 0000000..6675c4d --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/SpotifyAPI.Web.Auth.nuspec @@ -0,0 +1,31 @@ + + + + + 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 new file mode 100644 index 0000000..e4f668b --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/SpotifyAuthServer.cs @@ -0,0 +1,101 @@ +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 new file mode 100644 index 0000000..73e7b0d --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/TokenSwapAuth.cs @@ -0,0 +1,217 @@ +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 new file mode 100644 index 0000000..1cc9128 --- /dev/null +++ b/Source Files/SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs @@ -0,0 +1,282 @@ +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/Enums/AlbumType.cs b/Source Files/SpotifyAPI.Web/Enums/AlbumType.cs new file mode 100644 index 0000000..c05df33 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/AlbumType.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..6cd802b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/FollowType.cs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..d4abcf6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/RepeatState.cs @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..5b500b9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/Scope.cs @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..652161c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/SearchType.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..468a808 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/TimeRangeType.cs @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..63c4341 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Enums/TrackType.cs @@ -0,0 +1,20 @@ +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/IClient.cs b/Source Files/SpotifyAPI.Web/IClient.cs new file mode 100644 index 0000000..c0960a6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/IClient.cs @@ -0,0 +1,125 @@ +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 new file mode 100644 index 0000000..bb9b777 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AnalysisMeta.cs @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..bcb92bc --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AnalysisSection.cs @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..bdf5270 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AnalysisSegment.cs @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..41ab5f1 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AnalysisTimeSlice.cs @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..e84bc62 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AnalysisTrack.cs @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000..eea3be6 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/ArrayResponse.cs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..bc8f622 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AudioAnalysis.cs @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..a3b029f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AudioFeatures.cs @@ -0,0 +1,61 @@ +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 new file mode 100644 index 0000000..6275ec0 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/AvailabeDevices.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..d888dc2 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/BasicModel.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..03be7a0 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Category.cs @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..c46d5cb --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/CategoryList.cs @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..02875d5 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/CategoryPlaylist.cs @@ -0,0 +1,10 @@ +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/CursorPaging.cs b/Source Files/SpotifyAPI.Web/Models/CursorPaging.cs new file mode 100644 index 0000000..3496212 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/CursorPaging.cs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..3a0d79c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Device.cs @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..ab1dc09 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FeaturedPlaylists.cs @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..163ce87 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FollowedArtists.cs @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..b22e4f2 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FullAlbum.cs @@ -0,0 +1,65 @@ +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 new file mode 100644 index 0000000..1b38a74 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FullArtist.cs @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..aec32d0 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FullPlaylist.cs @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..bab52fe --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/FullTrack.cs @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..8dffd92 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/GeneralModels.cs @@ -0,0 +1,159 @@ +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/NewAlbumReleases.cs b/Source Files/SpotifyAPI.Web/Models/NewAlbumReleases.cs new file mode 100644 index 0000000..c85139e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/NewAlbumReleases.cs @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..03e5305 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Paging.cs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..ae1c28e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/PlayHistory.cs @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..37d6d4a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/PlaybackContext.cs @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..243d6b9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/PrivateProfile.cs @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..a28b8ea --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/PublicProfile.cs @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..b0cd94a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/RecommendationSeed .cs @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..663c8bf --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/RecommendationSeedGenres.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..921522e --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Recommendations.cs @@ -0,0 +1,14 @@ +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/ResponseInfo.cs b/Source Files/SpotifyAPI.Web/Models/ResponseInfo.cs new file mode 100644 index 0000000..1a8a12c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/ResponseInfo.cs @@ -0,0 +1,13 @@ +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/SearchItem.cs b/Source Files/SpotifyAPI.Web/Models/SearchItem.cs new file mode 100644 index 0000000..65b76c5 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SearchItem.cs @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..74025da --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SeveralAlbums.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..28df5db --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SeveralArtists.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..34ea651 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SeveralAudioFeatures.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..a72c216 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SeveralTracks.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..2e2aa0f --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SimpleAlbum.cs @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..c1294ee --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SimpleArtist.cs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..37b3760 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SimplePlaylist.cs @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..85702a3 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/SimpleTrack.cs @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..4a7e3e7 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Snapshot.cs @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..941d6f9 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/Token.cs @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..9f3943b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Models/TuneableTrack.cs @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..309f577 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/ProxyConfig.cs @@ -0,0 +1,77 @@ +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/SpotifyAPI.Web.csproj b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj new file mode 100644 index 0000000..9021175 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.csproj @@ -0,0 +1,30 @@ + + + + net46;netstandard2.0 + + + + bin\Debug\netstandard2.0\SpotifyAPI.Web.xml + 1701;1702;1705;1591 + + + + bin\Release\netstandard2.0\SpotifyAPI.Web.xml + 1701;1702;1705;1591 + + + + + + + + + + + + + + + + diff --git a/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec new file mode 100644 index 0000000..a154e5b --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyAPI.Web.nuspec @@ -0,0 +1,30 @@ + + + + + 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/SpotifyWebAPI.cs b/Source Files/SpotifyAPI.Web/SpotifyWebAPI.cs new file mode 100644 index 0000000..1efe238 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyWebAPI.cs @@ -0,0 +1,2958 @@ +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 new file mode 100644 index 0000000..47e913a --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyWebBuilder.cs @@ -0,0 +1,1129 @@ +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 new file mode 100644 index 0000000..54b2ff4 --- /dev/null +++ b/Source Files/SpotifyAPI.Web/SpotifyWebClient.cs @@ -0,0 +1,223 @@ +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 new file mode 100644 index 0000000..5d6df3c --- /dev/null +++ b/Source Files/SpotifyAPI.Web/Util.cs @@ -0,0 +1,41 @@ +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/SpotifyIntegration.cs b/Source Files/SpotifyIntegration.cs new file mode 100644 index 0000000..0430fe5 --- /dev/null +++ b/Source Files/SpotifyIntegration.cs @@ -0,0 +1,244 @@ +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; + +namespace MusicBeePlugin +{ + + public partial class Plugin + { + + private static SpotifyWebAPI _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; + + + + static async void SpotifyWebAuth(bool autoRefresh) + { + ImplicitGrantAuth auth = + new ImplicitGrantAuth("777824a07eeb4312972ff5fcec54c565", "http://localhost:4002", "http://localhost:4002", Scope.UserLibraryModify | Scope.UserFollowModify | Scope.UserFollowRead | Scope.UserLibraryRead); + auth.AuthReceived += async (sender, payload) => + { + + auth.Stop(); // `sender` is also the auth instance + _spotify = new SpotifyWebAPI() { TokenType = payload.TokenType, AccessToken = payload.AccessToken }; + + + }; + auth.Start(); // Starts an internal HTTP Server + + auth.OpenBrowser(autoRefresh); + + _auth = 1; + _runOnce = true; + } + + + + public FullTrack TrackSearch() + { + + + SearchItem track = _spotify.SearchItems(_searchTerm, SearchType.Track, 10); + + if (track.HasError()) + { + + _trackMissing = 1; + Console.WriteLine("Error Status: " + track.Error.Status); + Console.WriteLine("Error Msg: " + track.Error.Message); + + } + else if (track.Tracks.Total >= 1) + { + _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); + _trackID = track.Tracks.Items[_num].Id; + _albumID = track.Tracks.Items[_num].Album.Id; + _artistID = track.Tracks.Items[_num].Artists[0].Id; + _imageURL = track.Tracks.Items[_num].Album.Images[0].Url; + + } + else + { + + _trackMissing = 1; + + } + + return null; + } + + + + public void SaveTrack() + { + + ErrorResponse response = _spotify.SaveTrack(_trackID); + if (!response.HasError()) + MessageBox.Show("Track Saved."); + else + MessageBox.Show(response.Error.Message); + + } + + public void SaveAlbum() + { + + ErrorResponse response = _spotify.SaveAlbum(_albumID); + if (!response.HasError()) + MessageBox.Show("Album Saved."); + else + MessageBox.Show(response.Error.Message); + + } + + public void FollowArtist() + { + ErrorResponse response = _spotify.Follow(FollowType.Artist, _artistID); + if (!response.HasError()) + MessageBox.Show("Artist Followed."); + else + MessageBox.Show(response.Error.Message); + + } + + + + public void RemoveTrack() + { + + ErrorResponse response = _spotify.RemoveSavedTracks(new List { _trackID }); + if (!response.HasError()) + MessageBox.Show("Track Unsaved."); + else + MessageBox.Show(response.Error.Message); + + } + + public void RemoveAlbum() + { + ErrorResponse response = _spotify.RemoveSavedAlbums(new List { _albumID}); + if (!response.HasError()) + MessageBox.Show("Album Unsaved."); + else + MessageBox.Show(response.Error.Message); + + } + + public void UnfollowArtist() + { + ErrorResponse response = _spotify.Unfollow(FollowType.Artist, _artistID); + if (!response.HasError()) + MessageBox.Show("Artist Unfollowed."); + else + MessageBox.Show(response.Error.Message); + + } + + + + public Boolean CheckTrack(string id) + { + ListResponse tracksSaved = _spotify.CheckSavedTracks(new List { id }); + if (tracksSaved.List[0]) + { + _trackLIB = true; + return true; + } + else + { + _trackLIB = false; + return false; + } + } + + 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)) + { + if (line.Contains(_albumID)) + { + _albumLIB = true; + return true; + } + else + { + _albumLIB = false; + } + } + + + if (_albumLIB) + { return true; } + else + { return false; } + + + + } + + public Boolean CheckArtist(string id) + { + ListResponse response = _spotify.IsFollowing(FollowType.Artist, id); + if (response.List[0] == true) + { + _artistLIB = true; + return true; + } + else + { + _artistLIB = false; + return false; + } + } + + // 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 new file mode 100644 index 0000000..a53f1c6 --- /dev/null +++ b/Source Files/mb_Spotify-Plugin.csproj @@ -0,0 +1,64 @@ + + + + + Debug + AnyCPU + {EA4E342C-C766-4C68-A28B-7ADBE7C3667D} + Library + Properties + mb_Spotify_Plugin + mb_Spotify-Plugin + v4.6.2 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + SpotifyAPI.Web\bin\Debug\net46\SpotifyAPI.Web.dll + + + False + SpotifyAPI.Web.Auth\bin\Debug\net46\SpotifyAPI.Web.Auth.dll + + + + + + + + + + + + + + + + + + + + + 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" + + \ No newline at end of file diff --git a/Source Files/mb_Spotify-Plugin.sln b/Source Files/mb_Spotify-Plugin.sln new file mode 100644 index 0000000..e35b199 --- /dev/null +++ b/Source Files/mb_Spotify-Plugin.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.421 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mb_Spotify-Plugin", "mb_Spotify-Plugin.csproj", "{EA4E342C-C766-4C68-A28B-7ADBE7C3667D}" +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}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web.Auth", "..\Source Files\SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA4E342C-C766-4C68-A28B-7ADBE7C3667D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA4E342C-C766-4C68-A28B-7ADBE7C3667D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA4E342C-C766-4C68-A28B-7ADBE7C3667D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA4E342C-C766-4C68-A28B-7ADBE7C3667D}.Release|Any CPU.Build.0 = Release|Any CPU + {C222F9E6-9053-49E1-A1A0-F5A409159847}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C222F9E6-9053-49E1-A1A0-F5A409159847}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C222F9E6-9053-49E1-A1A0-F5A409159847}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C222F9E6-9053-49E1-A1A0-F5A409159847}.Release|Any CPU.Build.0 = Release|Any CPU + {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C70690B-F28B-415D-BFF2-FCDD34CAD1F0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2222581B-11F2-4C19-AC21-6498C86C5764} + EndGlobalSection +EndGlobal