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.
This commit is contained in:
Michael Bucari-Tovo 2025-05-09 17:32:12 -06:00
parent 10c01f4147
commit f35c82d59d
14 changed files with 58 additions and 61 deletions

View File

@ -32,7 +32,7 @@ namespace ApplicationServices
ScanEnd += (_, __) => Scanning = false; ScanEnd += (_, __) => Scanning = false;
} }
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, IEnumerable<LibraryBook> existingLibrary, params Account[] accounts) public static async Task<List<LibraryBook>> FindInactiveBooks(IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
{ {
logRestart(); logRestart();
@ -58,7 +58,7 @@ namespace ApplicationServices
try try
{ {
logTime($"pre {nameof(scanAccountsAsync)} all"); logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); var libraryItems = await scanAccountsAsync(accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all"); logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = libraryItems.Count; var totalCount = libraryItems.Count;
@ -101,7 +101,7 @@ namespace ApplicationServices
} }
#region FULL LIBRARY scan and import #region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts) public static async Task<(int totalCount, int newCount)> ImportAccountAsync(params Account[]? accounts)
{ {
logRestart(); logRestart();
@ -131,7 +131,7 @@ namespace ApplicationServices
| LibraryOptions.ResponseGroupOptions.IsFinished, | LibraryOptions.ResponseGroupOptions.IsFinished,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215 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"); logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count; var totalCount = importItems.Count;
@ -262,7 +262,7 @@ namespace ApplicationServices
return null; return null;
} }
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions) private static async Task<List<ImportItem>> scanAccountsAsync(Account[] accounts, LibraryOptions libraryOptions)
{ {
var tasks = new List<Task<List<ImportItem>>>(); var tasks = new List<Task<List<ImportItem>>>();
@ -278,7 +278,7 @@ namespace ApplicationServices
try try
{ {
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll) // 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 // add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver)); tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver));

View File

@ -11,11 +11,13 @@ using Polly;
using Polly.Retry; using Polly.Retry;
using System.Threading; using System.Threading;
#nullable enable
namespace AudibleUtilities namespace AudibleUtilities
{ {
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary> /// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public class ApiExtended public class ApiExtended
{ {
public static Func<Account, ILoginChoiceEager>? LoginChoiceFactory { get; set; }
public Api Api { get; private set; } public Api Api { get; private set; }
private const int MaxConcurrency = 10; private const int MaxConcurrency = 10;
@ -24,52 +26,46 @@ namespace AudibleUtilities
private ApiExtended(Api api) => Api = api; private ApiExtended(Api api) => Api = api;
/// <summary>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.</summary> /// <summary>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.</summary>
public static async Task<ApiExtended> 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);
}
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
public static async Task<ApiExtended> CreateAsync(Account account) public static async Task<ApiExtended> CreateAsync(Account account)
{ {
ArgumentValidator.EnsureNotNull(account, nameof(account)); ArgumentValidator.EnsureNotNull(account, nameof(account));
ArgumentValidator.EnsureNotNull(account.AccountId, nameof(account.AccountId));
ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale)); 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); var api = await EzApiCreator.GetApiAsync(
} account.Locale,
AudibleApiStorage.AccountsSettingsFile,
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary> account.GetIdentityTokensJsonPath());
public static async Task<ApiExtended> CreateAsync(string username, string localeName) return new ApiExtended(api);
{ }
Serilog.Log.Logger.Information("{@DebugInfo}", new catch
{ {
Username = username.ToMask(), if (LoginChoiceFactory is null)
LocaleName = localeName, throw new InvalidOperationException($"The UI module must first set {LoginChoiceFactory} before attempting to create the api");
});
var api = await EzApiCreator.GetApiAsync( Serilog.Log.Logger.Information("{@DebugInfo}", new
Localization.Get(localeName), {
LoginType = nameof(ILoginChoiceEager),
Account = account.MaskedLogEntry ?? "[null]",
LocaleName = account.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
LoginChoiceFactory(account),
account.Locale,
AudibleApiStorage.AccountsSettingsFile, AudibleApiStorage.AccountsSettingsFile,
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName)); account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
} return new ApiExtended(api);
}
}
private static AsyncRetryPolicy policy { get; } private static AsyncRetryPolicy policy { get; }
= Policy.Handle<Exception>() = Policy.Handle<Exception>()

View File

@ -20,9 +20,15 @@ namespace FileLiberator
account: libraryBook.Account.ToMask() account: libraryBook.Account.ToMask()
); );
public static Func<Account, Task<ApiExtended>>? ApiExtendedFunc { get; set; }
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook) public static async Task<AudibleApi.Api> 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; return apiExtended.Api;
} }

