From ef9b700ac4cda80c2cf144024463aa738fdc7ed6 Mon Sep 17 00:00:00 2001 From: Soapwood Date: Mon, 2 Dec 2024 22:03:05 +0000 Subject: [PATCH] [WIP] Adding Tidal API auth workflow. Added OVR Toolkit connection status to HomePage. --- .../Spotify/SpotifyClientBuilder.cs | 10 +- .../Tidal/Authentication/TidalOAuthClient.cs | 90 +++++++++++++++ .../Connections/Tidal/Model/TidalClient.cs | 6 + .../Connections/Tidal/Model/TidalScopes.cs | 7 ++ VXMusic/Connections/Tidal/TidalUtil.cs | 33 ++++++ VXMusicDesktop/App.xaml.cs | 6 + VXMusicDesktop/MVVM/View/ConnectionsView.xaml | 104 +++++++++++++++++- VXMusicDesktop/MVVM/View/HomeView.xaml | 69 ++++++++---- .../MVVM/ViewModel/ConnectionsViewModel.cs | 45 +++++++- .../MVVM/ViewModel/SharedViewModel.cs | 11 ++ VXMusicDesktop/VXMusicSession.cs | 12 ++ VXMusicDesktop/appsettings.json | 3 + 12 files changed, 362 insertions(+), 34 deletions(-) create mode 100644 VXMusic/Connections/Tidal/Authentication/TidalOAuthClient.cs create mode 100644 VXMusic/Connections/Tidal/Model/TidalClient.cs create mode 100644 VXMusic/Connections/Tidal/Model/TidalScopes.cs create mode 100644 VXMusic/Connections/Tidal/TidalUtil.cs diff --git a/VXMusic/Connections/Spotify/SpotifyClientBuilder.cs b/VXMusic/Connections/Spotify/SpotifyClientBuilder.cs index eaf171e0..5cf42371 100644 --- a/VXMusic/Connections/Spotify/SpotifyClientBuilder.cs +++ b/VXMusic/Connections/Spotify/SpotifyClientBuilder.cs @@ -3,22 +3,20 @@ using SpotifyAPI.Web.Auth; using VXMusic.Spotify.Authentication; using VXMusic.Spotify.Model; - -namespace VXMusic.Spotify; - using System.Net; using System.Text; using Newtonsoft.Json; +namespace VXMusic.Spotify; + + public class SpotifyClientBuilder { private static readonly Lazy> _spotifyClient = new Lazy>(() => CreateSpotifyClient()); public static Task Instance => _spotifyClient.Value; - - private readonly string _spotifyAccountEndpoint = "https://accounts.spotify.com/api/token"; - + public static async Task CreateSpotifyClient() { await SpotifyAuthentication.GetSpotifyUserAuthentication(); diff --git a/VXMusic/Connections/Tidal/Authentication/TidalOAuthClient.cs b/VXMusic/Connections/Tidal/Authentication/TidalOAuthClient.cs new file mode 100644 index 00000000..37d1ed5b --- /dev/null +++ b/VXMusic/Connections/Tidal/Authentication/TidalOAuthClient.cs @@ -0,0 +1,90 @@ +using System.Text; +using SpotifyAPI.Web; +using SpotifyAPI.Web.Http; +using VXMusic.Connections.Tidal; + +namespace VXMusic.Tidal.Authentication; + +public class TidalOAuthClient : APIClient, IOAuthClient +{ + public Task RequestToken(PKCETokenRequest request, CancellationToken cancel = default (CancellationToken)) => RequestToken(request, this.API, cancel); + + public static Task RequestToken( + PKCETokenRequest request, + IAPIConnector apiConnector, + CancellationToken cancel = default (CancellationToken)) + { + TidalUtil.Ensure.ArgumentNotNull((object) request, nameof (request)); + TidalUtil.Ensure.ArgumentNotNull((object) apiConnector, nameof (apiConnector)); + List> form = new List>() + { + new KeyValuePair("client_id", request.ClientId), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", request.Code), + new KeyValuePair("redirect_uri", request.RedirectUri.ToString()), + new KeyValuePair("code_verifier", request.CodeVerifier) + }; + return SendOAuthRequest(apiConnector, form, (string) null, (string) null, cancel); + } + + private static Task SendOAuthRequest( + IAPIConnector apiConnector, + List> form, + string? clientId, + string? clientSecret, + CancellationToken cancel = default (CancellationToken)) + { + // TODO Inject creds here + Dictionary headers = BuildAuthHeader(clientId, clientSecret); + return apiConnector.Post(TidalAuthentication.TidalAuthApiUrl, (IDictionary) null, (object) new FormUrlEncodedContent((IEnumerable>) form), headers, cancel); + } + + private static Dictionary BuildAuthHeader(string? clientId, string? clientSecret) + { + if (clientId == null || clientSecret == null) + return new Dictionary(); + return new Dictionary() + { + { + "Authorization", + "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(clientId + ":" + clientSecret)) + } + }; + } + + public TidalOAuthClient(SpotifyClientConfig config) + : base((IAPIConnector) ValidateConfig(config)) + { + } + + private static APIConnector ValidateConfig(SpotifyClientConfig config) + { + TidalUtil.Ensure.ArgumentNotNull((object) config, nameof (config)); + return new APIConnector(config.BaseAddress, config.Authenticator, config.JSONSerializer, config.HTTPClient, config.RetryHandler, config.HTTPLogger); + } + + public Task RequestToken(ClientCredentialsRequest request, CancellationToken cancel = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task RequestToken(AuthorizationCodeRefreshRequest request, CancellationToken cancel = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task RequestToken(AuthorizationCodeTokenRequest request, CancellationToken cancel = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task RequestToken(TokenSwapTokenRequest request, CancellationToken cancel = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task RequestToken(TokenSwapRefreshRequest request, CancellationToken cancel = new CancellationToken()) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/VXMusic/Connections/Tidal/Model/TidalClient.cs b/VXMusic/Connections/Tidal/Model/TidalClient.cs new file mode 100644 index 00000000..9f835265 --- /dev/null +++ b/VXMusic/Connections/Tidal/Model/TidalClient.cs @@ -0,0 +1,6 @@ +namespace VXMusic.Tidal.Model; + +public class TidalClient +{ + +} \ No newline at end of file diff --git a/VXMusic/Connections/Tidal/Model/TidalScopes.cs b/VXMusic/Connections/Tidal/Model/TidalScopes.cs new file mode 100644 index 00000000..1bd719db --- /dev/null +++ b/VXMusic/Connections/Tidal/Model/TidalScopes.cs @@ -0,0 +1,7 @@ +namespace VXMusic.Tidal.Model; + +public static class TidalScopes +{ + public const string PlaylistRead = "playlists.read"; + public const string PlaylistWrite = "playlists.write"; +} \ No newline at end of file diff --git a/VXMusic/Connections/Tidal/TidalUtil.cs b/VXMusic/Connections/Tidal/TidalUtil.cs new file mode 100644 index 00000000..a2f4c4ab --- /dev/null +++ b/VXMusic/Connections/Tidal/TidalUtil.cs @@ -0,0 +1,33 @@ +namespace VXMusic.Connections.Tidal; + +public class TidalUtil +{ + internal static class Ensure + { + /// Checks an argument to ensure it isn't null. + /// The argument value to check + /// The name of the argument + public static void ArgumentNotNull(object value, string name) + { + if (value == null) + throw new ArgumentNullException(name); + } + + /// + /// Checks an argument to ensure it isn't null or an empty string + /// + /// The argument value to check + /// The name of the argument + public static void ArgumentNotNullOrEmptyString(string value, string name) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException("String is empty or null", name); + } + + public static void ArgumentNotNullOrEmptyList(IEnumerable value, string name) + { + if (value == null || !value.Any()) + throw new ArgumentException("List is empty or null", name); + } + } +} \ No newline at end of file diff --git a/VXMusicDesktop/App.xaml.cs b/VXMusicDesktop/App.xaml.cs index ae93f9f4..365e5aea 100644 --- a/VXMusicDesktop/App.xaml.cs +++ b/VXMusicDesktop/App.xaml.cs @@ -96,6 +96,12 @@ public App() : base() ClientId = configuration["Connections:Spotify:ClientId"], PlaylistSavingSaveSetting = VXUserSettings.Connections.GetPlaylistSaveSetting() }, + + TidalSettings = new TidalSettings() + { + ClientId = configuration["Connections:Tidal:ClientId"], + PlaylistSavingSaveSetting = VXUserSettings.Connections.GetPlaylistSaveSetting() + }, LastfmSettings = new LastfmSettings() { diff --git a/VXMusicDesktop/MVVM/View/ConnectionsView.xaml b/VXMusicDesktop/MVVM/View/ConnectionsView.xaml index 8ec78374..3c7ca7ef 100644 --- a/VXMusicDesktop/MVVM/View/ConnectionsView.xaml +++ b/VXMusicDesktop/MVVM/View/ConnectionsView.xaml @@ -12,6 +12,9 @@ + + @@ -29,6 +32,8 @@ + @@ -265,12 +270,105 @@ - - + Margin="0,0,0,10"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VXMusicDesktop/MVVM/View/HomeView.xaml b/VXMusicDesktop/MVVM/View/HomeView.xaml index 7e988493..e137ccd4 100644 --- a/VXMusicDesktop/MVVM/View/HomeView.xaml +++ b/VXMusicDesktop/MVVM/View/HomeView.xaml @@ -263,6 +263,8 @@ + + @@ -275,13 +277,13 @@ Text="SteamVR" Foreground="{StaticResource TextBasic}" FontSize="12" - Margin="10,20,0,0" + Margin="10,12,0,0" HorizontalAlignment="Left" VerticalAlignment="Center"/> @@ -294,13 +296,13 @@ Text="XSOverlay" Foreground="{StaticResource TextBasic}" FontSize="12" - Margin="10,20,0,0" + Margin="10,12,0,0" HorizontalAlignment="Left" VerticalAlignment="Center"/> @@ -308,27 +310,46 @@ Color="{Binding SharedViewModel.IsXsOverlayNotificationServiceEnabled, Converter={StaticResource BooleanToColorConverter}}" /> + + + + + + + - - - - - - - - + + + + + + + + diff --git a/VXMusicDesktop/MVVM/ViewModel/ConnectionsViewModel.cs b/VXMusicDesktop/MVVM/ViewModel/ConnectionsViewModel.cs index 670f36cd..f3ea9a8d 100644 --- a/VXMusicDesktop/MVVM/ViewModel/ConnectionsViewModel.cs +++ b/VXMusicDesktop/MVVM/ViewModel/ConnectionsViewModel.cs @@ -1,12 +1,17 @@ using System; using System.ComponentModel; using System.Threading.Tasks; +using System.Windows.Forms.Design; using System.Windows.Input; using SpotifyAPI.Web; using VXMusic.Lastfm.Authentication; using VXMusic.Spotify; using VXMusic.Spotify.Authentication; +using VXMusic.Tidal; +using VXMusic.Tidal.Authentication; using VXMusicDesktop.Core; +using SpotifyConnectionState = VXMusic.Spotify.Authentication.SpotifyConnectionState; +using SpotifyConnectionStateExtensions = VXMusic.Spotify.Authentication.SpotifyConnectionStateExtensions; namespace VXMusicDesktop.MVVM.ViewModel { @@ -18,10 +23,13 @@ public class ConnectionsViewModel : INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private RelayCommand linkSpotifyButtonClick; + private RelayCommand linkTidalButtonClick; private RelayCommand linkLastfmButtonClick; private bool _shouldSpotifyLinkButtonBeShown; private string _spotifyLinkButtonText; + + private string _tidalLinkButtonText; private string _lastFmUsername = "Username"; private string _lastFmPassword = "Password"; @@ -30,6 +38,7 @@ public class ConnectionsViewModel : INotifyPropertyChanged private string _lastFmLinkButtonText; public ICommand LinkSpotifyButtonClick => linkSpotifyButtonClick ??= new RelayCommand(PerformLinkSpotifyButtonClick); + public ICommand LinkTidalButtonClick => linkTidalButtonClick ??= new RelayCommand(PerformLinkTidalButtonClick); public ICommand LinkLastfmButtonClick => linkLastfmButtonClick ??= new RelayCommand(PerformLastfmLogin); private bool _isLastFmConnected = false; @@ -128,6 +137,19 @@ public string SpotifyLinkButtonText } } + public string TidalLinkButtonText + { + get { return TidalConnectionStateExtensions.ToDisplayString(TidalAuthentication.CurrentConnectionState); } + set + { + if (_tidalLinkButtonText != value) + { + _tidalLinkButtonText = value; + OnPropertyChanged(nameof(TidalLinkButtonText)); + } + } + } + public string LastFmLinkButtonText { get { return _lastFmLinkButtonText; } @@ -154,12 +176,33 @@ private void PerformLinkSpotifyButtonClick(object commandParameter) SharedViewModel.IsSpotifyConnected = true; } } - + public async static Task LinkSpotify() { var spotify = await SpotifyClientBuilder.CreateSpotifyClient(); return await spotify.UserProfile.Current(); } + + private void PerformLinkTidalButtonClick(object commandParameter) + { + if (TidalAuthentication.CurrentConnectionState == TidalConnectionState.Connected) + return; + + var response = LinkTidal(); + + if (response != null) + { + TidalAuthentication.RaiseTidalLoggingIn(); + SharedViewModel.IsTidalConnected = true; + } + } + + public async static Task LinkTidal() + { + var tidal = await TidalClientBuilder.CreateTidalClient(); + //return await tidal.UserProfile.Current(); + return null; + } private async void PerformLastfmLogin(object commandParameter) { diff --git a/VXMusicDesktop/MVVM/ViewModel/SharedViewModel.cs b/VXMusicDesktop/MVVM/ViewModel/SharedViewModel.cs index ef75bac7..507b9238 100644 --- a/VXMusicDesktop/MVVM/ViewModel/SharedViewModel.cs +++ b/VXMusicDesktop/MVVM/ViewModel/SharedViewModel.cs @@ -33,6 +33,7 @@ public SharedViewModel() // Connection Shared Fields private bool _isSpotifyConnected; + private bool _isTidalConnected; private bool _isLastFmConnected; // Game Client Shared Fields @@ -115,6 +116,16 @@ public bool IsSpotifyConnected } } + public bool IsTidalConnected + { + get { return _isTidalConnected; } + set + { + _isTidalConnected = value; + OnPropertyChanged(nameof(IsTidalConnected)); + } + } + public bool IsLastFmConnected { get { return _isLastFmConnected; } diff --git a/VXMusicDesktop/VXMusicSession.cs b/VXMusicDesktop/VXMusicSession.cs index 13980cfa..10e7785e 100644 --- a/VXMusicDesktop/VXMusicSession.cs +++ b/VXMusicDesktop/VXMusicSession.cs @@ -13,6 +13,7 @@ using VXMusic.Recognition.AudD; using VXMusic.Recognition.Shazam; using VXMusic.Spotify.Authentication; +using VXMusic.Tidal.Authentication; using VXMusic.VRChat; using VXMusicDesktop.Core; using VXMusicDesktop.Theme; @@ -94,6 +95,10 @@ public void Initialise() SpotifyAuthentication.CredentialsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VirtualXtensions", "VXMusic", "LocalAuth", "Spotify"); SpotifyAuthentication.CredentialsFilePath = Path.Combine(SpotifyAuthentication.CredentialsPath, "credentials.json"); SpotifyAuthentication.ClientId = ConnectionsSettings.SpotifySettings.ClientId; + + TidalAuthentication.CredentialsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VirtualXtensions", "VXMusic", "LocalAuth", "Tidal"); + TidalAuthentication.CredentialsFilePath = Path.Combine(TidalAuthentication.CredentialsPath, "credentials_t.json"); + TidalAuthentication.ClientId = ConnectionsSettings.TidalSettings.ClientId; if (OverlaySettings.LaunchOverlayOnStartup) App.VXMOverlayProcess = VXMusicOverlayInterface.LaunchVXMOverlayRuntime(OverlaySettings.RuntimePath); @@ -220,6 +225,7 @@ public class ConnectionsSettings { // App Settings public SpotifySettings SpotifySettings { get; set; } + public TidalSettings TidalSettings { get; set; } public LastfmSettings LastfmSettings { get; set; } public bool IsLastfmConnected { get; set; } public bool IsVrChatConnected { get; set; } @@ -287,6 +293,12 @@ public class SpotifySettings public PlaylistSaveSettings PlaylistSavingSaveSetting { get; set; } } +public class TidalSettings +{ + public required string ClientId { get; set; } + public PlaylistSaveSettings PlaylistSavingSaveSetting { get; set; } +} + public class LastfmSettings { public required string ClientId { get; set; } diff --git a/VXMusicDesktop/appsettings.json b/VXMusicDesktop/appsettings.json index f9380e28..66501273 100644 --- a/VXMusicDesktop/appsettings.json +++ b/VXMusicDesktop/appsettings.json @@ -15,6 +15,9 @@ "Spotify": { "ClientId": "52e2f3931eab490c99039b3217b697d7" }, + "Tidal": { + "ClientId": "JZmzGWa3UdM0lsr1" + }, "Lastfm": { "ClientId": "4fd4dcd389d8292084f00f38ae7a2505", "AppToken": "84745d1187b224ecaad5c8d64e7ee848"