diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj
index aca3a938..c4ee9183 100644
--- a/Source/AppScaffolding/AppScaffolding.csproj
+++ b/Source/AppScaffolding/AppScaffolding.csproj
@@ -3,7 +3,7 @@
net6.0-windows
- 7.3.0.1
+ 7.4.0.1
diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs
index 3bbf93c5..027ae536 100644
--- a/Source/ApplicationServices/LibraryCommands.cs
+++ b/Source/ApplicationServices/LibraryCommands.cs
@@ -33,18 +33,20 @@ namespace ApplicationServices
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
- var libraryResponseGroups =
- LibraryOptions.ResponseGroupOptions.ProductAttrs |
- LibraryOptions.ResponseGroupOptions.ProductDesc |
- LibraryOptions.ResponseGroupOptions.Relationships;
-
- if (accounts is null || accounts.Length == 0)
+ var libraryOptions = new LibraryOptions
+ {
+ ResponseGroups
+ = LibraryOptions.ResponseGroupOptions.ProductAttrs
+ | LibraryOptions.ResponseGroupOptions.ProductDesc
+ | LibraryOptions.ResponseGroupOptions.Relationships
+ };
+ if (accounts is null || accounts.Length == 0)
return new List();
try
{
logTime($"pre {nameof(scanAccountsAsync)} all");
- var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryResponseGroups);
+ var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = libraryItems.Count;
@@ -102,7 +104,12 @@ namespace ApplicationServices
}
logTime($"pre {nameof(scanAccountsAsync)} all");
- var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
+ var libraryOptions = new LibraryOptions
+ {
+ ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
+ ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
+ };
+ var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count;
@@ -150,7 +157,7 @@ namespace ApplicationServices
}
}
- private static async Task> scanAccountsAsync(Func> apiExtendedfunc, Account[] accounts, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
+ private static async Task> scanAccountsAsync(Func> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
{
var tasks = new List>>();
foreach (var account in accounts)
@@ -159,7 +166,7 @@ namespace ApplicationServices
var apiExtended = await apiExtendedfunc(account);
// add scanAccountAsync as a TASK: do not await
- tasks.Add(scanAccountAsync(apiExtended, account, libraryResponseGroups));
+ tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
}
// import library in parallel
@@ -168,7 +175,7 @@ namespace ApplicationServices
return importItems;
}
- private static async Task> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
+ private static async Task> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
@@ -179,7 +186,7 @@ namespace ApplicationServices
logTime($"pre scanAccountAsync {account.AccountName}");
- var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, Configuration.Instance.ImportEpisodes);
+ var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
@@ -259,30 +266,47 @@ namespace ApplicationServices
/// Occurs when , , or
/// changed values are successfully persisted.
///
- public static event EventHandler BookUserDefinedItemCommitted;
+ public static event EventHandler BookUserDefinedItemCommitted;
#region Update book details
- public static int UpdateUserDefinedItem(Book book)
+ public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
+ public static int UpdateUserDefinedItem(IEnumerable books)
{
try
{
+ if (books is null || !books.Any())
+ return 0;
+
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
- context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
+ foreach (var book in books)
+ context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
+
var qtyChanges = context.SaveChanges();
- if (qtyChanges > 0)
+ if (qtyChanges == 0)
+ return 0;
+
+ // semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
+ // I did not benchmark before choosing the number here
+ if (qtyChanges > 15)
+ SearchEngineCommands.FullReIndex();
+ else
{
- SearchEngineCommands.UpdateLiberatedStatus(book);
- SearchEngineCommands.UpdateBookTags(book);
- BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
+ foreach (var book in books)
+ {
+ SearchEngineCommands.UpdateLiberatedStatus(book);
+ SearchEngineCommands.UpdateBookTags(book);
+ }
}
+ BookUserDefinedItemCommitted?.Invoke(null, null);
+
return qtyChanges;
}
catch (Exception ex)
{
- Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
+ Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
throw;
}
}
diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs
index 512949ef..5a00c9a7 100644
--- a/Source/AudibleUtilities/ApiExtended.cs
+++ b/Source/AudibleUtilities/ApiExtended.cs
@@ -106,16 +106,16 @@ namespace AudibleUtilities
// 2 retries == 3 total
.RetryAsync(2);
- public Task> GetLibraryValidatedAsync(LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, bool importEpisodes = true)
+ public Task> GetLibraryValidatedAsync(LibraryOptions libraryOptions, bool importEpisodes = true)
{
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
// worse, this 1st dummy call doesn't seem to help:
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
- return policy.ExecuteAsync(() => getItemsAsync(responseGroups, importEpisodes));
+ return policy.ExecuteAsync(() => getItemsAsync(libraryOptions, importEpisodes));
}
- private async Task> getItemsAsync(LibraryOptions.ResponseGroupOptions responseGroups, bool importEpisodes)
+ private async Task> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
{
var items = new List- ();
#if DEBUG
@@ -131,7 +131,7 @@ namespace AudibleUtilities
Serilog.Log.Logger.Debug("Begin initial library scan");
if (!items.Any())
- items = await Api.GetAllLibraryItemsAsync(responseGroups);
+ items = await Api.GetAllLibraryItemsAsync(libraryOptions);
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj
index 1b933a47..b96dcfe2 100644
--- a/Source/AudibleUtilities/AudibleUtilities.csproj
+++ b/Source/AudibleUtilities/AudibleUtilities.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj
index 64a4f93d..51bbd9b1 100644
--- a/Source/DataLayer/DataLayer.csproj
+++ b/Source/DataLayer/DataLayer.csproj
@@ -12,13 +12,13 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/DataLayer/EfClasses/UserDefinedItem.cs b/Source/DataLayer/EfClasses/UserDefinedItem.cs
index f5da2cf8..28267a31 100644
--- a/Source/DataLayer/EfClasses/UserDefinedItem.cs
+++ b/Source/DataLayer/EfClasses/UserDefinedItem.cs
@@ -141,9 +141,11 @@ namespace DataLayer
get => _bookStatus;
set
{
- if (_bookStatus != value)
- {
- _bookStatus = value;
+ // PartialDownload is a live/ephemeral status, not a persistent one. Do not store
+ var displayStatus = value == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : value;
+ if (_bookStatus != displayStatus)
+ {
+ _bookStatus = displayStatus;
OnItemChanged(nameof(BookStatus));
}
}
diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs
index 7fc6a273..bea2e358 100644
--- a/Source/FileLiberator/DownloadDecryptBook.cs
+++ b/Source/FileLiberator/DownloadDecryptBook.cs
@@ -79,6 +79,7 @@ namespace FileLiberator
DownloadCoverArt(libraryBook);
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
+ ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
return new StatusHandler();
}
diff --git a/Source/FileLiberator/DownloadPdf.cs b/Source/FileLiberator/DownloadPdf.cs
index a2378e31..bd4817e2 100644
--- a/Source/FileLiberator/DownloadPdf.cs
+++ b/Source/FileLiberator/DownloadPdf.cs
@@ -29,6 +29,7 @@ namespace FileLiberator
var result = verifyDownload(actualDownloadedFilePath);
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
+ ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
return result;
}
diff --git a/Source/FileLiberator/FileLiberator.csproj b/Source/FileLiberator/FileLiberator.csproj
index 3da05437..460308d6 100644
--- a/Source/FileLiberator/FileLiberator.csproj
+++ b/Source/FileLiberator/FileLiberator.csproj
@@ -6,6 +6,7 @@
+
diff --git a/Source/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/Source/LibationWinForms/BookLiberation/ProcessorAutomationController.cs
index dca83b84..eb6ac3af 100644
--- a/Source/LibationWinForms/BookLiberation/ProcessorAutomationController.cs
+++ b/Source/LibationWinForms/BookLiberation/ProcessorAutomationController.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
@@ -61,7 +63,7 @@ namespace LibationWinForms.BookLiberation
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
}
- public static async Task BackupAllBooksAsync()
+ public static async Task BackupAllBooksAsync(List libraryBooks = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
@@ -69,7 +71,7 @@ namespace LibationWinForms.BookLiberation
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(logMe);
- await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
+ await new BackupLoop(logMe, backupBook, automatedBackupsForm, libraryBooks).RunBackupAsync();
}
public static async Task ConvertAllBooksAsync()
@@ -255,6 +257,8 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == SkipResult)
{
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
+ ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
+
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
@@ -307,13 +311,16 @@ An error occurred while trying to process this book.
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult SkipResult => DialogResult.Ignore;
- public BackupLoop(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm)
- : base(logMe, processable, automatedBackupsForm) { }
+ private List libraryBooks { get; }
+
+ public BackupLoop(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm, List libraryBooks = null)
+ : base(logMe, processable, automatedBackupsForm)
+ => this.libraryBooks = libraryBooks ?? ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking();
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
- foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))
+ foreach (var libraryBook in Processable.GetValidLibraryBooks(libraryBooks))
{
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing)
diff --git a/Source/LibationWinForms/grid/GridEntry.cs b/Source/LibationWinForms/grid/GridEntry.cs
index ce9d552d..aee51d4c 100644
--- a/Source/LibationWinForms/grid/GridEntry.cs
+++ b/Source/LibationWinForms/grid/GridEntry.cs
@@ -58,11 +58,7 @@ namespace LibationWinForms
public string Category { get; private set; }
public string Misc { get; private set; }
public string Description { get; private set; }
- public string DisplayTags
- {
- get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
- set => Book.UserDefinedItem.Tags = value;
- }
+ public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
@@ -77,18 +73,10 @@ namespace LibationWinForms
}
return (_bookStatus, _pdfStatus);
}
-
- set
- {
- _bookStatus = value.BookStatus;
- _pdfStatus = value.PdfStatus;
- LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
- LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
- }
}
#endregion
- public event EventHandler LibraryBookUpdated;
+ public event EventHandler LibraryBookUpdated;
public event EventHandler Committed;
// alias
@@ -98,18 +86,18 @@ namespace LibationWinForms
public async Task DownloadBook()
{
- if (!DownloadInProgress)
+ if (DownloadInProgress)
+ return;
+
+ try
{
- try
- {
- DownloadInProgress = true;
- await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(LibraryBook);
- UpdateLiberatedStatus();
- }
- finally
- {
- DownloadInProgress = false;
- }
+ DownloadInProgress = true;
+ await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(LibraryBook);
+ UpdateLiberatedStatus();
+ }
+ finally
+ {
+ DownloadInProgress = false;
}
}
@@ -156,7 +144,7 @@ namespace LibationWinForms
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
// this will never have a value when triggered by ctor b/c nothing can subscribe to the event until after ctor is complete
- LibraryBookUpdated?.Invoke(this, AudibleProductId);
+ LibraryBookUpdated?.Invoke(this, null);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
@@ -196,38 +184,28 @@ namespace LibationWinForms
NotifyPropertyChanged(nameof(Liberate));
break;
}
-
- if (!suspendCommit)
- Commit();
- }
- private bool suspendCommit = false;
-
- ///
- /// Begin editing the model, suspending commits until is called.
- ///
- public void BeginEdit() => suspendCommit = true;
-
- ///
- /// Save all edits to the database.
- ///
- public void EndEdit()
- {
- Commit();
- suspendCommit = false;
}
- private void Commit()
+ /// Save edits to the database
+ public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
{
- // We don't want LiberatedStatus.PartialDownload to be a persistent status.
- // If display/icon status is PartialDownload then save NotLiberated to db then restore PartialDownload for display
- var displayStatus = Book.UserDefinedItem.BookStatus;
- var saveStatus = displayStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : displayStatus;
- Book.UserDefinedItem.BookStatus = saveStatus;
+ // validate
+ if (DisplayTags.EqualsInsensitive(newTags) &&
+ Liberate.BookStatus == bookStatus &&
+ Liberate.PdfStatus == pdfStatus)
+ return;
+ // update cache
+ _bookStatus = bookStatus;
+ _pdfStatus = pdfStatus;
+
+ // set + save
+ Book.UserDefinedItem.Tags = newTags;
+ Book.UserDefinedItem.BookStatus = bookStatus;
+ Book.UserDefinedItem.PdfStatus = pdfStatus;
LibraryCommands.UpdateUserDefinedItem(Book);
- Book.UserDefinedItem.BookStatus = displayStatus;
-
+ // notify
Committed?.Invoke(this, null);
}
diff --git a/Source/LibationWinForms/grid/ProductsGrid.cs b/Source/LibationWinForms/grid/ProductsGrid.cs
index dfb73563..9f293658 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.cs
+++ b/Source/LibationWinForms/grid/ProductsGrid.cs
@@ -143,15 +143,8 @@ namespace LibationWinForms
private static void Details_Click(GridEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
- if (bookDetailsForm.ShowDialog() != DialogResult.OK)
- return;
-
- liveGridEntry.BeginEdit();
-
- liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
- liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
-
- liveGridEntry.EndEdit();
+ if (bookDetailsForm.ShowDialog() == DialogResult.OK)
+ liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
#endregion
@@ -232,7 +225,7 @@ namespace LibationWinForms
{
var entry = new GridEntry(libraryBook);
entry.Committed += Filter;
- entry.LibraryBookUpdated += (sender, productId) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender));
+ entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender));
return entry;
}
@@ -265,14 +258,19 @@ namespace LibationWinForms
// Causes repainting of the DataGridView
bindingContext.ResumeBinding();
- VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible));
+ VisibleCountChanged?.Invoke(this, GetVisible().Count());
}
#endregion
- #region DataGridView Macro
+ internal IEnumerable GetVisible()
+ => _dataGridView
+ .AsEnumerable()
+ .Where(row => row.Visible)
+ .Select(row => ((GridEntry)row.DataBoundItem).LibraryBook)
+ .ToList();
+
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex);
- #endregion
#region Column Customizations