View File

@ -9,10 +9,6 @@ namespace LibationAvalonia.Dialogs.Login
{ {
public class AvaloniaLoginChoiceEager : ILoginChoiceEager public class AvaloniaLoginChoiceEager : ILoginChoiceEager
{ {
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
public static async Task<ApiExtended> ApiExtendedFunc(Account account)
=> await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
public ILoginCallback LoginCallback { get; } public ILoginCallback LoginCallback { get; }
private readonly Account _account; private readonly Account _account;

View File

@ -201,7 +201,7 @@ namespace LibationAvalonia.ViewModels
{ {
try 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 // 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) if (Configuration.Instance.ShowImportedStats && newAdded > 0)

View File

@ -27,7 +27,7 @@ namespace LibationAvalonia.ViewModels
// in autoScan, new books SHALL NOT show dialog // in autoScan, new books SHALL NOT show dialog
try try
{ {
await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); await LibraryCommands.ImportAccountAsync(accounts);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@ -431,7 +431,7 @@ namespace LibationAvalonia.ViewModels
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated()); .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(); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();

View File

@ -21,6 +21,7 @@ namespace LibationAvalonia.Views
public MainWindow() public MainWindow()
{ {
DataContext = new MainVM(this); DataContext = new MainVM(this);
ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account);
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError; AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
InitializeComponent(); InitializeComponent();

View File

@ -32,7 +32,7 @@ namespace LibationCli
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account."; : $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
Console.WriteLine(intro); 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("Scan complete.");
Console.WriteLine($"Total processed: {TotalBooksProcessed}"); Console.WriteLine($"Total processed: {TotalBooksProcessed}");

View File

@ -9,17 +9,11 @@ namespace LibationWinForms.Login
{ {
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
{ {
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
public static Func<Account, Task<ApiExtended>> CreateApiExtendedFunc(IWin32Window owner) => a => ApiExtendedFunc(a, owner);
private static async Task<ApiExtended> ApiExtendedFunc(Account account, IWin32Window owner)
=> await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account, owner));
public ILoginCallback LoginCallback { get; private set; } public ILoginCallback LoginCallback { get; private set; }
private Account _account { get; } 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)); _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new WinformLoginCallback(_account, owner); LoginCallback = new WinformLoginCallback(_account, owner);

View File

@ -32,7 +32,7 @@ namespace LibationWinForms
// in autoScan, new books SHALL NOT show dialog // in autoScan, new books SHALL NOT show dialog
try try
{ {
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts); Task importAsync() => LibraryCommands.ImportAccountAsync(accounts);
if (InvokeRequired) if (InvokeRequired)
await Invoke(importAsync); await Invoke(importAsync);
else else

View File

@ -74,7 +74,7 @@ namespace LibationWinForms
{ {
try 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 // 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) if (Configuration.Instance.ShowImportedStats && newAdded > 0)

View File

@ -4,8 +4,11 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using ApplicationServices; using ApplicationServices;
using AudibleUtilities;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.Login;
using Octokit;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -56,6 +59,7 @@ namespace LibationWinForms
=> Invoke(() => productsDisplay.DisplayAsync(fullLibrary)); => Invoke(() => productsDisplay.DisplayAsync(fullLibrary));
} }
Shown += Form1_Shown; Shown += Form1_Shown;
ApiExtended.LoginChoiceFactory = account => new WinformLoginChoiceEager(account, this);
} }
private void Form1_FormClosing(object sender, FormClosingEventArgs e) private void Form1_FormClosing(object sender, FormClosingEventArgs e)

View File

@ -351,7 +351,7 @@ namespace LibationWinForms.GridView
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated()); .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(); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();