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.
This commit is contained in:
MBucari
2025-11-20 22:05:16 -07:00
parent f9ac0253fb
commit a55da5f187
25 changed files with 218 additions and 228 deletions

View File

@@ -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);
}
}
}

View File

@@ -3,20 +3,20 @@ using LibationFileManager;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
#nullable enable
namespace ApplicationServices
{
public static class DbContexts
{
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
public static LibationContext GetContext()
=> InstanceQueue<LibationContext>.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;
}
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> 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<LibraryBook> 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);
}
}
}

View File

@@ -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<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
public static Task<int> 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<int> importIntoDbAsync(List<ImportItem> importItems)
private static async Task<int> ImportIntoDbAsync(List<ImportItem> importItems) => await Task.Run(() => importIntoDb(importItems));
private static int importIntoDb(List<ImportItem> 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<int> RemoveBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
public static int RemoveBook(this LibraryBook idToRemove) => removeBooks(new[] { idToRemove });
private static int removeBooks(IEnumerable<LibraryBook> 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<int> RestoreBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => restoreBooks(idsToRemove));
private static int restoreBooks(this IEnumerable<LibraryBook> 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<LibraryBook> libraryBooks)
{
try
public static Task<int> PermanentlyDeleteBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove));
private static int permanentlyDeleteBooks(this IEnumerable<LibraryBook> 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<LibraryBook> libraryBooks)
static int DoDbSizeChangeOperation(Action<LibationContext> action)
{
try
{
if (libraryBooks is null || !libraryBooks.Any())
return 0;
try
{
int qtyChanges;
List<LibraryBook>? 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<LibraryBook> library)
{
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
LibrarySizeChanged?.Invoke(null, library);
}
@@ -490,21 +483,21 @@ namespace ApplicationServices
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(
public static async Task<int> 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<int> UpdateUserDefinedItemAsync(
this IEnumerable<LibraryBook> 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<int> 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<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static async Task<int> UpdateBookStatusAsync(this LibraryBook libraryBook, LiberatedStatus bookStatus)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus);
public static async Task<int> UpdateBookStatusAsync(this IEnumerable<LibraryBook> 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<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static async Task<int> UpdatePdfStatusAsync(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus));
public static async Task<int> UpdatePdfStatusAsync(this IEnumerable<LibraryBook> 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<LibraryBook> libraryBooks, string tags)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static async Task<int> UpdateTagsAsync(this LibraryBook libraryBook, string tags)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
public static async Task<int> UpdateTagsAsync(this IEnumerable<LibraryBook> libraryBooks, string tags)
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
=> libraryBook.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
=> libraryBooks.updateUserDefinedItem(action);
public static async Task<int> UpdateUserDefinedItemAsync(this LibraryBook libraryBook, Action<UserDefinedItem> action)
=> await UpdateUserDefinedItemAsync([libraryBook], action);
private static int updateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action) => new[] { libraryBook }.updateUserDefinedItem(action);
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
public static Task<int> UpdateUserDefinedItemAsync(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
=> Task.Run(() => libraryBooks.updateUserDefinedItem(action));
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> 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<Rating> 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<Rating> ratingEntry)
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges();
qtyChanges = context.SaveChanges();
}
if (qtyChanges > 0)
BookUserDefinedItemCommitted?.Invoke(null, libraryBooks);

View File

@@ -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, "");

View File

@@ -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)))
{

View File

@@ -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;
}

View File

@@ -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 }));

View File

@@ -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<LibraryBook>().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<LibraryBook> deletedBooks = DbContexts.GetDeletedLibraryBooks();
foreach (var lb in deletedBooks)
deletedCbl.Items.Add(lb);
setLabel();

View File

@@ -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)

View File

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

View File

@@ -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 }));

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}");
}

View File

@@ -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($"""

View File

@@ -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;
}
}

View File

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

View File

@@ -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<LibraryBook>().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<LibraryBook>().ToList();
removeFromCheckList(removed);
await Task.Run(removed.RestoreBooks);
await removed.RestoreBooksAsync();
setControlsEnabled(true);
}

View File

@@ -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)

View File

@@ -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;