feat(metadata): rebuild book auto-fetch as library scopes with counts

Show the global rule and each library's own rule on Field Rules-style tabs, with live counts per
condition, the score histogram and the books a run would take. Store only what a library changes,
run every library with its own rule, and record each library's run. Also fix the DjVu queue test
that assumed which request waits, and inventory the DjVu reader routes.
This commit is contained in:
neon
2026-10-16 19:05:32 +00:00
parent c378354dac
commit 7d823039b0
46 changed files with 2660 additions and 1626 deletions
+57
View File
@@ -1,3 +1,4 @@
import type { CoverAspectRatio } from "./library";
import type { MetadataField } from "./metadata-preferences";
export interface BookMetadataFetchScoreCondition {
@@ -84,3 +85,59 @@ export interface BookMetadataFetchFailedPage {
page: number;
limit: number;
}
/** Every books library's override and run history, from `GET /book-metadata-fetch/config/libraries`. */
export interface BookMetadataFetchLibraryConfigEntry extends BookMetadataFetchLibraryConfig {
libraryId: number;
libraryName: string;
}
/**
* Body of `POST /book-metadata-fetch/preview-breakdown` and `POST /book-metadata-fetch/preview-books`.
* `libraryIds` omitted means every books library.
*/
export interface BookMetadataFetchPreviewScope {
conditions: BookMetadataFetchConditions;
libraryIds?: number[];
}
/** Width of each `scoreBins` bucket: [0-4], [5-9], ... [95-100]. */
export const BOOK_METADATA_FETCH_SCORE_BIN_WIDTH = 5;
/**
* How a rule lands on a scope. Each condition is counted on its own, whether or not it is switched on,
* so the settings page can say what turning one on would take. `matching` honours the switches and
* includes books that are already queued.
*/
export interface BookMetadataFetchBreakdown {
total: number;
matching: number;
neverFetched: number;
lowScore: number;
missingAny: number;
missingByField: Record<MetadataField, number>;
/** Books whose files include audio; only they can lack narrators, duration, abridged or an audiobook cover. */
withAudio: number;
/** Scored books per 5-point bucket, lowest first; 20 entries. */
scoreBins: number[];
unscored: number;
}
export interface BookMetadataFetchPreviewBook {
bookId: number;
libraryId: number;
title: string | null;
author: string | null;
metadataScore: number | null;
neverFetched: boolean;
/** The rule's selected fields this book lacks. */
missingFields: MetadataField[];
hasCover: boolean;
coverAspectRatio: CoverAspectRatio | null;
coverVersion: string | null;
}
export interface BookMetadataFetchPreviewBooks {
items: BookMetadataFetchPreviewBook[];
total: number;
}