mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 13:47:16 -04:00
A book the index does not hold cannot be found by any positive filter, and every negated filter - which resolves to every document in the index - drops it from the grid instead. That is why issue #1989 reads as half a working filter: Absent found nothing while -Absent removed exactly the absent books. The index is only ever written as a whole, and a rebuild that fails is deliberately swallowed so a bad index cannot fail a good scan, so a short index stayed short until something else changed the library. Count the index against the library once per run and rebuild when it is short. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
165 lines
6.5 KiB
C#
165 lines
6.5 KiB
C#
using Dinah.Core;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
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
|
|
{
|
|
private static System.Linq.Expressions.Expression<System.Func<LibraryBook, bool>> 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
|
|
//public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
|
// => context
|
|
// .Library
|
|
// .GetLibrary()
|
|
// .ToList();
|
|
|
|
public List<LibraryBook> 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, string? account = null)
|
|
{
|
|
var libraryQuery
|
|
= context
|
|
.LibraryBooks
|
|
.AsNoTrackingWithIdentityResolution()
|
|
.GetLibrary();
|
|
|
|
var matches = caseSensative
|
|
? libraryQuery.Where(lb => lb.Book.AudibleProductId == productId)
|
|
: libraryQuery.Where(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId);
|
|
|
|
if (account is not null)
|
|
matches = matches.Where(lb => lb.Account == account);
|
|
|
|
return matches.FirstOrDefault();
|
|
}
|
|
|
|
public List<LibraryBook> GetUnLiberated_Flat_NoTracking()
|
|
=> context
|
|
.LibraryBooks
|
|
.AsNoTrackingWithIdentityResolution()
|
|
.GetLibrary()
|
|
.Where(IsUnLiberatedExpression)
|
|
.AsEnumerable()
|
|
.ToList();
|
|
|
|
public List<LibraryBook> 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();
|
|
|
|
/// <summary>
|
|
/// How many books a full search re-index writes: the same rows as
|
|
/// <see cref="GetLibrary_Flat_NoTracking"/> without the podcast parents, which are stripped before
|
|
/// indexing. A row count only; no entities are loaded, so this is cheap enough to check the index against.
|
|
/// </summary>
|
|
public int GetIndexableBookCount()
|
|
=> context
|
|
.LibraryBooks
|
|
.AsNoTracking()
|
|
.Count(lb => !lb.IsDeleted && lb.Book.ContentType != ContentType.Parent);
|
|
|
|
/// <summary>Counts <see cref="LibraryBook"/> rows by <see cref="LibraryBook.IsDeleted"/> (no related entities loaded).</summary>
|
|
public (int NotInTrash, int InTrash) GetLibraryBookCountsByTrashFlag()
|
|
{
|
|
var q = context.LibraryBooks.AsNoTracking();
|
|
return (q.Count(lb => !lb.IsDeleted), q.Count(lb => lb.IsDeleted));
|
|
}
|
|
}
|
|
|
|
extension(IQueryable<LibraryBook> library)
|
|
{
|
|
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
|
public IQueryable<LibraryBook> GetLibrary()
|
|
=> library.Where(lb => !lb.IsDeleted).getLibrary();
|
|
|
|
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
|
private IQueryable<LibraryBook> 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);
|
|
}
|
|
|
|
extension(IEnumerable<LibraryBook> libraryBooks)
|
|
{
|
|
public IEnumerable<LibraryBook> UnLiberated()
|
|
=> libraryBooks.Where(lb => lb.NeedsPdfDownload || lb.NeedsBookDownload);
|
|
|
|
public IEnumerable<LibraryBook> ParentedEpisodes()
|
|
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren);
|
|
|
|
public IEnumerable<LibraryBook> FindOrphanedEpisodes()
|
|
=> libraryBooks
|
|
.Where(lb => lb.Book.IsEpisodeChild())
|
|
.ExceptBy(
|
|
libraryBooks
|
|
.ParentedEpisodes()
|
|
.Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId);
|
|
|
|
/// <summary>
|
|
/// Books displayed as top-level rows: ordinary products plus episodes whose series parent
|
|
/// is not in the database. Showing orphaned episodes here keeps a successfully imported
|
|
/// title discoverable instead of silently hiding it from both grids.
|
|
/// </summary>
|
|
public IEnumerable<LibraryBook> StandaloneBooks()
|
|
=> libraryBooks
|
|
.Where(lb => lb.Book.IsProduct())
|
|
.Concat(libraryBooks.FindOrphanedEpisodes());
|
|
|
|
public IEnumerable<LibraryBook> FindChildren(LibraryBook parent)
|
|
=> libraryBooks.Where(lb => lb.Book.IsEpisodeChild() && lb.HasSeriesId(parent.Book.AudibleProductId));
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|