From f35c82d59dfb63043f3711b6617ab75bcb69245d Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Fri, 9 May 2025 17:32:12 -0600 Subject: [PATCH] Change ApiExtended to always allow provide login option Previously, only some calls to ApiExtended.CreateAsync() would prompt users to login if necessary. Other calls would only work if the account already had a valid identity, and they would throw exceptions otherwise. Changed ApiExtended so that the UI registers a static ILoginChoiceEager factory delegate that ApiExtended will use in the event that a login is required. --- Source/ApplicationServices/LibraryCommands.cs | 12 ++-- Source/AudibleUtilities/ApiExtended.cs | 68 +++++++++---------- Source/FileLiberator/UtilityExtensions.cs | 8 ++- .../Dialogs/Login/AvaloniaLoginChoiceEager.cs | 4 -- .../ViewModels/MainVM.Import.cs | 2 +- .../ViewModels/MainVM.ScanAuto.cs | 2 +- .../ViewModels/ProductsDisplayViewModel.cs | 2 +- .../Views/MainWindow.axaml.cs | 1 + Source/LibationCli/Options/ScanOptions.cs | 2 +- .../Dialogs/Login/WinformLoginChoiceEager.cs | 8 +-- Source/LibationWinForms/Form1.ScanAuto.cs | 2 +- Source/LibationWinForms/Form1.ScanManual.cs | 2 +- Source/LibationWinForms/Form1.cs | 4 ++ .../GridView/ProductsDisplay.cs | 2 +- 14 files changed, 58 insertions(+), 61 deletions(-) diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 9278b084..7c373d0b 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -32,7 +32,7 @@ namespace ApplicationServices ScanEnd += (_, __) => Scanning = false; } - public static async Task> FindInactiveBooks(Func> apiExtendedfunc, IEnumerable existingLibrary, params Account[] accounts) + public static async Task> FindInactiveBooks(IEnumerable existingLibrary, params Account[] accounts) { logRestart(); @@ -58,7 +58,7 @@ namespace ApplicationServices try { logTime($"pre {nameof(scanAccountsAsync)} all"); - var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); + var libraryItems = await scanAccountsAsync(accounts, libraryOptions); logTime($"post {nameof(scanAccountsAsync)} all"); var totalCount = libraryItems.Count; @@ -101,7 +101,7 @@ namespace ApplicationServices } #region FULL LIBRARY scan and import - public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func> apiExtendedfunc, params Account[]? accounts) + public static async Task<(int totalCount, int newCount)> ImportAccountAsync(params Account[]? accounts) { logRestart(); @@ -131,7 +131,7 @@ namespace ApplicationServices | LibraryOptions.ResponseGroupOptions.IsFinished, ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215 }; - var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); + var importItems = await scanAccountsAsync(accounts, libraryOptions); logTime($"post {nameof(scanAccountsAsync)} all"); var totalCount = importItems.Count; @@ -262,7 +262,7 @@ namespace ApplicationServices return null; } - private static async Task> scanAccountsAsync(Func> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions) + private static async Task> scanAccountsAsync(Account[] accounts, LibraryOptions libraryOptions) { var tasks = new List>>(); @@ -278,7 +278,7 @@ namespace ApplicationServices try { // get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll) - var apiExtended = await apiExtendedfunc(account); + var apiExtended = await ApiExtended.CreateAsync(account); // add scanAccountAsync as a TASK: do not await tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver)); diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 201e2c8b..12a506fd 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -11,11 +11,13 @@ using Polly; using Polly.Retry; using System.Threading; +#nullable enable namespace AudibleUtilities { /// USE THIS from within Libation. It wraps the call with correct JSONPath public class ApiExtended { + public static Func? LoginChoiceFactory { get; set; } public Api Api { get; private set; } private const int MaxConcurrency = 10; @@ -24,52 +26,46 @@ namespace AudibleUtilities private ApiExtended(Api api) => Api = api; /// Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks. - public static async Task CreateAsync(Account account, ILoginChoiceEager loginChoiceEager) - { - Serilog.Log.Logger.Information("{@DebugInfo}", new - { - LoginType = nameof(ILoginChoiceEager), - Account = account?.MaskedLogEntry ?? "[null]", - LocaleName = account?.Locale?.Name - }); - - var api = await EzApiCreator.GetApiAsync( - loginChoiceEager, - account.Locale, - AudibleApiStorage.AccountsSettingsFile, - account.GetIdentityTokensJsonPath()); - return new ApiExtended(api); - } - - /// Get api from existing tokens. Assumes you have valid login tokens. Else exception public static async Task CreateAsync(Account account) { ArgumentValidator.EnsureNotNull(account, nameof(account)); + ArgumentValidator.EnsureNotNull(account.AccountId, nameof(account.AccountId)); ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale)); - Serilog.Log.Logger.Information("{@DebugInfo}", new + try { - AccountMaskedLogEntry = account.MaskedLogEntry - }); + Serilog.Log.Logger.Information("{@DebugInfo}", new + { + AccountMaskedLogEntry = account.MaskedLogEntry + }); - return await CreateAsync(account.AccountId, account.Locale.Name); - } - - /// Get api from existing tokens. Assumes you have valid login tokens. Else exception - public static async Task CreateAsync(string username, string localeName) - { - Serilog.Log.Logger.Information("{@DebugInfo}", new + var api = await EzApiCreator.GetApiAsync( + account.Locale, + AudibleApiStorage.AccountsSettingsFile, + account.GetIdentityTokensJsonPath()); + return new ApiExtended(api); + } + catch { - Username = username.ToMask(), - LocaleName = localeName, - }); + if (LoginChoiceFactory is null) + throw new InvalidOperationException($"The UI module must first set {LoginChoiceFactory} before attempting to create the api"); - var api = await EzApiCreator.GetApiAsync( - Localization.Get(localeName), + Serilog.Log.Logger.Information("{@DebugInfo}", new + { + LoginType = nameof(ILoginChoiceEager), + Account = account.MaskedLogEntry ?? "[null]", + LocaleName = account.Locale?.Name + }); + + var api = await EzApiCreator.GetApiAsync( + LoginChoiceFactory(account), + account.Locale, AudibleApiStorage.AccountsSettingsFile, - AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName)); - return new ApiExtended(api); - } + account.GetIdentityTokensJsonPath()); + + return new ApiExtended(api); + } + } private static AsyncRetryPolicy policy { get; } = Policy.Handle() diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 08f11a50..4a2f6de5 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -20,9 +20,15 @@ namespace FileLiberator account: libraryBook.Account.ToMask() ); + public static Func>? ApiExtendedFunc { get; set; } + public static async Task GetApiAsync(this LibraryBook libraryBook) { - var apiExtended = await ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale); + Account account; + using (var accounts = AudibleApiStorage.GetAccountsSettingsPersister()) + account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale); + + var apiExtended = await ApiExtended.CreateAsync(account); return apiExtended.Api; } diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs index 52f588f1..b8122f9d 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs @@ -9,10 +9,6 @@ namespace LibationAvalonia.Dialogs.Login { public class AvaloniaLoginChoiceEager : ILoginChoiceEager { - /// Convenience method. Recommended when wiring up Winforms to - public static async Task ApiExtendedFunc(Account account) - => await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account)); - public ILoginCallback LoginCallback { get; } private readonly Account _account; diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs index 2a39fee7..213a7cf4 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs @@ -201,7 +201,7 @@ namespace LibationAvalonia.ViewModels { try { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); + var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts); // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop if (Configuration.Instance.ShowImportedStats && newAdded > 0) diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs index 23e5b8bb..2ae562c5 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs @@ -27,7 +27,7 @@ namespace LibationAvalonia.ViewModels // in autoScan, new books SHALL NOT show dialog try { - await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); + await LibraryCommands.ImportAccountAsync(accounts); } catch (OperationCanceledException) { diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 271feb44..13e282f5 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -431,7 +431,7 @@ namespace LibationAvalonia.ViewModels .Select(lbe => lbe.LibraryBook) .Where(lb => !lb.Book.HasLiberated()); - var removedBooks = await LibraryCommands.FindInactiveBooks(AvaloniaLoginChoiceEager.ApiExtendedFunc, lib, accounts); + var removedBooks = await LibraryCommands.FindInactiveBooks(lib, accounts); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList(); diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 950bfc5e..22262c5a 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -21,6 +21,7 @@ namespace LibationAvalonia.Views public MainWindow() { DataContext = new MainVM(this); + ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account); AudibleApiStorage.LoadError += AudibleApiStorage_LoadError; InitializeComponent(); diff --git a/Source/LibationCli/Options/ScanOptions.cs b/Source/LibationCli/Options/ScanOptions.cs index 43c888dd..532193a2 100644 --- a/Source/LibationCli/Options/ScanOptions.cs +++ b/Source/LibationCli/Options/ScanOptions.cs @@ -32,7 +32,7 @@ namespace LibationCli : $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account."; Console.WriteLine(intro); - var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((a) => ApiExtended.CreateAsync(a), _accounts); + var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync(_accounts); Console.WriteLine("Scan complete."); Console.WriteLine($"Total processed: {TotalBooksProcessed}"); diff --git a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs index 55d48f36..48724c7e 100644 --- a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs +++ b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs @@ -9,17 +9,11 @@ namespace LibationWinForms.Login { public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager { - /// Convenience method. Recommended when wiring up Winforms to - public static Func> CreateApiExtendedFunc(IWin32Window owner) => a => ApiExtendedFunc(a, owner); - - private static async Task ApiExtendedFunc(Account account, IWin32Window owner) - => await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account, owner)); - public ILoginCallback LoginCallback { get; private set; } private Account _account { get; } - private WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner) + public WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner) { _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account)); LoginCallback = new WinformLoginCallback(_account, owner); diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs index 6b671c6a..2d3636bf 100644 --- a/Source/LibationWinForms/Form1.ScanAuto.cs +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -32,7 +32,7 @@ namespace LibationWinForms // in autoScan, new books SHALL NOT show dialog try { - Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts); + Task importAsync() => LibraryCommands.ImportAccountAsync(accounts); if (InvokeRequired) await Invoke(importAsync); else diff --git a/Source/LibationWinForms/Form1.ScanManual.cs b/Source/LibationWinForms/Form1.ScanManual.cs index 83f5149f..851ac9d9 100644 --- a/Source/LibationWinForms/Form1.ScanManual.cs +++ b/Source/LibationWinForms/Form1.ScanManual.cs @@ -74,7 +74,7 @@ namespace LibationWinForms { try { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts); + var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts); // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop if (Configuration.Instance.ShowImportedStats && newAdded > 0) diff --git a/Source/LibationWinForms/Form1.cs b/Source/LibationWinForms/Form1.cs index 876e2ca8..f8413dcb 100644 --- a/Source/LibationWinForms/Form1.cs +++ b/Source/LibationWinForms/Form1.cs @@ -4,8 +4,11 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; using ApplicationServices; +using AudibleUtilities; using DataLayer; using LibationFileManager; +using LibationWinForms.Login; +using Octokit; namespace LibationWinForms { @@ -56,6 +59,7 @@ namespace LibationWinForms => Invoke(() => productsDisplay.DisplayAsync(fullLibrary)); } Shown += Form1_Shown; + ApiExtended.LoginChoiceFactory = account => new WinformLoginChoiceEager(account, this); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index 37f16e24..4844ec6a 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -351,7 +351,7 @@ namespace LibationWinForms.GridView .Select(lbe => lbe.LibraryBook) .Where(lb => !lb.Book.HasLiberated()); - var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), lib, accounts); + var removedBooks = await LibraryCommands.FindInactiveBooks(lib, accounts); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();