From a55da5f1879ed8544211ee0c907584ba90e7b945 Mon Sep 17 00:00:00 2001 From: MBucari Date: Thu, 20 Nov 2025 22:05:16 -0700 Subject: [PATCH] Refactor DbContext access and disposal - Remove instance queue. This is a database, after all, and is designed to be accessed and written to concurrently - Reduce the number of calls to DbContexts.Create() - Ensure that no LibationContext remains open across an await boundary. Multithread context access is the most likely culprit for past issues. - Make all Update UserDefinedItem methods asynchronous. --- .../BulkSetDownloadStatus.cs | 4 +- Source/ApplicationServices/DbContexts.cs | 30 +- Source/ApplicationServices/LibraryCommands.cs | 283 +++++++++--------- Source/FileLiberator/AudioFileStorageExt.cs | 4 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileLiberator/DownloadPdf.cs | 2 +- .../ViewModels/TrashBinViewModel.cs | 6 +- Source/HangoverWinForms/Form1.Deleted.cs | 15 +- .../Dialogs/BookDetailsDialog.axaml.cs | 7 +- .../Dialogs/LocateAudiobooksDialog.axaml.cs | 6 +- .../Dialogs/TrashBinDialog.axaml.cs | 7 +- .../ViewModels/MainVM.VisibleBooks.cs | 8 +- .../LibationCli/Options/GetLicenseOptions.cs | 3 +- Source/LibationCli/Options/LiberateOptions.cs | 11 +- .../Options/SetDownloadStatusOptions.cs | 4 +- .../Options/_ProcessableOptionsBase.cs | 7 +- .../GridView/GridContextMenu.cs | 7 +- Source/LibationUiBase/GridView/GridEntry.cs | 2 +- .../ProcessQueue/ProcessBookViewModel.cs | 2 +- .../ProcessQueue/ProcessQueueViewModel.cs | 2 +- .../LibationUiBase/SeriesView/AyceButton.cs | 5 +- .../Dialogs/LocateAudiobooksDialog.cs | 6 +- .../Dialogs/TrashBinDialog.cs | 7 +- Source/LibationWinForms/Form1.VisibleBooks.cs | 14 +- .../LibationWinForms/GridView/ProductsGrid.cs | 2 +- 25 files changed, 218 insertions(+), 228 deletions(-) diff --git a/Source/ApplicationServices/BulkSetDownloadStatus.cs b/Source/ApplicationServices/BulkSetDownloadStatus.cs index 13a303be..bd63446f 100644 --- a/Source/ApplicationServices/BulkSetDownloadStatus.cs +++ b/Source/ApplicationServices/BulkSetDownloadStatus.cs @@ -69,10 +69,10 @@ namespace ApplicationServices return Count; } - public void Execute() + public async Task ExecuteAsync() { foreach (var a in actionSets) - a.LibraryBooks.UpdateBookStatus(a.newStatus); + await a.LibraryBooks.UpdateBookStatusAsync(a.newStatus); } } } diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index 6678ed30..8e5d44e2 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -3,20 +3,20 @@ using LibationFileManager; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; +#nullable enable namespace ApplicationServices { public static class DbContexts { /// Use for fully functional context, incl. SaveChanges(). For query-only, use the other method public static LibationContext GetContext() - => InstanceQueue.WaitToCreateInstance(() => - { - var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) - ? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString) - : LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString); - context.Database.Migrate(); - return context; - }); + { + var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) + ? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString) + : LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString); + context.Database.Migrate(); + return context; + } /// Use for full library querying. No lazy loading public static List GetLibrary_Flat_NoTracking(bool includeParents = false) @@ -24,5 +24,17 @@ namespace ApplicationServices using var context = GetContext(); return context.GetLibrary_Flat_NoTracking(includeParents); } - } + + public static List GetDeletedLibraryBooks() + { + using var context = GetContext(); + return context.GetDeletedLibraryBooks(); + } + + public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true) + { + using var context = GetContext(); + return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative); + } + } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index f750eaff..24e80a01 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AudibleApi; +using AudibleApi; using AudibleUtilities; using DataLayer; using Dinah.Core; @@ -11,8 +6,14 @@ using Dinah.Core.Logging; using DtoImporterService; using FileManager; using LibationFileManager; +using Microsoft.Extensions.DependencyModel; using Newtonsoft.Json.Linq; using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using static DtoImporterService.PerfLogger; #nullable enable @@ -141,9 +142,9 @@ namespace ApplicationServices return default; Log.Logger.Information("Begin long-running import"); - logTime($"pre {nameof(importIntoDbAsync)}"); - newCount = await importIntoDbAsync(importItems); - logTime($"post {nameof(importIntoDbAsync)}"); + logTime($"pre {nameof(ImportIntoDbAsync)}"); + newCount = await Task.Run(() => ImportIntoDbAsync(importItems)); + logTime($"post {nameof(ImportIntoDbAsync)}"); Log.Logger.Information($"Import complete. New count {newCount}"); return (totalCount, newCount); @@ -180,7 +181,8 @@ namespace ApplicationServices } } - public static async Task ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName) + public static Task ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName) => Task.Run(() => importSingleToDb(item, accountId, localeName)); + private static int importSingleToDb(AudibleApi.Common.Item item, string accountId, string localeName) { ArgumentValidator.EnsureNotNull(item, "item"); ArgumentValidator.EnsureNotNull(accountId, "accountId"); @@ -203,35 +205,23 @@ namespace ApplicationServices return 0; } - using var context = DbContexts.GetContext(); + return DoDbSizeChangeOperation(ctx => + { + var bookImporter = new BookImporter(ctx); + bookImporter.Import(importItems); + var book = ctx.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId); - var bookImporter = new BookImporter(context); - await Task.Run(() => bookImporter.Import(importItems)); - var book = await Task.Run(() => context.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId)); - - if (book is null) - { - book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId); - context.LibraryBooks.Add(book); - } - else - { - book.AbsentFromLastScan = false; - } - - try - { - int qtyChanged = await Task.Run(() => SaveContext(context)); - if (qtyChanged > 0) - await Task.Run(() => finalizeLibrarySizeChange(context)); - return qtyChanged; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error adding single library book to DB. {@DebugInfo}", new { item, accountId, localeName }); - return 0; - } - } + if (book is null) + { + book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId); + ctx.LibraryBooks.Add(book); + } + else + { + book.AbsentFromLastScan = false; + } + }); + } private static LogArchiver? openLogArchive(string? archivePath) { @@ -347,23 +337,21 @@ namespace ApplicationServices } } - private static async Task importIntoDbAsync(List importItems) + private static async Task ImportIntoDbAsync(List importItems) => await Task.Run(() => importIntoDb(importItems)); + private static int importIntoDb(List importItems) { - logTime("importIntoDbAsync -- pre db"); - using var context = DbContexts.GetContext(); - var libraryBookImporter = new LibraryBookImporter(context); - var newCount = await Task.Run(() => libraryBookImporter.Import(importItems)); - logTime("importIntoDbAsync -- post Import()"); - int qtyChanges = SaveContext(context); - logTime("importIntoDbAsync -- post SaveChanges"); + logTime("importIntoDbAsync -- pre db"); - // this is any changes at all to the database, not just new books - if (qtyChanges > 0) - await Task.Run(() => finalizeLibrarySizeChange(context)); - logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); + int newCount = 0; - return newCount; - } + DoDbSizeChangeOperation(ctx => + { + var libraryBookImporter = new LibraryBookImporter(ctx); + newCount = libraryBookImporter.Import(importItems); + logTime("importIntoDbAsync -- post Import()"); + }); + return newCount; + } public static int SaveContext(LibationContext context) { @@ -389,57 +377,58 @@ namespace ApplicationServices #region remove/restore books public static Task RemoveBooksAsync(this IEnumerable idsToRemove) => Task.Run(() => removeBooks(idsToRemove)); - public static int RemoveBook(this LibraryBook idToRemove) => removeBooks(new[] { idToRemove }); private static int removeBooks(IEnumerable removeLibraryBooks) - { - try - { - if (removeLibraryBooks is null || !removeLibraryBooks.Any()) - return 0; - - using var context = DbContexts.GetContext(); + { + if (removeLibraryBooks is null || !removeLibraryBooks.Any()) + return 0; + return DoDbSizeChangeOperation(ctx => + { // Entry() NoTracking entities before SaveChanges() foreach (var lb in removeLibraryBooks) - { + { lb.IsDeleted = true; - context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; } + }); + } - var qtyChanges = context.SaveChanges(); - if (qtyChanges > 0) - finalizeLibrarySizeChange(context); - - return qtyChanges; - } + public static Task RestoreBooksAsync(this IEnumerable idsToRemove) => Task.Run(() => restoreBooks(idsToRemove)); + private static int restoreBooks(this IEnumerable libraryBooks) + { + if (libraryBooks is null || !libraryBooks.Any()) + return 0; + try + { + return DoDbSizeChangeOperation(ctx => + { + // Entry() NoTracking entities before SaveChanges() + foreach (var lb in libraryBooks) + { + lb.IsDeleted = false; + ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + } + }); + } catch (Exception ex) { - Log.Logger.Error(ex, "Error removing books"); + Log.Logger.Error(ex, "Error restoring books"); throw; } } - public static int RestoreBooks(this IEnumerable libraryBooks) - { - try + public static Task PermanentlyDeleteBooksAsync(this IEnumerable idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove)); + private static int permanentlyDeleteBooks(this IEnumerable libraryBooks) + { + if (libraryBooks is null || !libraryBooks.Any()) + return 0; + try { - if (libraryBooks is null || !libraryBooks.Any()) - return 0; - - using var context = DbContexts.GetContext(); - - // Entry() NoTracking entities before SaveChanges() - foreach (var lb in libraryBooks) - { - lb.IsDeleted = false; - context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - } - - var qtyChanges = context.SaveChanges(); - if (qtyChanges > 0) - finalizeLibrarySizeChange(context); - - return qtyChanges; + return DoDbSizeChangeOperation(ctx => + { + ctx.LibraryBooks.RemoveRange(libraryBooks); + ctx.Books.RemoveRange(libraryBooks.Select(lb => lb.Book)); + }); } catch (Exception ex) { @@ -448,36 +437,40 @@ namespace ApplicationServices } } - public static int PermanentlyDeleteBooks(this IEnumerable libraryBooks) + static int DoDbSizeChangeOperation(Action action) { - try - { - if (libraryBooks is null || !libraryBooks.Any()) - return 0; + try + { + int qtyChanges; + List? library; - using var context = DbContexts.GetContext(); + using (var context = DbContexts.GetContext()) + { + action?.Invoke(context); - context.LibraryBooks.RemoveRange(libraryBooks); - context.Books.RemoveRange(libraryBooks.Select(lb => lb.Book)); + qtyChanges = SaveContext(context); + logTime("importIntoDbAsync -- post SaveChanges"); + library = qtyChanges == 0 ? null : context.GetLibrary_Flat_NoTracking(includeParents: true); + } - var qtyChanges = context.SaveChanges(); - if (qtyChanges > 0) - finalizeLibrarySizeChange(context); + if (library is not null) + finalizeLibrarySizeChange(library); + logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); return qtyChanges; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error restoring books"); - throw; - } - } + } + catch (Exception ex) + { + Log.Logger.Error(ex, "Error performing DB Size change operation"); + throw; + } + } + #endregion // call this whenever books are added or removed from library - private static void finalizeLibrarySizeChange(LibationContext context) + private static void finalizeLibrarySizeChange(List library) { - var library = context.GetLibrary_Flat_NoTracking(includeParents: true); LibrarySizeChanged?.Invoke(null, library); } @@ -490,21 +483,21 @@ namespace ApplicationServices public static event EventHandler>? BookUserDefinedItemCommitted; #region Update book details - public static int UpdateUserDefinedItem( + public static async Task UpdateUserDefinedItemAsync( this LibraryBook lb, string? tags = null, LiberatedStatus? bookStatus = null, LiberatedStatus? pdfStatus = null, Rating? rating = null) - => new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating); + => await UpdateUserDefinedItemAsync([lb], tags, bookStatus, pdfStatus, rating); - public static int UpdateUserDefinedItem( + public static async Task UpdateUserDefinedItemAsync( this IEnumerable lb, string? tags = null, LiberatedStatus? bookStatus = null, LiberatedStatus? pdfStatus = null, Rating? rating = null) - => updateUserDefinedItem( + => await UpdateUserDefinedItemAsync( lb, udi => { // blank tags are expected. null tags are not @@ -521,52 +514,54 @@ namespace ApplicationServices udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating); }); - public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion) - => lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); }); + public static async Task UpdateBookStatusAsync(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion) + => await lb.UpdateUserDefinedItemAsync(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); }); - public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus) - => libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); - public static int UpdateBookStatus(this IEnumerable libraryBooks, LiberatedStatus bookStatus) - => libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); + public static async Task UpdateBookStatusAsync(this LibraryBook libraryBook, LiberatedStatus bookStatus) + => await libraryBook.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus); + public static async Task UpdateBookStatusAsync(this IEnumerable libraryBooks, LiberatedStatus bookStatus) + => await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus); - public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus) - => libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); - public static int UpdatePdfStatus(this IEnumerable libraryBooks, LiberatedStatus pdfStatus) - => libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); + public static async Task UpdatePdfStatusAsync(this LibraryBook libraryBook, LiberatedStatus pdfStatus) + => await libraryBook.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus)); + public static async Task UpdatePdfStatusAsync(this IEnumerable libraryBooks, LiberatedStatus pdfStatus) + => await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus)); - public static int UpdateTags(this LibraryBook libraryBook, string tags) - => libraryBook.UpdateUserDefinedItem(udi => udi.Tags = tags); - public static int UpdateTags(this IEnumerable libraryBooks, string tags) - => libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags); + public static async Task UpdateTagsAsync(this LibraryBook libraryBook, string tags) + => await libraryBook.UpdateUserDefinedItemAsync(udi => udi.Tags = tags); + public static async Task UpdateTagsAsync(this IEnumerable libraryBooks, string tags) + => await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags); - public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action action) - => libraryBook.updateUserDefinedItem(action); - public static int UpdateUserDefinedItem(this IEnumerable libraryBooks, Action action) - => libraryBooks.updateUserDefinedItem(action); + public static async Task UpdateUserDefinedItemAsync(this LibraryBook libraryBook, Action action) + => await UpdateUserDefinedItemAsync([libraryBook], action); - private static int updateUserDefinedItem(this LibraryBook libraryBook, Action action) => new[] { libraryBook }.updateUserDefinedItem(action); - private static int updateUserDefinedItem(this IEnumerable libraryBooks, Action action) + public static Task UpdateUserDefinedItemAsync(this IEnumerable libraryBooks, Action action) + => Task.Run(() => libraryBooks.updateUserDefinedItem(action)); + + private static int updateUserDefinedItem(this IEnumerable libraryBooks, Action action) { try { if (libraryBooks is null || !libraryBooks.Any()) return 0; - using var context = DbContexts.GetContext(); - - // Entry() instead of Attach() due to possible stack overflow with large tables - foreach (var book in libraryBooks) + int qtyChanges; + using (var context = DbContexts.GetContext()) { - action?.Invoke(book.Book.UserDefinedItem); + // Entry() instead of Attach() due to possible stack overflow with large tables + foreach (var book in libraryBooks) + { + action?.Invoke(book.Book.UserDefinedItem); - var udiEntity = context.Entry(book.Book.UserDefinedItem); + var udiEntity = context.Entry(book.Book.UserDefinedItem); - udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified; - if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry ratingEntry) - ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; - } + udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry ratingEntry) + ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + } - var qtyChanges = context.SaveChanges(); + qtyChanges = context.SaveChanges(); + } if (qtyChanges > 0) BookUserDefinedItemCommitted?.Invoke(null, libraryBooks); diff --git a/Source/FileLiberator/AudioFileStorageExt.cs b/Source/FileLiberator/AudioFileStorageExt.cs index 90ab45b1..c180fe85 100644 --- a/Source/FileLiberator/AudioFileStorageExt.cs +++ b/Source/FileLiberator/AudioFileStorageExt.cs @@ -23,9 +23,7 @@ namespace FileLiberator var series = libraryBook.Book.SeriesLink.SingleOrDefault(); if (series is not null) { - using var context = ApplicationServices.DbContexts.GetContext(); - var seriesParent = context.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); - + LibraryBook seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); if (seriesParent is not null) { return Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, ""); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index eeaf06c0..97ba1e16 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -100,7 +100,7 @@ namespace FileLiberator { if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested) { - libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion, audioFormat, audioVersion); + await libraryBook.UpdateBookStatusAsync(LiberatedStatus.Liberated, Configuration.LibationVersion, audioFormat, audioVersion); SetDirectoryTime(libraryBook, finalStorageDir); foreach (var cacheFile in result.CacheFiles.Where(f => File.Exists(f.FilePath))) { diff --git a/Source/FileLiberator/DownloadPdf.cs b/Source/FileLiberator/DownloadPdf.cs index 1e7f325a..52646fcc 100644 --- a/Source/FileLiberator/DownloadPdf.cs +++ b/Source/FileLiberator/DownloadPdf.cs @@ -35,7 +35,7 @@ namespace FileLiberator SetFileTime(libraryBook, actualDownloadedFilePath); SetDirectoryTime(libraryBook, Path.GetDirectoryName(actualDownloadedFilePath)); } - libraryBook.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated); + await libraryBook.UpdatePdfStatusAsync(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated); return result; } diff --git a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs index 22b5d15b..da2ea8fa 100644 --- a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs +++ b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs @@ -80,7 +80,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable public async Task RestoreCheckedAsync() { ControlsEnabled = false; - var qtyChanges = await Task.Run(CheckedBooks.RestoreBooks); + var qtyChanges = await CheckedBooks.RestoreBooksAsync(); if (qtyChanges > 0) Reload(); ControlsEnabled = true; @@ -89,7 +89,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable public async Task PermanentlyDeleteCheckedAsync() { ControlsEnabled = false; - var qtyChanges = await Task.Run(CheckedBooks.PermanentlyDeleteBooks); + var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync(); if (qtyChanges > 0) Reload(); ControlsEnabled = true; @@ -97,7 +97,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable public void Reload() { - var deletedBooks = DbContexts.GetContext().GetDeletedLibraryBooks(); + var deletedBooks = DbContexts.GetDeletedLibraryBooks(); DeletedBooks.Clear(); DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb })); diff --git a/Source/HangoverWinForms/Form1.Deleted.cs b/Source/HangoverWinForms/Form1.Deleted.cs index 0419d827..e48bd511 100644 --- a/Source/HangoverWinForms/Form1.Deleted.cs +++ b/Source/HangoverWinForms/Form1.Deleted.cs @@ -39,19 +39,22 @@ namespace HangoverWinForms deletedCbl.SetItemChecked(i, false); } - private void saveBtn_Click(object sender, EventArgs e) + private async void saveBtn_Click(object sender, EventArgs e) { var libraryBooksToRestore = deletedCbl.CheckedItems.Cast().ToList(); - var qtyChanges = libraryBooksToRestore.RestoreBooks(); + saveBtn.Enabled = false; + var qtyChanges = await libraryBooksToRestore.RestoreBooksAsync(); if (qtyChanges > 0) - reload(); - } + Invoke(reload); + Invoke(() => saveBtn.Enabled = true); + } private void reload() { deletedCbl.Items.Clear(); - var deletedBooks = DbContexts.GetContext().GetDeletedLibraryBooks(); - foreach (var lb in deletedBooks) + List deletedBooks = DbContexts.GetDeletedLibraryBooks(); + + foreach (var lb in deletedBooks) deletedCbl.Items.Add(lb); setLabel(); diff --git a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs index 7772893d..3f60f925 100644 --- a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs @@ -10,6 +10,7 @@ using LibationFileManager; using ReactiveUI; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Windows.Input; namespace LibationAvalonia.Dialogs @@ -58,10 +59,10 @@ namespace LibationAvalonia.Dialogs LibraryBook = libraryBook; } - protected override void SaveAndClose() + protected override async Task SaveAndCloseAsync() { - LibraryBook.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus); - base.SaveAndClose(); + await LibraryBook.UpdateUserDefinedItemAsync(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus); + await base.SaveAndCloseAsync(); } public void BookStatus_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs index f1eb4c75..dbabcf36 100644 --- a/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs @@ -81,17 +81,15 @@ namespace LibationAvalonia.Dialogs return; } - using var context = DbContexts.GetContext(); - await foreach (var book in AudioFileStorage.FindAudiobooksAsync(selectedFolder, tokenSource.Token)) { try { FilePathCache.Insert(book); - var lb = context.GetLibraryBook_Flat_NoTracking(book.Id); + var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id); if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) - await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); + await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated); tokenSource.Token.ThrowIfCancellationRequested(); FileFound?.Invoke(this, book); diff --git a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs index 324aee12..b3a69b4f 100644 --- a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs @@ -104,7 +104,7 @@ namespace LibationAvalonia.Dialogs public async Task RestoreCheckedAsync() { ControlsEnabled = false; - var qtyChanges = await Task.Run(CheckedBooks.RestoreBooks); + var qtyChanges = await CheckedBooks.RestoreBooksAsync(); if (qtyChanges > 0) Reload(); ControlsEnabled = true; @@ -113,7 +113,7 @@ namespace LibationAvalonia.Dialogs public async Task PermanentlyDeleteCheckedAsync() { ControlsEnabled = false; - var qtyChanges = await Task.Run(CheckedBooks.PermanentlyDeleteBooks); + var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync(); if (qtyChanges > 0) Reload(); ControlsEnabled = true; @@ -121,8 +121,7 @@ namespace LibationAvalonia.Dialogs private void Reload() { - using var context = DbContexts.GetContext(); - var deletedBooks = context.GetDeletedLibraryBooks(); + var deletedBooks = DbContexts.GetDeletedLibraryBooks(); DeletedBooks.Clear(); DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb })); diff --git a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs index 728a0b1f..bc40f7d5 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs @@ -102,7 +102,7 @@ namespace LibationAvalonia.ViewModels if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdateTags(dialog.NewTags); + await visibleLibraryBooks.UpdateTagsAsync(dialog.NewTags); } public async Task SetBookDownloadedAsync() @@ -124,7 +124,7 @@ namespace LibationAvalonia.ViewModels if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); + await visibleLibraryBooks.UpdateBookStatusAsync(dialog.BookLiberatedStatus); } public async Task SetPdfDownloadedAsync() @@ -146,7 +146,7 @@ namespace LibationAvalonia.ViewModels if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus); + await visibleLibraryBooks.UpdatePdfStatusAsync(dialog.BookLiberatedStatus); } public async Task SetDownloadedAutoAsync() @@ -172,7 +172,7 @@ namespace LibationAvalonia.ViewModels if (confirmationResult != DialogResult.Yes) return; - bulkSetStatus.Execute(); + await bulkSetStatus.ExecuteAsync(); } public async Task RemoveVisibleAsync() diff --git a/Source/LibationCli/Options/GetLicenseOptions.cs b/Source/LibationCli/Options/GetLicenseOptions.cs index ea8296c8..bc637953 100644 --- a/Source/LibationCli/Options/GetLicenseOptions.cs +++ b/Source/LibationCli/Options/GetLicenseOptions.cs @@ -25,8 +25,7 @@ internal class GetLicenseOptions : OptionsBase return; } - using var dbContext = DbContexts.GetContext(); - if (dbContext.GetLibraryBook_Flat_NoTracking(Asin) is not LibraryBook libraryBook) + if (DbContexts.GetLibraryBook_Flat_NoTracking(Asin) is not LibraryBook libraryBook) { Console.Error.WriteLine($"Book not found with asin={Asin}"); return; diff --git a/Source/LibationCli/Options/LiberateOptions.cs b/Source/LibationCli/Options/LiberateOptions.cs index 0ff87aaa..fa327eee 100644 --- a/Source/LibationCli/Options/LiberateOptions.cs +++ b/Source/LibationCli/Options/LiberateOptions.cs @@ -53,15 +53,10 @@ namespace LibationCli return; } - LibraryBook libraryBook; - using (var dbContext = DbContexts.GetContext()) + if (DbContexts.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook libraryBook) { - if (dbContext.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook lb) - { - Console.Error.WriteLine($"Book not found with asin={asin}"); - return; - } - libraryBook = lb; + Console.Error.WriteLine($"Book not found with asin={asin}"); + return; } SetDownloadedStatus(libraryBook); diff --git a/Source/LibationCli/Options/SetDownloadStatusOptions.cs b/Source/LibationCli/Options/SetDownloadStatusOptions.cs index b563c152..1baeed57 100644 --- a/Source/LibationCli/Options/SetDownloadStatusOptions.cs +++ b/Source/LibationCli/Options/SetDownloadStatusOptions.cs @@ -53,14 +53,14 @@ namespace LibationCli { var status = SetDownloaded ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated; - var num = libraryBooks.UpdateBookStatus(status); + var num = await libraryBooks.UpdateBookStatusAsync(status); Console.WriteLine($"Set LiberatedStatus to '{status}' on {"book".PluralizeWithCount(num)}"); } else { var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded); await Task.Run(() => bulkSetStatus.Discover()); - bulkSetStatus.Execute(); + await bulkSetStatus.ExecuteAsync(); foreach (var msg in bulkSetStatus.Messages) Console.WriteLine(msg); diff --git a/Source/LibationCli/Options/_ProcessableOptionsBase.cs b/Source/LibationCli/Options/_ProcessableOptionsBase.cs index 6450d20e..bb719300 100644 --- a/Source/LibationCli/Options/_ProcessableOptionsBase.cs +++ b/Source/LibationCli/Options/_ProcessableOptionsBase.cs @@ -77,12 +77,7 @@ namespace LibationCli { foreach (var asin in Asins.Select(a => a.TrimStart('[').TrimEnd(']'))) { - LibraryBook? lb = null; - using (var dbContext = DbContexts.GetContext()) - { - lb = dbContext.GetLibraryBook_Flat_NoTracking(asin, caseSensative: false); - } - if (lb is not null) + if (DbContexts.GetLibraryBook_Flat_NoTracking(asin, caseSensative: false) is LibraryBook lb) { config?.Invoke(lb); await ProcessOneAsync(Processable, lb, true); diff --git a/Source/LibationUiBase/GridView/GridContextMenu.cs b/Source/LibationUiBase/GridView/GridContextMenu.cs index dd73fec2..115912d6 100644 --- a/Source/LibationUiBase/GridView/GridContextMenu.cs +++ b/Source/LibationUiBase/GridView/GridContextMenu.cs @@ -57,7 +57,7 @@ public class GridContextMenu public void SetDownloaded() { LibraryBookEntries.Select(e => e.LibraryBook) - .UpdateUserDefinedItem(udi => + .UpdateUserDefinedItemAsync(udi => { udi.BookStatus = LiberatedStatus.Liberated; if (udi.Book.HasPdf()) @@ -68,7 +68,7 @@ public class GridContextMenu public void SetNotDownloaded() { LibraryBookEntries.Select(e => e.LibraryBook) - .UpdateUserDefinedItem(udi => + .UpdateUserDefinedItemAsync(udi => { udi.BookStatus = LiberatedStatus.NotLiberated; if (udi.Book.HasPdf()) @@ -90,8 +90,7 @@ public class GridContextMenu Configuration.Instance.SavePodcastsToParentFolder && libraryBook.Book.SeriesLink.SingleOrDefault() is SeriesBook series) { - using var context = DbContexts.GetContext(); - var seriesParent = context.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); + var seriesParent = DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); folderDto = seriesParent?.ToDto() ?? fileDto; } diff --git a/Source/LibationUiBase/GridView/GridEntry.cs b/Source/LibationUiBase/GridView/GridEntry.cs index 72b4ba1d..754a3324 100644 --- a/Source/LibationUiBase/GridView/GridEntry.cs +++ b/Source/LibationUiBase/GridView/GridEntry.cs @@ -91,7 +91,7 @@ namespace LibationUiBase.GridView var api = await LibraryBook.GetApiAsync(); if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating)) - LibraryBook.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating); + await LibraryBook.UpdateUserDefinedItemAsync(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating); } #endregion diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs index c47da12e..78735622 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs @@ -364,7 +364,7 @@ public class ProcessBookViewModel : ReactiveObject if (dialogResult == SkipResult) { - libraryBook.UpdateBookStatus(LiberatedStatus.Error); + await libraryBook.UpdateBookStatusAsync(LiberatedStatus.Error); LogInfo($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}"); } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 9a3472d8..e666edd5 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -277,7 +277,7 @@ public class ProcessQueueViewModel : ReactiveObject else if (result == ProcessBookResult.FailedAbort) Queue.ClearQueue(); else if (result == ProcessBookResult.FailedSkip) - nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error); + await nextBook.LibraryBook.UpdateBookStatusAsync(LiberatedStatus.Error); else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) { await MessageBoxBase.Show($""" diff --git a/Source/LibationUiBase/SeriesView/AyceButton.cs b/Source/LibationUiBase/SeriesView/AyceButton.cs index 00fb2403..842dc85f 100644 --- a/Source/LibationUiBase/SeriesView/AyceButton.cs +++ b/Source/LibationUiBase/SeriesView/AyceButton.cs @@ -70,9 +70,8 @@ namespace LibationUiBase.SeriesView if (await api.RemoveItemFromLibraryAsync(Item.ProductId)) { - using var context = DbContexts.GetContext(); - var lb = context.GetLibraryBook_Flat_NoTracking(Item.ProductId); - int result = await Task.Run((new[] { lb }).PermanentlyDeleteBooks); + var lb = DbContexts.GetLibraryBook_Flat_NoTracking(Item.ProductId); + int result = await LibraryCommands.PermanentlyDeleteBooksAsync([lb]); InLibrary = result == 0; } } diff --git a/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs b/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs index 88e3c552..c05d0e34 100644 --- a/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs +++ b/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs @@ -71,17 +71,15 @@ namespace LibationWinForms.Dialogs return; } - using var context = DbContexts.GetContext(); - await foreach (var book in AudioFileStorage.FindAudiobooksAsync(fbd.SelectedPath, tokenSource.Token)) { try { FilePathCache.Insert(book); - var lb = context.GetLibraryBook_Flat_NoTracking(book.Id); + var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id); if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) - await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); + await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated); tokenSource.Token.ThrowIfCancellationRequested(); this.Invoke(FileFound, this, book); diff --git a/Source/LibationWinForms/Dialogs/TrashBinDialog.cs b/Source/LibationWinForms/Dialogs/TrashBinDialog.cs index a4058633..fbb6c452 100644 --- a/Source/LibationWinForms/Dialogs/TrashBinDialog.cs +++ b/Source/LibationWinForms/Dialogs/TrashBinDialog.cs @@ -23,8 +23,7 @@ namespace LibationWinForms.Dialogs deletedCheckedTemplate = deletedCheckedLbl.Text; - using var context = DbContexts.GetContext(); - var deletedBooks = context.GetDeletedLibraryBooks(); + var deletedBooks = DbContexts.GetDeletedLibraryBooks(); foreach (var lb in deletedBooks) deletedCbl.Items.Add(lb); @@ -44,7 +43,7 @@ namespace LibationWinForms.Dialogs var removed = deletedCbl.CheckedItems.Cast().ToList(); removeFromCheckList(removed); - await Task.Run(removed.PermanentlyDeleteBooks); + await removed.PermanentlyDeleteBooksAsync(); setControlsEnabled(true); } @@ -56,7 +55,7 @@ namespace LibationWinForms.Dialogs var removed = deletedCbl.CheckedItems.Cast().ToList(); removeFromCheckList(removed); - await Task.Run(removed.RestoreBooks); + await removed.RestoreBooksAsync(); setControlsEnabled(true); } diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs index fd248c3b..7c1f99ee 100644 --- a/Source/LibationWinForms/Form1.VisibleBooks.cs +++ b/Source/LibationWinForms/Form1.VisibleBooks.cs @@ -68,7 +68,7 @@ namespace LibationWinForms } } - private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e) + private async void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e) { var dialog = new TagsBatchDialog(); var result = dialog.ShowDialog(); @@ -86,10 +86,10 @@ namespace LibationWinForms if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdateTags(dialog.NewTags); + await visibleLibraryBooks.UpdateTagsAsync(dialog.NewTags); } - private void setBookDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e) + private async void setBookDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e) { var dialog = new LiberatedStatusBatchManualDialog(); var result = dialog.ShowDialog(); @@ -107,10 +107,10 @@ namespace LibationWinForms if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); + await visibleLibraryBooks.UpdateBookStatusAsync(dialog.BookLiberatedStatus); } - private void setPdfDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e) + private async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e) { var dialog = new LiberatedStatusBatchManualDialog(isPdf: true); var result = dialog.ShowDialog(); @@ -128,7 +128,7 @@ namespace LibationWinForms if (confirmationResult != DialogResult.Yes) return; - visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus); + await visibleLibraryBooks.UpdatePdfStatusAsync(dialog.BookLiberatedStatus); } private async void setDownloadedAutoToolStripMenuItem_Click(object sender, EventArgs e) @@ -154,7 +154,7 @@ namespace LibationWinForms if (confirmationResult != DialogResult.Yes) return; - bulkSetStatus.Execute(); + await bulkSetStatus.ExecuteAsync(); } private async void removeToolStripMenuItem_Click(object sender, EventArgs e) diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index f17a62cb..2f8f1941 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -48,7 +48,7 @@ namespace LibationWinForms.GridView gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded; removeGVColumn.Frozen = false; - defaultFont = gridEntryDataGridView.DefaultCellStyle.Font; + defaultFont = gridEntryDataGridView.DefaultCellStyle.Font ?? gridEntryDataGridView.Font; setGridFontScale(Configuration.Instance.GridFontScaleFactor); setGridScale(Configuration.Instance.GridScaleFactor); Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;