fix(metadata): choose sources by medium and stop overwriting book data

Lead fetches with the primary file, give Book Dock its library's rules and read-along media,
default audiobook fields to sources that carry them, and flag providers that never fill a field.
Joining a format now fills gaps only and respects locks; a move picks its primary as a scan would.
This commit is contained in:
neon
2026-10-08 18:12:48 +00:00
parent 21b68685ad
commit c63f3ab757
53 changed files with 1420 additions and 177 deletions
+5
View File
@@ -99,6 +99,11 @@ export interface BookDockFile {
* single file, which is what `fileName` and `fileSize` already describe.
*/
unitFiles: BookDockUnitFile[];
/**
* Whether this EPUB carries synced narration. Only a single file's read that asks for it with
* `readAlong=true` answers it, since it means opening the file; every other response leaves it out.
*/
readAlong?: boolean;
}
export interface BookDockFilesPage {
+220
View File
@@ -166,3 +166,223 @@ export interface ProviderConnectionTestResult {
status: ProviderConnectionTestStatus;
message: string;
}
/**
* The field-rule fields each provider can ever fill, read from its mapper on 2026-10-08. The rule
* editor uses it to say a provider will never answer a field; nothing fetches by it, so an entry
* that drifts costs a wrong hint, never a value. A field filled only on some paths (Goodreads'
* publisher, which its fallback search lacks) counts as filled.
*
* The two cover rules list a provider only where its art can be that slot's shape. Audible's art is
* always square, so it is not under `cover`; it still fills an empty ebook slot as a last resort.
*/
export const METADATA_PROVIDER_SUPPORTED_FIELDS: Record<MetadataProviderKey, readonly MetadataField[]> = {
[MetadataProviderKey.GOOGLE]: [
"title",
"subtitle",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"genres",
],
[MetadataProviderKey.GOODREADS]: [
"title",
"subtitle",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.AMAZON]: [
"title",
"subtitle",
"description",
"cover",
"audioCover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.HARDCOVER]: [
"title",
"subtitle",
"description",
"cover",
"audioCover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.OPEN_LIBRARY]: [
"title",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"genres",
],
[MetadataProviderKey.ITUNES]: [
"title",
"description",
"cover",
"audioCover",
"authors",
"publisher",
"publishedYear",
"language",
"communityRating",
"genres",
],
[MetadataProviderKey.AUDIBLE]: [
"title",
"subtitle",
"description",
"audioCover",
"authors",
"publisher",
"publishedYear",
"language",
"communityRating",
"seriesName",
"seriesIndex",
"genres",
"narrators",
"duration",
"abridged",
],
[MetadataProviderKey.AUDNEXUS]: [
"title",
"subtitle",
"description",
"audioCover",
"authors",
"publisher",
"publishedYear",
"language",
"seriesName",
"seriesIndex",
"narrators",
"duration",
"abridged",
],
[MetadataProviderKey.LIBROFM]: [
"title",
"subtitle",
"description",
"audioCover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"seriesName",
"seriesIndex",
"genres",
"narrators",
"duration",
"abridged",
],
[MetadataProviderKey.COMICVINE]: ["title", "description", "cover", "authors", "publisher", "publishedYear", "seriesName", "seriesIndex"],
[MetadataProviderKey.RANOBEDB]: [
"title",
"subtitle",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"communityRating",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.KOBO]: [
"title",
"subtitle",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.LUBIMYCZYTAC]: [
"title",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"seriesName",
"seriesIndex",
"genres",
],
[MetadataProviderKey.ALADIN]: [
"title",
"description",
"cover",
"authors",
"publisher",
"isbn",
"publishedYear",
"language",
"pageCount",
"seriesName",
"genres",
],
};
/** How a provider stands against one field: it can fill it, its cover art is the other shape, or never. */
export type ProviderFieldSupport = "supplies" | "otherShape" | "never";
export function providerFieldSupport(provider: MetadataProviderKey, field: MetadataField): ProviderFieldSupport {
const fields = METADATA_PROVIDER_SUPPORTED_FIELDS[provider];
// An unknown key is stale data, and a hint about it would be a guess.
if (!fields || fields.includes(field)) return "supplies";
if (field === "cover" || field === "audioCover") return "otherShape";
return "never";
}