diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index 5f3cdfbd..8ae52402 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -25,7 +25,7 @@ namespace ApplicationServices return context.GetLibrary_Flat_NoTracking(includeParents); } - public static List GetUnliberated_Flat_NoTracking(bool includeParents = false) + public static List GetUnliberated_Flat_NoTracking() { using var context = GetContext(); return context.GetUnLiberated_Flat_NoTracking(); diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 3dd69429..bf1cfc25 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -1,132 +1,131 @@ using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; +using Dinah.Core; using Microsoft.EntityFrameworkCore; #nullable enable -namespace DataLayer +namespace DataLayer; + +// only library importing should use tracking. All else should be NoTracking. +// only library importing should directly query Book. All else should use LibraryBook +public static class LibraryBookQueries { - // only library importing should use tracking. All else should be NoTracking. - // only library importing should directly query Book. All else should use LibraryBook - public static class LibraryBookQueries + private static System.Linq.Expressions.Expression> IsUnLiberatedExpression { get; } + = lb => + !lb.AbsentFromLastScan && + (lb.Book.ContentType == ContentType.Product || lb.Book.ContentType == ContentType.Episode) && + (lb.Book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.PartialDownload); + + extension(LibationContext context) { - //// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid + //// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid //public static List GetLibrary_Flat_WithTracking(this LibationContext context) // => context // .Library // .GetLibrary() // .ToList(); - public static List GetLibrary_Flat_NoTracking(this LibationContext context, bool includeParents = false) - => context - .LibraryBooks - .AsNoTrackingWithIdentityResolution() - .GetLibrary() - .AsEnumerable() - .Where(c => !c.Book.IsEpisodeParent() || includeParents) - .ToList(); - - public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId, bool caseSensative = true) - { - var libraryQuery - = context - .LibraryBooks - .AsNoTrackingWithIdentityResolution() - .GetLibrary(); - - return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId) - : libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId); - } - - public static List GetUnLiberated_Flat_NoTracking(this LibationContext context) + public List GetLibrary_Flat_NoTracking(bool includeParents = false) => context + .LibraryBooks + .AsNoTrackingWithIdentityResolution() + .GetLibrary() + .Where(lb => lb.Book.ContentType != ContentType.Parent || includeParents) + .AsEnumerable() + .ToList(); + + public LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true) + { + var libraryQuery + = context .LibraryBooks .AsNoTrackingWithIdentityResolution() - .GetLibrary() - .Where(IsUnLiberated()) - .AsEnumerable() - .ToList(); + .GetLibrary(); - private static Expression> IsUnLiberated() - => lb => - !lb.AbsentFromLastScan && - (lb.Book.ContentType == ContentType.Product || lb.Book.ContentType == ContentType.Episode) && - (lb.Book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.PartialDownload); + return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId) + : libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId); + } + + public List GetUnLiberated_Flat_NoTracking() + => context + .LibraryBooks + .AsNoTrackingWithIdentityResolution() + .GetLibrary() + .Where(IsUnLiberatedExpression) + .AsEnumerable() + .ToList(); + + public List GetDeletedLibraryBooks() + => context + .LibraryBooks + .AsNoTrackingWithIdentityResolution() + //Return all parents so the trash bin grid can show podcasts beneath their parents + .Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent) + .getLibrary() + .ToList(); + } + + extension(IQueryable library) + { + /// This is still IQueryable. YOU MUST CALL ToList() YOURSELF + public IQueryable GetLibrary() + => library.Where(lb => !lb.IsDeleted).getLibrary(); /// This is still IQueryable. YOU MUST CALL ToList() YOURSELF - public static IQueryable GetLibrary(this IQueryable library) - => library - .Where(lb => !lb.IsDeleted) - .getLibrary(); + private IQueryable getLibrary() + => library + // owned items are always loaded. eg: book.UserDefinedItem, book.Supplements + .Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series) + .Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor) + .Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories); + } - public static List GetDeletedLibraryBooks(this LibationContext context) - => context - .LibraryBooks - .AsNoTrackingWithIdentityResolution() - //Return all parents so the trash bin grid can show podcasts beneath their parents - .Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent) - .getLibrary() - .ToList(); + extension(IEnumerable libraryBooks) + { + public IEnumerable UnLiberated() + => libraryBooks.Where(lb => lb.NeedsPdfDownload || lb.NeedsBookDownload); - /// This is still IQueryable. YOU MUST CALL ToList() YOURSELF - private static IQueryable getLibrary(this IQueryable library) - => library - // owned items are always loaded. eg: book.UserDefinedItem, book.Supplements - .Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series) - .Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor) - .Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories); + public IEnumerable ParentedEpisodes() + => libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren); - public static IEnumerable ParentedEpisodes(this IEnumerable libraryBooks) - => libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren); + public IEnumerable FindOrphanedEpisodes() + => libraryBooks + .Where(lb => lb.Book.IsEpisodeChild()) + .ExceptBy( + libraryBooks + .ParentedEpisodes() + .Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId); - public static IEnumerable FindOrphanedEpisodes(this IEnumerable libraryBooks) - => libraryBooks - .Where(lb => lb.Book.IsEpisodeChild()) - .ExceptBy( - libraryBooks - .ParentedEpisodes() - .Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId); + public IEnumerable FindChildren(LibraryBook parent) + => libraryBooks.Where(lb => lb.Book.IsEpisodeChild() && lb.HasSeriesId(parent.Book.AudibleProductId)); -#nullable enable - public static LibraryBook? FindSeriesParent(this IEnumerable libraryBooks, LibraryBook seriesEpisode) - { - if (seriesEpisode.Book.SeriesLink is null) return null; + public LibraryBook? FindSeriesParent(LibraryBook seriesEpisode) + { + if (seriesEpisode.Book.SeriesLink is null) return null; - try - { - //Parent books will always have exactly 1 SeriesBook due to how - //they are imported in ApiExtended.getChildEpisodesAsync() - return libraryBooks.FirstOrDefault( - lb => - lb.Book.IsEpisodeParent() && - seriesEpisode.Book.SeriesLink.Any( - s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); - } - catch (System.Exception ex) - { - Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent)); - return null; - } - } -#nullable disable + try + { + //Parent books will always have exactly 1 SeriesBook due to how + //they are imported in ApiExtended.getChildEpisodesAsync() + return libraryBooks.FirstOrDefault( + lb => + lb.Book.IsEpisodeParent() && + seriesEpisode.Book.SeriesLink.Any( + s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); + } + catch (System.Exception ex) + { + Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent)); + return null; + } + } + } - public static List FindChildren(this IEnumerable bookList, LibraryBook parent) - => bookList - .Where( - lb => - lb.Book.IsEpisodeChild() && - lb.Book.SeriesLink? - .Any( - s => - s.Series.AudibleSeriesId == parent.Book.AudibleProductId - ) == true - ).ToList(); - - public static bool NeedsPdfDownload(this LibraryBook libraryBook) - => !libraryBook.AbsentFromLastScan && libraryBook.Book.ContentType is ContentType.Product or ContentType.Episode && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated; - public static bool NeedsBookDownload(this LibraryBook libraryBook) - => !libraryBook.AbsentFromLastScan && libraryBook.Book.ContentType is ContentType.Product or ContentType.Episode && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload; - public static IEnumerable UnLiberated(this IEnumerable bookList) - => bookList.Where(lb => (lb.NeedsPdfDownload() || lb.NeedsBookDownload())); - } + extension(LibraryBook libraryBook) + { + public bool HasSeriesId(string audibleSeriesId) => libraryBook.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId.EqualsInsensitive(audibleSeriesId)) is true; + public bool Downloadable => !libraryBook.AbsentFromLastScan && libraryBook.Book.ContentType is ContentType.Product or ContentType.Episode; + public bool NeedsPdfDownload => libraryBook.Downloadable && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated; + public bool NeedsBookDownload => libraryBook.Downloadable && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload; + } } diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs index 225d8116..e4fd683d 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs @@ -17,7 +17,7 @@ namespace LibationAvalonia.ViewModels /// This gets called by the "Begin Book and PDF Backups" menu item. public async Task BackupAllBooks() { - var books = await Task.Run(() => DbContexts.GetUnliberated_Flat_NoTracking()); + var books = await Task.Run(DbContexts.GetUnliberated_Flat_NoTracking); BackupAllBooks(books); } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 92115bad..202476f5 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -96,7 +96,7 @@ public class ProcessQueueViewModel : ReactiveObject if (!IsBooksDirectoryValid(config)) return false; - var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray(); + var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload).ToArray(); if (needsPdf.Length > 0) { Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length); @@ -137,14 +137,14 @@ public class ProcessQueueViewModel : ReactiveObject if (item.AbsentFromLastScan) return false; - else if (item.NeedsBookDownload()) + else if (item.NeedsBookDownload) { RemoveCompleted(item); Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item); AddDownloadDecrypt([item], config); return true; } - else if (item.NeedsPdfDownload()) + else if (item.NeedsPdfDownload) { RemoveCompleted(item); Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item); diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs index 50c5b1bf..a7a8efeb 100644 --- a/Source/LibationWinForms/Form1.Liberate.cs +++ b/Source/LibationWinForms/Form1.Liberate.cs @@ -15,7 +15,7 @@ namespace LibationWinForms //GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null) { - var library = await Task.Run(() => DbContexts.GetUnliberated_Flat_NoTracking()); + var library = await Task.Run(DbContexts.GetUnliberated_Flat_NoTracking); BackupAllBooks(library); }