diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj
index 83008a3a..b1f58b39 100644
--- a/Source/AppScaffolding/AppScaffolding.csproj
+++ b/Source/AppScaffolding/AppScaffolding.csproj
@@ -15,6 +15,7 @@
+
diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index f12046bc..39cdd508 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
@@ -139,12 +140,14 @@ namespace AppScaffolding
config.DownloadCoverArt = true;
}
- /// Initialize logging. Run after migration
+ /// Initialize logging. Wire-up events. Run after migration
public static void RunPostMigrationScaffolding(Configuration config)
{
ensureSerilogConfig(config);
configureLogging(config);
logStartupState(config);
+
+ wireUpSystemEvents(config);
}
private static void ensureSerilogConfig(Configuration config)
@@ -282,6 +285,12 @@ namespace AppScaffolding
});
}
+ private static void wireUpSystemEvents(Configuration configuration)
+ {
+ LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
+ LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
+ }
+
public static UpgradeProperties GetLatestRelease()
{
// timed out
diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs
index 773a3567..7f8133e3 100644
--- a/Source/ApplicationServices/LibraryCommands.cs
+++ b/Source/ApplicationServices/LibraryCommands.cs
@@ -253,11 +253,7 @@ namespace ApplicationServices
#endregion
// call this whenever books are added or removed from library
- private static void finalizeLibrarySizeChange()
- {
- SearchEngineCommands.FullReIndex();
- LibrarySizeChanged?.Invoke(null, null);
- }
+ private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
/// Occurs when the size of the library changes. ie: books are added or removed
public static event EventHandler LibrarySizeChanged;
@@ -265,9 +261,47 @@ namespace ApplicationServices
///
/// Occurs when the size of the library does not change but book(s) details do. Especially when , , or changed values are successfully persisted.
///
- public static event EventHandler BookUserDefinedItemCommitted;
+ public static event EventHandler> BookUserDefinedItemCommitted;
#region Update book details
+ public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
+ {
+ book.UserDefinedItem.BookStatus = bookStatus;
+ return UpdateUserDefinedItem(book);
+ }
+ public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
+ {
+ book.UserDefinedItem.PdfStatus = pdfStatus;
+ return UpdateUserDefinedItem(book);
+ }
+ public static int UpdateBook(
+ this Book book,
+ string tags = null,
+ LiberatedStatus? bookStatus = null,
+ LiberatedStatus? pdfStatus = null)
+ => UpdateBooks(tags, bookStatus, pdfStatus, book);
+ public static int UpdateBooks(
+ string tags = null,
+ LiberatedStatus? bookStatus = null,
+ LiberatedStatus? pdfStatus = null,
+ params Book[] books)
+ {
+ foreach (var book in books)
+ {
+ // blank tags are expected. null tags are not
+ if (tags is not null && book.UserDefinedItem.Tags != tags)
+ book.UserDefinedItem.Tags = tags;
+
+ if (bookStatus is not null && book.UserDefinedItem.BookStatus != bookStatus.Value)
+ book.UserDefinedItem.BookStatus = bookStatus.Value;
+
+ // even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
+ if (pdfStatus is not null && book.UserDefinedItem.PdfStatus != pdfStatus.Value)
+ book.UserDefinedItem.PdfStatus = pdfStatus.Value;
+ }
+
+ return UpdateUserDefinedItem(books);
+ }
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
public static int UpdateUserDefinedItem(IEnumerable books)
{
@@ -283,23 +317,8 @@ namespace ApplicationServices
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
- 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
- {
- foreach (var book in books)
- {
- SearchEngineCommands.UpdateLiberatedStatus(book);
- SearchEngineCommands.UpdateBookTags(book);
- }
- }
-
- BookUserDefinedItemCommitted?.Invoke(null, null);
+ if (qtyChanges > 0)
+ BookUserDefinedItemCommitted?.Invoke(null, books);
return qtyChanges;
}
diff --git a/Source/ApplicationServices/SearchEngineCommands.cs b/Source/ApplicationServices/SearchEngineCommands.cs
index 9d871969..7a0e2219 100644
--- a/Source/ApplicationServices/SearchEngineCommands.cs
+++ b/Source/ApplicationServices/SearchEngineCommands.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using DataLayer;
using LibationSearchEngine;
@@ -7,51 +9,99 @@ namespace ApplicationServices
{
public static class SearchEngineCommands
{
- public static void FullReIndex(SearchEngine engine = null)
- {
- engine ??= new SearchEngine();
- var library = DbContexts.GetLibrary_Flat_NoTracking();
- engine.CreateNewIndex(library);
- }
-
- public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
+ #region Search
+ public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
e.Search(searchString)
);
- public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
- e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
+ private static T performSafeQuery(Func func)
+ {
+ var engine = new SearchEngine();
+ try
+ {
+ return func(engine);
+ }
+ catch (FileNotFoundException)
+ {
+ fullReIndex(engine);
+ return func(engine);
+ }
+ }
+ #endregion
+
+ public static EventHandler SearchEngineUpdated;
+
+ #region Update
+ private static bool isUpdating;
+
+ public static void UpdateBooks(IEnumerable books)
+ {
+ // 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 (books.Count() > 15)
+ FullReIndex();
+ else
+ {
+ foreach (var book in books)
+ {
+ UpdateLiberatedStatus(book);
+ UpdateBookTags(book);
+ }
+ }
+ }
+
+ public static void FullReIndex() => performSafeCommand(e =>
+ fullReIndex(e)
);
- public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
+ internal static void UpdateLiberatedStatus(Book book) => performSafeCommand(e =>
e.UpdateLiberatedStatus(book)
);
- private static void performSearchEngineAction_safe(Action action)
+ internal static void UpdateBookTags(Book book) => performSafeCommand(e =>
+ e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
+ );
+
+ private static void performSafeCommand(Action action)
{
- var engine = new SearchEngine();
try
{
- action(engine);
+ update(action);
}
catch (FileNotFoundException)
{
- FullReIndex(engine);
- action(engine);
+ fullReIndex(new SearchEngine());
+ update(action);
}
}
- private static T performSearchEngineFunc_safe(Func func)
+ private static void update(Action action)
{
- var engine = new SearchEngine();
+ if (action is null)
+ return;
+
+ // support nesting incl recursion
+ var prevIsUpdating = isUpdating;
try
{
- return func(engine);
+ isUpdating = true;
+
+ action(new SearchEngine());
+
+ if (!prevIsUpdating)
+ SearchEngineUpdated?.Invoke(null, null);
}
- catch (FileNotFoundException)
+ finally
{
- FullReIndex(engine);
- return func(engine);
+ isUpdating = prevIsUpdating;
}
}
+
+ private static void fullReIndex(SearchEngine engine)
+ {
+ var library = DbContexts.GetLibrary_Flat_NoTracking();
+ engine.CreateNewIndex(library);
+ }
+ #endregion
}
}
diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs
index 1236c7a5..20e55e39 100644
--- a/Source/FileLiberator/DownloadDecryptBook.cs
+++ b/Source/FileLiberator/DownloadDecryptBook.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
+using ApplicationServices;
using AudibleApi;
using DataLayer;
using Dinah.Core;
@@ -82,8 +83,7 @@ namespace FileLiberator
if (Configuration.Instance.DownloadCoverArt)
DownloadCoverArt(libraryBook);
- libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
- ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
+ libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
return new StatusHandler();
}
diff --git a/Source/FileLiberator/DownloadPdf.cs b/Source/FileLiberator/DownloadPdf.cs
index 5bc61f7c..fd87d455 100644
--- a/Source/FileLiberator/DownloadPdf.cs
+++ b/Source/FileLiberator/DownloadPdf.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
+using ApplicationServices;
using DataLayer;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
@@ -29,8 +30,7 @@ namespace FileLiberator
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(actualDownloadedFilePath);
- libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
- ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
+ libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
return result;
}
diff --git a/Source/LibationCli/LibationCli.csproj b/Source/LibationCli/LibationCli.csproj
index 4822e11c..ccc959aa 100644
--- a/Source/LibationCli/LibationCli.csproj
+++ b/Source/LibationCli/LibationCli.csproj
@@ -33,7 +33,6 @@
-
diff --git a/Source/LibationCli/Program.cs b/Source/LibationCli/Program.cs
index 2681d195..e1d2c32f 100644
--- a/Source/LibationCli/Program.cs
+++ b/Source/LibationCli/Program.cs
@@ -27,7 +27,6 @@ namespace LibationCli
// //
//***********************************************//
Setup.Initialize();
- Setup.SubscribeToDatabaseEvents();
var types = Setup.LoadVerbs();
diff --git a/Source/LibationCli/Setup.cs b/Source/LibationCli/Setup.cs
index dec0fff7..fc9e35ea 100644
--- a/Source/LibationCli/Setup.cs
+++ b/Source/LibationCli/Setup.cs
@@ -50,11 +50,6 @@ namespace LibationCli
}
}
- public static void SubscribeToDatabaseEvents()
- {
- DataLayer.UserDefinedItem.ItemChanged += (sender, e) => ApplicationServices.LibraryCommands.UpdateUserDefinedItem(((DataLayer.UserDefinedItem)sender).Book);
- }
-
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetCustomAttribute() is not null)
diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs
index 5f29172d..e44cadec 100644
--- a/Source/LibationWinForms/Form1.VisibleBooks.cs
+++ b/Source/LibationWinForms/Form1.VisibleBooks.cs
@@ -22,7 +22,7 @@ namespace LibationWinForms
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
- private async void setLiberatedVisibleMenuItemAsync(object _, EventArgs __)
+ private async void setLiberatedVisibleMenuItemAsync(object _, object __)
=> await Task.Run(setLiberatedVisibleMenuItem);
void setLiberatedVisibleMenuItem()
{
diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs
index 848c9e4f..be6836a8 100644
--- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs
+++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs
@@ -25,6 +25,7 @@ namespace LibationWinForms.GridView
#endregion
protected override Book Book => LibraryBook.Book;
+
#region Model properties exposed to the view
private DateTime lastStatusUpdate = default;
@@ -75,6 +76,7 @@ namespace LibationWinForms.GridView
if (AudibleProductId != libraryBook.Book.AudibleProductId)
throw new Exception("Invalid grid entry update. IDs must match");
+ UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
setLibraryBook(libraryBook);
NotifyPropertyChanged();
@@ -84,8 +86,6 @@ namespace LibationWinForms.GridView
{
LibraryBook = libraryBook;
- UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
-
// Immutable properties
{
Title = Book.Title;
@@ -110,7 +110,7 @@ namespace LibationWinForms.GridView
///
/// This event handler receives notifications from the model that it has changed.
- /// Save to the database and notify the view that it's changed.
+ /// Notify the view that it's changed.
///
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
@@ -119,23 +119,23 @@ namespace LibationWinForms.GridView
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
+ // UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
+ // - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
+ // - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
switch (itemName)
{
case nameof(udi.Tags):
Book.UserDefinedItem.Tags = udi.Tags;
- SearchEngineCommands.UpdateBookTags(Book);
NotifyPropertyChanged(nameof(DisplayTags));
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
- SearchEngineCommands.UpdateLiberatedStatus(Book);
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
_pdfStatus = udi.PdfStatus;
- SearchEngineCommands.UpdateLiberatedStatus(Book);
NotifyPropertyChanged(nameof(Liberate));
break;
}
@@ -143,23 +143,8 @@ namespace LibationWinForms.GridView
/// Save edits to the database
public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
- {
- // 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);
- }
+ // MVVM pass-through
+ => Book.UpdateBook(newTags, bookStatus: bookStatus, pdfStatus: pdfStatus);
#endregion
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
index 7c9778a5..9891e68b 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
@@ -1,8 +1,4 @@
-using DataLayer;
-using Dinah.Core;
-using FileLiberator;
-using LibationFileManager;
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
@@ -10,6 +6,11 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Forms;
+using ApplicationServices;
+using DataLayer;
+using Dinah.Core;
+using FileLiberator;
+using LibationFileManager;
namespace LibationWinForms.ProcessQueue
{
@@ -353,8 +354,7 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == SkipResult)
{
- libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
- ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
+ libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
index a55dc071..d6b9d322 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
+using ApplicationServices;
namespace LibationWinForms.ProcessQueue
{
@@ -156,10 +157,7 @@ namespace LibationWinForms.ProcessQueue
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
- {
- nextBook.LibraryBook.Book.UserDefinedItem.BookStatus = DataLayer.LiberatedStatus.Error;
- ApplicationServices.LibraryCommands.UpdateUserDefinedItem(nextBook.LibraryBook.Book);
- }
+ nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
}
Queue_CompletedCountChanged(this, 0);
counterTimer.Stop();