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();