@prose-reader/metadata-fetcher 1.371.0 → 1.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/utils/metadataAuthors.ts","../src/utils/toIsbn13.ts","../src/match/similarity.ts","../src/match/scoreMetadataCandidate.ts","../src/providers/responseError.ts","../src/report.ts","../src/utils/omitUndefined.ts","../src/fetchMetadata.ts","../src/metadataInputFromResolvedArchive.ts","../src/utils/json.ts","../src/utils/retryWithBackoff.ts","../src/providers/googleBooks/identifier.ts","../src/providers/googleBooks/parse.ts","../src/utils/aggregateRating.ts","../src/providers/googleBooks/resolve.ts","../src/providers/googleBooks/search.ts","../src/providers/googleBooks/createGoogleBooksProvider.ts","../src/providers/projectGutenberg/identifier.ts","../src/providers/openLibrary/parse.ts","../src/providers/openLibrary/marcLanguage.ts","../src/providers/openLibrary/resolve.ts","../src/providers/openLibrary/createOpenLibraryProvider.ts","../src/providers/projectGutenberg/parse.ts","../src/providers/projectGutenberg/resolve.ts","../src/providers/projectGutenberg/createProjectGutenbergProvider.ts","../src/utils/hasSearchTerms.ts"],"sourcesContent":["import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\n\n/**\n * The names to match a publication on: those credited as `author`, else — for\n * a publication crediting nobody as one, like a comic archive listing only a\n * penciler — every contributor. Order is preserved (sources list the lead\n * creator first) and duplicates are dropped.\n */\nexport const metadataAuthors = (\n metadata: ResolvedMetadata,\n): ReadonlyArray<string> => {\n const contributors = metadata.contributors ?? []\n const authors = contributors.filter((contributor) =>\n contributor.roles.includes(\"author\"),\n )\n const names = (authors.length > 0 ? authors : contributors)\n .map((contributor) => contributor.name.trim())\n .filter((name) => name.length > 0)\n\n return [...new Set(names)]\n}\n","import { normalizeIsbn } from \"@prose-reader/archive-reader\"\n\nconst ISBN_13 = /^\\d{13}$/\nconst ISBN_10 = /^\\d{9}[\\dX]$/\n\n/**\n * Put an ISBN in its 13-digit form so the two printed on the same book compare\n * equal. A book carries an ISBN-10 in its OPF and an ISBN-13 in a catalog\n * record all the time; comparing them verbatim would read as \"different\n * books\", which — ISBN being a decisive match signal — is the most damaging\n * false negative the matcher can produce.\n *\n * Input is put through `normalizeIsbn`, the same normalization used for ISBN\n * identifiers, so the hyphenated and prefixed forms a human types\n * (`978-0-441-01359-3`, `ISBN 0-441-01359-7`) are recognized rather than\n * silently dropped.\n *\n * Returns `undefined` when nothing ISBN-shaped can be recovered (check digits\n * are not verified: a wrong one still identifies the intended book).\n */\nexport const toIsbn13 = (isbn: string): string | undefined => {\n const normalized = normalizeIsbn(isbn)\n\n if (normalized === undefined) return undefined\n if (ISBN_13.test(normalized)) return normalized\n if (!ISBN_10.test(normalized)) return undefined\n\n // ISBN-10 → ISBN-13: prefix the 978 Bookland code, drop the ISBN-10 check\n // digit, recompute the EAN-13 one (weights 1,3,1,3…).\n const core = `978${normalized.slice(0, 9)}`\n let sum = 0\n\n for (let index = 0; index < core.length; index++) {\n // charCodeAt over indexing: `noUncheckedIndexedAccess` types `core[i]` as\n // possibly undefined even though the length is known here\n const digit = core.charCodeAt(index) - 48\n\n sum += index % 2 === 0 ? digit : digit * 3\n }\n\n return `${core}${(10 - (sum % 10)) % 10}`\n}\n","const DIACRITICS = /[\\u0300-\\u036f]/g\nconst NON_ALPHANUMERIC = /[^\\p{L}\\p{N}]+/gu\n\n/** `Les Misérables` ≡ `les miserables`, `Dune: Messiah` ≡ `dune messiah`. */\nexport const normalizeForComparison = (value: string): string =>\n value\n .normalize(\"NFD\")\n .replace(DIACRITICS, \"\")\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \" \")\n .trim()\n\nconst bigramCounts = (value: string): Map<string, number> => {\n const counts = new Map<string, number>()\n\n for (let index = 0; index < value.length - 1; index++) {\n const bigram = value.slice(index, index + 2)\n\n counts.set(bigram, (counts.get(bigram) ?? 0) + 1)\n }\n\n return counts\n}\n\n/**\n * Sørensen–Dice coefficient over character bigrams, `0` to `1`.\n *\n * Bigrams rather than an edit distance: titles disagree by whole words far\n * more often than by typos. Spaces are stripped so `Spider-Man` and\n * `Spider Man` stay exact.\n */\nexport const textSimilarity = (a: string, b: string): number => {\n const left = normalizeForComparison(a).replaceAll(\" \", \"\")\n const right = normalizeForComparison(b).replaceAll(\" \", \"\")\n\n if (left.length === 0 || right.length === 0) return 0\n if (left === right) return 1\n // nothing to compare: a single character has no bigram\n if (left.length < 2 || right.length < 2) return 0\n\n const leftBigrams = bigramCounts(left)\n let shared = 0\n\n for (const [bigram, count] of bigramCounts(right)) {\n shared += Math.min(count, leftBigrams.get(bigram) ?? 0)\n }\n\n return (2 * shared) / (left.length - 1 + (right.length - 1))\n}\n\n/** Spaced dashes only, so `Spider-Man` stays one word. */\nconst SUBTITLE_SEPARATOR = /\\s*[:;]\\s*|\\s+[-–—]\\s+/\n\nconst splitTitle = (value: string): { main: string; hasSubtitle: boolean } => {\n const [head = \"\", ...rest] = value.split(SUBTITLE_SEPARATOR)\n const main = head.trim()\n const subtitle = rest.join(\" \").trim()\n\n if (main.length === 0) return { main: value, hasSubtitle: false }\n\n return { main, hasSubtitle: subtitle.length > 0 }\n}\n\n/** Below an exact match: the omitted half could have been a volume title. */\nconst SUBTITLE_REPAIR_CEILING = 0.9\n\ntype TitleDivision = \"volume\" | \"part\" | \"book\"\nconst TITLE_DIVISIONS: ReadonlyArray<TitleDivision> = [\"volume\", \"part\", \"book\"]\n\nconst TITLE_DIVISION_PATTERN =\n /\\b(volumes?|vols?|parts?|books?)\\s+([\\p{L}\\p{N}]+)\\b/gu\n\nconst TITLE_NUMBER_WORDS: Readonly<Record<string, number>> = {\n one: 1,\n first: 1,\n two: 2,\n second: 2,\n three: 3,\n third: 3,\n four: 4,\n fourth: 4,\n five: 5,\n fifth: 5,\n six: 6,\n sixth: 6,\n seven: 7,\n seventh: 7,\n eight: 8,\n eighth: 8,\n nine: 9,\n ninth: 9,\n ten: 10,\n tenth: 10,\n eleven: 11,\n eleventh: 11,\n twelve: 12,\n twelfth: 12,\n}\n\nconst ROMAN_DIGIT_VALUES: Readonly<Record<string, number>> = {\n i: 1,\n v: 5,\n x: 10,\n l: 50,\n c: 100,\n d: 500,\n m: 1000,\n}\n\nconst parseRomanNumeral = (value: string): number | undefined => {\n if (!/^m{0,3}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$/i.test(value))\n return undefined\n\n let total = 0\n let previous = 0\n\n for (const symbol of [...value.toLowerCase()].reverse()) {\n const current = ROMAN_DIGIT_VALUES[symbol]\n\n if (current === undefined) return undefined\n\n if (current < previous) total -= current\n else {\n total += current\n previous = current\n }\n }\n\n return total > 0 ? total : undefined\n}\n\nconst parseTitleDivisionNumber = (value: string): number | undefined => {\n if (/^\\d+$/.test(value)) return Number.parseInt(value, 10)\n\n return TITLE_NUMBER_WORDS[value] ?? parseRomanNumeral(value)\n}\n\nconst titleDivision = (value: string): TitleDivision | undefined => {\n if (value.startsWith(\"vol\")) return \"volume\"\n if (value.startsWith(\"part\")) return \"part\"\n if (value.startsWith(\"book\")) return \"book\"\n\n return undefined\n}\n\nconst explicitTitleDivisions = (\n value: string,\n): ReadonlyMap<TitleDivision, ReadonlySet<number>> => {\n const divisions = new Map<TitleDivision, Set<number>>()\n\n for (const match of normalizeForComparison(value).matchAll(\n TITLE_DIVISION_PATTERN,\n )) {\n const division =\n match[1] !== undefined ? titleDivision(match[1]) : undefined\n const number =\n match[2] !== undefined ? parseTitleDivisionNumber(match[2]) : undefined\n\n if (division === undefined || number === undefined) continue\n\n const numbers = divisions.get(division) ?? new Set<number>()\n numbers.add(number)\n divisions.set(division, numbers)\n }\n\n return divisions\n}\n\nconst titleDivisionsContradict = (a: string, b: string): boolean => {\n const left = explicitTitleDivisions(a)\n const right = explicitTitleDivisions(b)\n\n for (const division of TITLE_DIVISIONS) {\n const leftNumbers = left.get(division)\n const rightNumbers = right.get(division)\n\n if (leftNumbers === undefined || rightNumbers === undefined) continue\n if ([...leftNumbers].every((number) => !rightNumbers.has(number)))\n return true\n }\n\n return false\n}\n\n/**\n * Every volume, part or book number a title states explicitly, whichever\n * division word introduced it — a catalog states the same fact as a series\n * `position`, where the division word is implicit.\n */\nexport const titleDivisionNumbers = (value: string): ReadonlySet<number> => {\n const numbers = new Set<number>()\n\n for (const division of explicitTitleDivisions(value).values()) {\n for (const number of division) numbers.add(number)\n }\n\n return numbers\n}\n\n/**\n * {@link textSimilarity}, with `Dune` matched against `Dune: a novel`.\n *\n * Only when **one** side states a subtitle: that asymmetry is the same title\n * catalogued at a different depth. When both state one, both catalogers meant\n * it — `Dune: Book One` and `Dune: Messiah` are two books.\n * Explicit volume, part, or book numbers are semantic identity: when both\n * sides state different ones, the title score is `0` however similar the rest.\n */\nexport const titleSimilarity = (a: string, b: string): number => {\n if (titleDivisionsContradict(a, b)) return 0\n\n const full = textSimilarity(a, b)\n const left = splitTitle(a)\n const right = splitTitle(b)\n\n if (left.hasSubtitle === right.hasSubtitle) return full\n\n return Math.max(\n full,\n textSimilarity(left.main, right.main) * SUBTITLE_REPAIR_CEILING,\n )\n}\n\n/**\n * The best {@link titleSimilarity} over the forms one candidate title takes —\n * bare, or composed with its subtitle.\n *\n * A contradiction in **any** form still scores `0`: `Irina` agreeing with\n * `Irina: … Vol. 2` is not evidence that the candidate's own `Vol. 1` does.\n * Taking the plain maximum would let the shorter form launder a stated\n * disagreement about which volume this is.\n */\nexport const bestTitleSimilarity = (\n query: string,\n forms: ReadonlyArray<string>,\n): { readonly score: number; readonly candidate: string } | undefined => {\n let best: { score: number; candidate: string } | undefined\n\n for (const form of forms) {\n if (titleDivisionsContradict(query, form))\n return { score: 0, candidate: form }\n\n const score = titleSimilarity(query, form)\n\n if (best === undefined || score > best.score)\n best = { score, candidate: form }\n }\n\n return best\n}\n\nconst sortedTokens = (value: string): string =>\n normalizeForComparison(value)\n .split(\" \")\n .filter((token) => token.length > 0)\n .sort()\n .join(\" \")\n\n/** {@link textSimilarity} on sorted tokens: `Herbert, Frank` ≡ `Frank Herbert`. */\nexport const personNameSimilarity = (a: string, b: string): number =>\n textSimilarity(sortedTokens(a), sortedTokens(b))\n","import {\n identifierValue,\n isIsbnBearingScheme,\n type MetadataIdentifier,\n mainTitle,\n type ResolvedMetadata,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../types/fetchMetadataInput.ts\"\nimport type { MetadataMatchField, MetadataMatchSignal } from \"../types/match.ts\"\nimport { metadataAuthors } from \"../utils/metadataAuthors.ts\"\nimport { toIsbn13 } from \"../utils/toIsbn13.ts\"\nimport {\n bestTitleSimilarity,\n personNameSimilarity,\n textSimilarity,\n titleDivisionNumbers,\n} from \"./similarity.ts\"\n\n/**\n * Intent, not statistics: identifiers *are* the book, a title is what a human\n * recognizes it by, everything else corroborates. An edition detail that\n * differs must never sink an otherwise convincing match on its own.\n */\nexport const METADATA_MATCH_WEIGHTS = {\n isbn: 1,\n gtin: 1,\n identifiers: 0.6,\n title: 0.8,\n authors: 0.5,\n series: 0.3,\n publishedYear: 0.2,\n publisher: 0.15,\n languages: 0.15,\n numberOfPages: 0.15,\n} as const satisfies Record<MetadataMatchField, number>\n\n/**\n * Identity rather than description, so these settle the score outright in both\n * directions. Catalogs and books disagree on titles and page counts\n * constantly; they do not agree or disagree on an ISBN by accident.\n */\nconst DECISIVE_FIELDS: ReadonlySet<MetadataMatchField> = new Set([\n \"isbn\",\n \"gtin\",\n])\n\n/** Blank is absent — the vocabulary says so, a hand-built query may not. */\nconst stated = (value: string | undefined): string | undefined => {\n const trimmed = value?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst identifiersHaveSameValue = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n const aValue = stated(a.value)?.toLowerCase()\n\n return aValue !== undefined && aValue === stated(b.value)?.toLowerCase()\n}\n\nconst identifierScheme = (\n identifier: MetadataIdentifier,\n): string | undefined =>\n identifier.scheme.trim().toLowerCase() === \"unknown\"\n ? undefined\n : stated(identifier.scheme)?.toLowerCase()\n\nconst identifierLabel = (identifier: MetadataIdentifier): string =>\n `${identifier.scheme}:${identifier.value}`\n\n/**\n * An `Unknown` scheme is not a contradiction; a `DOI` and an `ISBN` carrying\n * the same digits are.\n */\nconst identifiersAgree = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (!identifiersHaveSameValue(a, b)) return false\n const aScheme = identifierScheme(a)\n const bScheme = identifierScheme(b)\n\n if (aScheme === undefined || bScheme === undefined) return true\n\n return aScheme === bScheme\n}\n\n/** Only a known or consumer-authored scope makes a value unambiguous identity. */\nconst identifiersAgreeDecisively = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (!identifiersHaveSameValue(a, b)) return false\n const aScheme = identifierScheme(a)\n const bScheme = identifierScheme(b)\n\n return aScheme !== undefined && aScheme === bScheme\n}\n\nconst primaryLanguageSubtag = (language: string): string =>\n language.trim().toLowerCase().split(\"-\")[0] ?? \"\"\n\n/** Reprints shift the year around, so a near one still corroborates. */\nconst publicationYearScore = (\n queryYear: number,\n candidateYear: number,\n): number => {\n const distance = Math.abs(queryYear - candidateYear)\n\n if (distance === 0) return 1\n if (distance === 1) return 0.6\n if (distance <= 3) return 0.3\n\n return 0\n}\n\n/**\n * Proximity, not equality — front matter and ads move the count: identical is\n * `1`, 25% apart `0.5`, half as long `0`.\n */\nconst numberOfPagesScore = (query: number, candidate: number): number => {\n const largest = Math.max(query, candidate)\n\n if (largest <= 0) return 0\n\n return Math.max(0, 1 - (Math.abs(query - candidate) / largest) * 2)\n}\n\nconst bestSimilarity = (\n values: ReadonlyArray<string>,\n candidates: ReadonlyArray<string>,\n compare: (a: string, b: string) => number,\n): number =>\n values.reduce(\n (best, value) =>\n candidates.reduce(\n (innerBest, candidate) =>\n Math.max(innerBest, compare(value, candidate)),\n best,\n ),\n 0,\n )\n\ntype ComparedValues = {\n readonly score: number\n readonly query: string\n readonly candidate: string\n}\n\nconst bestStringComparison = (\n rawQueryValue: string | undefined,\n rawCandidateValues: ReadonlyArray<string | undefined>,\n compare: (a: string, b: string) => number = textSimilarity,\n): ComparedValues | undefined => {\n const query = stated(rawQueryValue)\n\n if (query === undefined) return undefined\n\n let best: ComparedValues | undefined\n\n for (const rawCandidate of rawCandidateValues) {\n const candidate = stated(rawCandidate)\n\n if (candidate === undefined) continue\n\n const comparison = { score: compare(query, candidate), query, candidate }\n\n if (best === undefined || comparison.score > best.score) best = comparison\n }\n\n return best\n}\n\n/**\n * A comparison string, not a value: a book states `Dune: A Novel` in one\n * `dc:title` where a catalog states a title and a subtitle, and the query\n * side has whichever spelling its source used. Composing the two for the\n * comparison alone keeps that asymmetry from costing score, while the\n * candidate's own metadata stays exactly as the catalog stated it.\n */\nconst subtitledTitle = (candidate: ResolvedMetadata): string | undefined => {\n const main = mainTitle(candidate)\n const subtitle = candidate.titles?.find(\n (title) => title.type === \"subtitle\",\n )?.value\n\n return main !== undefined && subtitle !== undefined\n ? `${main}: ${subtitle}`\n : undefined\n}\n\n/**\n * A book says `BLAME! Vol. 2` in its title where a catalog says `BLAME!` with\n * a series `position` of 2 — the same fact in two shapes. Comparing the\n * shapes against each other keeps a numbered query from accepting the wrong\n * volume of the right series, which the title comparison alone cannot see\n * once the number lives in structured data instead of the title string.\n */\nconst seriesPositionContradicts = (\n queryTitle: string,\n candidate: ResolvedMetadata,\n): boolean => {\n const queryNumbers = titleDivisionNumbers(queryTitle)\n\n if (queryNumbers.size === 0) return false\n\n const positions = (candidate.belongsTo?.series ?? []).flatMap((series) =>\n series.position !== undefined ? [series.position] : [],\n )\n\n return (\n positions.length > 0 &&\n positions.every((position) => !queryNumbers.has(position))\n )\n}\n\nconst bestPublicationYearComparison = (\n query: number | undefined,\n candidates: ReadonlyArray<number | undefined>,\n): ComparedValues | undefined => {\n if (query === undefined) return undefined\n\n let best: ComparedValues | undefined\n\n for (const candidate of candidates) {\n if (candidate === undefined) continue\n\n const comparison = {\n score: publicationYearScore(query, candidate),\n query: String(query),\n candidate: String(candidate),\n }\n\n if (best === undefined || comparison.score > best.score) best = comparison\n }\n\n return best\n}\n\nexport type ScoredMetadataCandidate = {\n readonly score: number\n readonly signals: ReadonlyArray<MetadataMatchSignal>\n}\n\n/**\n * How well a candidate agrees with the query, field by field, and why. Every\n * provider is scored by these rules, so their scores stay comparable:\n *\n * - **Only comparable fields count** — a field is compared when both sides\n * state it, so what the book doesn't know can neither raise nor lower a\n * candidate, and nothing in common scores `0`.\n * - **The aggregate is a weighted average** ({@link METADATA_MATCH_WEIGHTS}),\n * putting a rich query and a sparse one on the same scale.\n * - **Confirmed identity settles it** — an agreeing ISBN, GTIN, or\n * scheme-scoped identifier pins the score to `1`. A contradictory ISBN or\n * GTIN pins it to `0`; two disjoint provider-specific identifier lists are\n * merely unrelated.\n */\nexport const scoreMetadataCandidate = (\n query: FetchMetadataInput,\n candidate: ResolvedMetadata,\n): ScoredMetadataCandidate => {\n const signals: MetadataMatchSignal[] = []\n\n const addSignal = (\n field: MetadataMatchField,\n values: ComparedValues | undefined,\n ) => {\n if (values === undefined) return\n\n signals.push({\n field,\n weight: METADATA_MATCH_WEIGHTS[field],\n score: Math.min(1, Math.max(0, values.score)),\n query: values.query,\n candidate: values.candidate,\n })\n }\n\n const compareStrings = (\n field: MetadataMatchField,\n rawQueryValue: string | undefined,\n rawCandidateValue: string | undefined,\n compare: (a: string, b: string) => number = textSimilarity,\n ) => {\n const queryValue = stated(rawQueryValue)\n const candidateValue = stated(rawCandidateValue)\n\n if (queryValue === undefined || candidateValue === undefined) return\n\n addSignal(field, {\n score: compare(queryValue, candidateValue),\n query: queryValue,\n candidate: candidateValue,\n })\n }\n\n const queryIsbn = identifierValue(query.identifiers, \"ISBN\")\n const candidateIsbn = identifierValue(candidate.identifiers, \"ISBN\")\n const queryIsbn13 = queryIsbn !== undefined ? toIsbn13(queryIsbn) : undefined\n const candidateIsbn13 =\n candidateIsbn !== undefined ? toIsbn13(candidateIsbn) : undefined\n\n const comparedIsbn =\n queryIsbn !== undefined &&\n candidateIsbn !== undefined &&\n queryIsbn13 !== undefined &&\n candidateIsbn13 !== undefined\n\n if (comparedIsbn) {\n addSignal(\"isbn\", {\n score: queryIsbn13 === candidateIsbn13 ? 1 : 0,\n query: queryIsbn,\n candidate: candidateIsbn,\n })\n }\n\n const queryGtin = identifierValue(query.identifiers, \"GTIN\")\n const candidateGtin = identifierValue(candidate.identifiers, \"GTIN\")\n\n if (!comparedIsbn && queryGtin !== undefined && candidateGtin !== undefined) {\n addSignal(\"gtin\", {\n score: queryGtin === candidateGtin ? 1 : 0,\n query: queryGtin,\n candidate: candidateGtin,\n })\n }\n\n // ISBN and GTIN have normalized comparisons above. Comparing their raw\n // spellings again would emit a false generic mismatch for hyphenated ISBNs.\n const queryIdentifiers = (query.identifiers ?? []).filter(\n (identifier) => !isIsbnBearingScheme(identifier.scheme),\n )\n const candidateIdentifiers = (candidate.identifiers ?? []).filter(\n (identifier) => !isIsbnBearingScheme(identifier.scheme),\n )\n const decisiveIdentifierAgreement = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) =>\n identifiersAgreeDecisively(identifier, other),\n ),\n )\n\n if (queryIdentifiers.length > 0 && candidateIdentifiers.length > 0) {\n const shared = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) => identifiersAgree(identifier, other)),\n )\n\n addSignal(\"identifiers\", {\n score: shared ? 1 : 0,\n query: queryIdentifiers.map(identifierLabel).join(\", \"),\n candidate: candidateIdentifiers.map(identifierLabel).join(\", \"),\n })\n }\n\n const queryTitle = stated(query.title)\n const candidateTitleForms = [\n mainTitle(candidate),\n subtitledTitle(candidate),\n ].flatMap((form) => {\n const value = stated(form)\n\n return value !== undefined ? [value] : []\n })\n\n if (queryTitle !== undefined && candidateTitleForms.length > 0) {\n const compared = bestTitleSimilarity(queryTitle, candidateTitleForms)\n\n if (compared !== undefined) {\n addSignal(\"title\", {\n score: seriesPositionContradicts(queryTitle, candidate)\n ? 0\n : compared.score,\n query: queryTitle,\n candidate: compared.candidate,\n })\n }\n }\n addSignal(\n \"publisher\",\n bestStringComparison(query.publisher, [\n candidate.publication?.original?.publisher,\n candidate.publication?.edition?.publisher,\n ]),\n )\n compareStrings(\"series\", query.series, candidate.belongsTo?.series?.[0]?.name)\n\n const candidateAuthors = metadataAuthors(candidate)\n const queryAuthors = (query.authors ?? []).flatMap((author) => {\n const name = stated(author)\n\n return name !== undefined ? [name] : []\n })\n\n if (queryAuthors.length > 0 && candidateAuthors.length > 0) {\n addSignal(\"authors\", {\n score: bestSimilarity(\n queryAuthors,\n candidateAuthors,\n personNameSimilarity,\n ),\n query: queryAuthors.join(\", \"),\n candidate: candidateAuthors.join(\", \"),\n })\n }\n\n addSignal(\n \"publishedYear\",\n bestPublicationYearComparison(query.publishedYear, [\n candidate.publication?.original?.date?.year,\n candidate.publication?.edition?.date?.year,\n ]),\n )\n\n const queryLanguages = query.languages ?? []\n const candidateLanguages = candidate.languages ?? []\n\n if (queryLanguages.length > 0 && candidateLanguages.length > 0) {\n const candidateSubtags = new Set(\n candidateLanguages.map(primaryLanguageSubtag),\n )\n\n addSignal(\"languages\", {\n score: queryLanguages.some((language) =>\n candidateSubtags.has(primaryLanguageSubtag(language)),\n )\n ? 1\n : 0,\n query: queryLanguages.join(\", \"),\n candidate: candidateLanguages.join(\", \"),\n })\n }\n\n if (\n query.numberOfPages !== undefined &&\n candidate.numberOfPages !== undefined\n ) {\n addSignal(\"numberOfPages\", {\n score: numberOfPagesScore(query.numberOfPages, candidate.numberOfPages),\n query: String(query.numberOfPages),\n candidate: String(candidate.numberOfPages),\n })\n }\n\n const decisive = signals.filter((signal) => DECISIVE_FIELDS.has(signal.field))\n\n // Contradiction wins over agreement: refusing a publication whose stated\n // identity disagrees is the recoverable mistake. Without this the average is\n // merciful to exactly the wrong candidate — a different edition agreeing on\n // title and author scores (0.8 + 0.5) / 2.3 ≈ 0.57, accepted.\n if (decisive.some((signal) => signal.score === 0))\n return { score: 0, signals }\n if (\n decisiveIdentifierAgreement ||\n decisive.some((signal) => signal.score === 1)\n )\n return { score: 1, signals }\n\n const totalWeight = signals.reduce(\n (total, signal) => total + signal.weight,\n 0,\n )\n\n if (totalWeight === 0) return { score: 0, signals }\n\n const weighted = signals.reduce(\n (total, signal) => total + signal.score * signal.weight,\n 0,\n )\n\n return { score: weighted / totalWeight, signals }\n}\n","/**\n * Thrown by a provider when a catalog answered with a failing status. Carrying\n * it is what lets `fetchMetadata` report *why* a provider dropped out: `429`\n * is \"ask again later\", `503` is \"the catalog is down\", and a consumer decides\n * differently for each.\n *\n * ```ts\n * if (!response.ok) {\n * throw new MetadataProviderResponseError(response.status, \"My Catalog search failed\")\n * }\n * ```\n */\nexport class MetadataProviderResponseError extends Error {\n readonly status: number\n\n constructor(status: number, message?: string) {\n super(message ?? `Provider responded with status ${status}`)\n\n this.name = \"MetadataProviderResponseError\"\n this.status = status\n }\n}\n\n/**\n * The HTTP status behind a thrown error, when there is one.\n *\n * Structural rather than `instanceof`, so a provider built on another HTTP\n * client works unadapted. No status means it was never a response — network,\n * parse, bug — which is itself the answer.\n */\nexport const responseErrorStatus = (error: unknown): number | undefined => {\n if (typeof error !== \"object\" || error === null) return undefined\n if (!(\"status\" in error)) return undefined\n\n const { status } = error\n\n if (typeof status !== \"number\" || !Number.isInteger(status)) return undefined\n\n // an unrelated `status` field means something else entirely\n return status >= 100 && status <= 599 ? status : undefined\n}\n","import { Report as SharedReport } from \"@prose-reader/shared\"\n\n// Spelled out rather than read from package.json: node runs this source\n// directly in development, where a JSON import needs an import attribute.\nconst NAME = \"@prose-reader/metadata-fetcher\"\n\n// No explicit `enabled`: inherit the shared root, which auto-configures from\n// the `globalThis.__PROSE_READER_DEBUG` flag.\nexport const Report = SharedReport.namespace(NAME, undefined, {\n color: \"#b06642\",\n})\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * results stay sparse (an absent key and an `undefined` value are the same\n * statement: \"nothing was found for this field\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","import { scoreMetadataCandidate } from \"./match/scoreMetadataCandidate.ts\"\nimport { responseErrorStatus } from \"./providers/responseError.ts\"\nimport { Report } from \"./report.ts\"\nimport type {\n FailedMetadataProvider,\n FetchedMetadata,\n FetchedMetadataSource,\n} from \"./types/fetchedMetadata.ts\"\nimport type { FetchMetadataInput } from \"./types/fetchMetadataInput.ts\"\nimport type { MetadataMatch } from \"./types/match.ts\"\nimport type { MetadataCandidate, MetadataProvider } from \"./types/provider.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\nexport type FetchMetadataOptions = {\n /**\n * The catalogs to ask, queried concurrently. Their order is the tie-break\n * precedence: equally-scored matches rank in the order declared here.\n */\n readonly providers: ReadonlyArray<MetadataProvider>\n /**\n * Hard cap on the matches kept **per provider**, best first; also the\n * page-size hint each provider receives. Defaults to `5`.\n */\n readonly limit?: number\n /**\n * Score a match must reach to be `accepted`. Defaults to `0.5`. Below it,\n * matches are still returned and ranked, for a consumer to offer as \"did\n * you mean?\".\n */\n readonly minScore?: number\n /**\n * Keep each provider's own record on `match.raw`. Defaults to `false`: it is\n * provenance most consumers don't need, and can dwarf what they persist.\n */\n readonly includeRaw?: boolean\n readonly signal?: AbortSignal\n}\n\nconst DEFAULT_LIMIT = 5\nconst DEFAULT_MIN_SCORE = 0.5\nconst FETCHED_METADATA_VERSION = 5\n\nconst toMatch = ({\n candidate,\n providerId,\n score,\n signals,\n accepted,\n includeRaw,\n}: {\n candidate: MetadataCandidate\n providerId: string\n score: number\n signals: MetadataMatch[\"signals\"]\n accepted: boolean\n includeRaw: boolean\n}): MetadataMatch =>\n omitUndefined({\n providerId,\n score,\n signals,\n accepted,\n metadata: candidate.metadata,\n id: candidate.id,\n url: candidate.url,\n raw: includeRaw ? candidate.raw : undefined,\n })\n\n/**\n * Asks every provider what it knows about a book, scores the answers against\n * what you gave it, and returns the ranked candidates with the evidence behind\n * each score. The input contains only fields understood for lookup and\n * matching; candidates retain archive-reader's rich `ResolvedMetadata`\n * vocabulary, including catalog-only covers, descriptions and subjects.\n *\n * ```ts\n * import { resolveArchive } from \"@prose-reader/archive-reader\"\n * import {\n * createGoogleBooksProvider,\n * createOpenLibraryProvider,\n * createProjectGutenbergProvider,\n * fetchMetadata,\n * metadataInputFromResolvedArchive,\n * } from \"@prose-reader/metadata-fetcher\"\n *\n * const resolved = await resolveArchive(archive)\n * const fetched = await fetchMetadata(metadataInputFromResolvedArchive(resolved), {\n * providers: [\n * createProjectGutenbergProvider(),\n * createGoogleBooksProvider({ apiKey: \"your-api-key\" }),\n * createOpenLibraryProvider(),\n * ],\n * })\n *\n * // what the best accepted candidate says, and why we believe it\n * const match = fetched.matches.find(({ accepted }) => accepted)\n * match?.metadata.cover?.uri\n * match?.signals // [{ field: \"isbn\", score: 1, … }, …]\n * ```\n *\n * Error policy mirrors `resolveArchive`: a provider that throws never fails\n * the fetch — it is logged through the debug `Report` and listed in\n * `failedProviders`. An abort is the exception: cancelling through `signal`\n * rejects, since the caller asked for it.\n */\nexport const fetchMetadata = async (\n input: FetchMetadataInput,\n options: FetchMetadataOptions,\n): Promise<FetchedMetadata> => {\n const limit = options.limit ?? DEFAULT_LIMIT\n const minScore = options.minScore ?? DEFAULT_MIN_SCORE\n const includeRaw = options.includeRaw === true\n\n const results = await Promise.all(\n options.providers.map(async (provider) => {\n try {\n const candidates = await provider.search(input, {\n limit,\n signal: options.signal,\n })\n\n return { provider, candidates, failure: undefined }\n } catch (error) {\n // the caller pulled the plug: their cancellation, their rejection\n if (options.signal?.aborted === true) throw error\n\n // catalogs in the wild are flaky: a failing provider never fails the\n // fetch, it just doesn't contribute. The status, when the catalog\n // answered with one, is what tells a rate limit from an outage.\n Report.error(`fetchMetadata: provider \"${provider.id}\" failed`, error)\n\n return {\n provider,\n candidates: undefined,\n failure: omitUndefined({\n id: provider.id,\n status: responseErrorStatus(error),\n }),\n }\n }\n }),\n )\n\n const failedProviders: FailedMetadataProvider[] = results.flatMap((result) =>\n result.failure !== undefined ? [result.failure] : [],\n )\n\n const sources: Record<string, FetchedMetadataSource> = {}\n const matches: MetadataMatch[] = []\n\n for (const { provider, candidates } of results) {\n if (candidates === undefined) continue\n\n const providerMatches = candidates\n .map((candidate) => {\n const { score, signals } = scoreMetadataCandidate(\n input,\n candidate.metadata,\n )\n\n return toMatch({\n candidate,\n providerId: provider.id,\n score,\n signals,\n accepted: score >= minScore,\n includeRaw,\n })\n })\n // `sort` is stable, so equally-scored candidates keep the order the\n // provider ranked them in\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n\n // ids key `sources`, so a duplicate would silently drop a provider's\n // detail — the kind of thing that costs an hour to notice\n if (sources[provider.id] !== undefined) {\n Report.warn(\n `fetchMetadata: several providers share the id \"${provider.id}\"`,\n )\n }\n\n sources[provider.id] = {\n provider: { id: provider.id, name: provider.name },\n matches: providerMatches,\n }\n\n matches.push(...providerMatches)\n }\n\n // again stable: providers were walked in declaration order, so that order\n // breaks ties between equally convincing catalogs\n const rankedMatches = [...matches].sort((a, b) => b.score - a.score)\n\n return {\n version: FETCHED_METADATA_VERSION,\n matches: rankedMatches,\n sources,\n failedProviders,\n }\n}\n","import { mainTitle, type ResolvedArchive } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"./types/fetchMetadataInput.ts\"\nimport { metadataAuthors } from \"./utils/metadataAuthors.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * Projects archive-reader's rich metadata entity into the fields understood by\n * metadata lookup and matching. Edition publication details take precedence\n * because embedded publication metadata normally describes that concrete\n * edition; original-publication details are the fallback.\n */\nexport const metadataInputFromResolvedArchive = (\n resolved: Pick<ResolvedArchive, \"metadata\">,\n): FetchMetadataInput => {\n const { metadata } = resolved\n const authors = metadataAuthors(metadata)\n const identifiers = metadata.identifiers?.map(({ value, scheme }) => ({\n value,\n scheme,\n }))\n\n return omitUndefined({\n title: mainTitle(metadata),\n authors: authors.length > 0 ? authors : undefined,\n identifiers:\n identifiers !== undefined && identifiers.length > 0\n ? identifiers\n : undefined,\n series: metadata.belongsTo?.series?.[0]?.name,\n publisher:\n metadata.publication?.edition?.publisher ??\n metadata.publication?.original?.publisher,\n publishedYear:\n metadata.publication?.edition?.date?.year ??\n metadata.publication?.original?.date?.year,\n languages: metadata.languages,\n numberOfPages: metadata.numberOfPages,\n })\n}\n","/**\n * Readers for `JSON.parse` output. A catalog's payload is third-party data, so\n * it enters as `unknown` and every field goes through a guard: a renamed or\n * nulled field reads as absent instead of crashing the fetch, or producing a\n * candidate with `undefined` where a string was promised.\n */\nexport const isJsonRecord = (\n value: unknown,\n): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nexport const readString = (\n record: Record<string, unknown>,\n key: string,\n): string | undefined => {\n const value = record[key]\n\n if (typeof value !== \"string\") return undefined\n\n const trimmed = value.trim()\n\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport const readNumber = (\n record: Record<string, unknown>,\n key: string,\n): number | undefined => {\n const value = record[key]\n\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined\n}\n\nexport const readStringArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<string> | undefined => {\n const value = record[key]\n\n if (!Array.isArray(value)) return undefined\n\n const strings = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n\n return strings.length > 0 ? strings : undefined\n}\n\nexport const readRecordArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<Record<string, unknown>> => {\n const value = record[key]\n\n if (!Array.isArray(value)) return []\n\n return value.filter(isJsonRecord)\n}\n","export type RetryWithBackoffOptions = {\n /** Total calls, including the initial one. Must be a positive integer. */\n readonly attempts: number\n /** Delay before the second call. Later delays double. */\n readonly initialDelayMs: number\n /** Upper bound for one delay, including jitter. */\n readonly maxDelayMs?: number\n /** Adds up to one base delay of randomness. Defaults to `true`. */\n readonly jitter?: boolean\n /** Decides whether a failed call is safe and useful to repeat. */\n readonly shouldRetry: (error: unknown, attempt: number) => boolean\n /** Stops future attempts and cancels a wait between attempts. */\n readonly signal?: AbortSignal\n}\n\nconst abortReason = (signal: AbortSignal): unknown =>\n signal.reason ?? new DOMException(\"The operation was aborted\", \"AbortError\")\n\nconst throwIfAborted = (signal: AbortSignal | undefined): void => {\n if (signal?.aborted === true) throw abortReason(signal)\n}\n\nconst wait = (\n delayMs: number,\n signal: AbortSignal | undefined,\n): Promise<void> => {\n throwIfAborted(signal)\n\n return new Promise((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timeout)\n reject(signal !== undefined ? abortReason(signal) : undefined)\n }\n const timeout = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort)\n resolve()\n }, delayMs)\n\n signal?.addEventListener(\"abort\", onAbort, { once: true })\n })\n}\n\nconst validateOptions = (options: RetryWithBackoffOptions): void => {\n if (!Number.isInteger(options.attempts) || options.attempts < 1) {\n throw new RangeError(\"attempts must be a positive integer\")\n }\n\n if (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs < 0) {\n throw new RangeError(\"initialDelayMs must be a non-negative finite number\")\n }\n\n if (\n options.maxDelayMs !== undefined &&\n (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < 0)\n ) {\n throw new RangeError(\"maxDelayMs must be a non-negative finite number\")\n }\n}\n\nconst retryDelay = (\n failedAttempt: number,\n options: RetryWithBackoffOptions,\n): number => {\n const exponentialDelay = options.initialDelayMs * 2 ** (failedAttempt - 1)\n const jitteredDelay =\n options.jitter === false\n ? exponentialDelay\n : exponentialDelay * (1 + Math.random())\n\n return Math.min(jitteredDelay, options.maxDelayMs ?? Number.MAX_SAFE_INTEGER)\n}\n\n/**\n * Repeats a failing asynchronous operation with abortable exponential backoff.\n * `attempts` is the total call count, not the number of retries. The original\n * error is rethrown when it is not retryable or the final attempt fails.\n */\nexport const retryWithBackoff = async <Value>(\n operation: (attempt: number) => Promise<Value>,\n options: RetryWithBackoffOptions,\n): Promise<Value> => {\n validateOptions(options)\n\n for (let attempt = 1; attempt <= options.attempts; attempt += 1) {\n throwIfAborted(options.signal)\n\n try {\n return await operation(attempt)\n } catch (error) {\n if (\n attempt === options.attempts ||\n options.signal?.aborted === true ||\n !options.shouldRetry(error, attempt)\n ) {\n throw error\n }\n\n await wait(retryDelay(attempt, options), options.signal)\n }\n }\n\n throw new Error(\"retryWithBackoff reached an invalid state\")\n}\n","import {\n identifierValue,\n type KnownMetadataIdentifierScheme,\n type MetadataIdentifier,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\n\nexport const GOOGLE_BOOKS_IDENTIFIER_SCHEME =\n \"GoogleBooks\" satisfies KnownMetadataIdentifierScheme\n\nexport type GoogleBooksLookup = {\n readonly id: string\n /** The book's exact identifier spelling, echoed only after Google confirms it. */\n readonly identifier: MetadataIdentifier\n}\n\n/**\n * Recognizes an authored Google Books volume id and the official Google Books\n * website/API URL forms. Arbitrary scheme-less strings are not reinterpreted\n * as ids: callers with a raw id should label it `GoogleBooks`.\n */\nexport const googleBooksLookupFromInput = (\n input: FetchMetadataInput,\n): GoogleBooksLookup | undefined => {\n for (const identifier of input.identifiers ?? []) {\n const id = identifierValue([identifier], GOOGLE_BOOKS_IDENTIFIER_SCHEME)\n\n if (id !== undefined) {\n return {\n id,\n identifier: { value: identifier.value, scheme: identifier.scheme },\n }\n }\n }\n\n return undefined\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\n\nexport type GoogleBooksIndustryIdentifier = {\n readonly type?: string\n readonly identifier: string\n}\n\nexport type GoogleBooksImageLinks = {\n readonly smallThumbnail?: string\n readonly thumbnail?: string\n readonly small?: string\n readonly medium?: string\n readonly large?: string\n readonly extraLarge?: string\n}\n\nexport type GoogleBooksVolumeSeries = {\n readonly seriesId?: string\n readonly orderNumber?: number\n}\n\n/**\n * Only the volume's place in its series is retained. `bookDisplayNumber` and\n * `shortSeriesBookTitle` are display strings by Google's own definition — the\n * sequence is `orderNumber` — and the payload never states the series name,\n * only an id, so a name would have to be invented.\n */\nexport type GoogleBooksSeriesInfo = {\n readonly volumeSeries?: ReadonlyArray<GoogleBooksVolumeSeries>\n}\n\n/**\n * The subset of a Google Books `volumeInfo` record that has a normalized home.\n * Every field remains optional because the public catalog is sparse in practice.\n */\nexport type GoogleBooksVolumeInfo = {\n readonly title?: string\n readonly subtitle?: string\n readonly authors?: ReadonlyArray<string>\n readonly publisher?: string\n readonly publishedDate?: string\n readonly description?: string\n readonly industryIdentifiers?: ReadonlyArray<GoogleBooksIndustryIdentifier>\n readonly pageCount?: number\n readonly categories?: ReadonlyArray<string>\n /** Mean review rating; Google states it on a 1–5 scale, rounded to a half. */\n readonly averageRating?: number\n readonly ratingsCount?: number\n readonly language?: string\n readonly imageLinks?: GoogleBooksImageLinks\n readonly infoLink?: string\n readonly canonicalVolumeLink?: string\n readonly seriesInfo?: GoogleBooksSeriesInfo\n}\n\n/** One Google Books volume, retaining the source's nested field names. */\nexport type GoogleBooksVolume = {\n readonly id?: string\n readonly volumeInfo?: GoogleBooksVolumeInfo\n}\n\nconst readRecord = (\n record: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined => {\n const value = record[key]\n\n return isJsonRecord(value) ? value : undefined\n}\n\nconst parseIndustryIdentifiers = (\n volumeInfo: Record<string, unknown>,\n): ReadonlyArray<GoogleBooksIndustryIdentifier> | undefined => {\n const identifiers = readRecordArray(\n volumeInfo,\n \"industryIdentifiers\",\n ).flatMap((record) => {\n const identifier = readString(record, \"identifier\")\n\n return identifier !== undefined\n ? [\n omitUndefined({\n type: readString(record, \"type\"),\n identifier,\n }),\n ]\n : []\n })\n\n return identifiers.length > 0 ? identifiers : undefined\n}\n\nconst parseImageLinks = (\n volumeInfo: Record<string, unknown>,\n): GoogleBooksImageLinks | undefined => {\n const imageLinks = readRecord(volumeInfo, \"imageLinks\")\n\n if (imageLinks === undefined) return undefined\n\n const parsed = omitUndefined({\n smallThumbnail: readString(imageLinks, \"smallThumbnail\"),\n thumbnail: readString(imageLinks, \"thumbnail\"),\n small: readString(imageLinks, \"small\"),\n medium: readString(imageLinks, \"medium\"),\n large: readString(imageLinks, \"large\"),\n extraLarge: readString(imageLinks, \"extraLarge\"),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\nconst parseSeriesInfo = (\n volumeInfo: Record<string, unknown>,\n): GoogleBooksSeriesInfo | undefined => {\n const seriesInfo = readRecord(volumeInfo, \"seriesInfo\")\n\n if (seriesInfo === undefined) return undefined\n\n const volumeSeries = readRecordArray(seriesInfo, \"volumeSeries\").flatMap(\n (record) => {\n const parsed = omitUndefined({\n seriesId: readString(record, \"seriesId\"),\n orderNumber: readNumber(record, \"orderNumber\"),\n })\n\n return Object.keys(parsed).length > 0 ? [parsed] : []\n },\n )\n\n return volumeSeries.length > 0 ? { volumeSeries } : undefined\n}\n\nconst parseVolumeInfo = (\n record: Record<string, unknown>,\n): GoogleBooksVolumeInfo | undefined => {\n const volumeInfo = readRecord(record, \"volumeInfo\")\n\n if (volumeInfo === undefined) return undefined\n\n const parsed = omitUndefined({\n title: readString(volumeInfo, \"title\"),\n subtitle: readString(volumeInfo, \"subtitle\"),\n authors: readStringArray(volumeInfo, \"authors\"),\n publisher: readString(volumeInfo, \"publisher\"),\n publishedDate: readString(volumeInfo, \"publishedDate\"),\n description: readString(volumeInfo, \"description\"),\n industryIdentifiers: parseIndustryIdentifiers(volumeInfo),\n pageCount: readNumber(volumeInfo, \"pageCount\"),\n categories: readStringArray(volumeInfo, \"categories\"),\n averageRating: readNumber(volumeInfo, \"averageRating\"),\n ratingsCount: readNumber(volumeInfo, \"ratingsCount\"),\n language: readString(volumeInfo, \"language\"),\n imageLinks: parseImageLinks(volumeInfo),\n infoLink: readString(volumeInfo, \"infoLink\"),\n canonicalVolumeLink: readString(volumeInfo, \"canonicalVolumeLink\"),\n seriesInfo: parseSeriesInfo(volumeInfo),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\n/** Reads one public Google Books Volume resource from untrusted JSON. */\nexport const parseGoogleBooksVolume = (\n payload: unknown,\n): GoogleBooksVolume | undefined => {\n if (!isJsonRecord(payload)) return undefined\n\n const parsed = omitUndefined({\n id: readString(payload, \"id\"),\n volumeInfo: parseVolumeInfo(payload),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\n/** Reads the `items` array of a Google Books volumes search response. */\nexport const parseGoogleBooksVolumesResponse = (\n payload: unknown,\n): ReadonlyArray<GoogleBooksVolume> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"items\").flatMap((record) => {\n const volume = parseGoogleBooksVolume(record)\n\n return volume !== undefined ? [volume] : []\n })\n}\n","import type { ResolvedAggregateRating } from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"./omitUndefined.ts\"\n\nconst MAX_VALUE = 5\n\n/**\n * Builds the cross-format aggregate rating from a catalog's mean and count.\n * Both Google Books and Open Library state the mean on a 1–5 star scale, which\n * is the scale {@link ResolvedAggregateRating} promises, so neither needs\n * rescaling — a source on another scale divides before calling this.\n *\n * A mean outside 0–5, a count that isn't a non-negative integer, and a mean\n * announced over zero ratings are all dropped: a catalog contradicting itself\n * would otherwise render as a real star count.\n */\nexport const resolvedAggregateRating = (\n mean: number | undefined,\n count: number | undefined,\n): ResolvedAggregateRating | undefined => {\n if (mean === undefined || mean < 0 || mean > MAX_VALUE) return undefined\n\n const ratings =\n count !== undefined && Number.isInteger(count) && count >= 0\n ? count\n : undefined\n\n if (ratings === 0) return undefined\n\n return omitUndefined({ value: mean, count: ratings })\n}\n","import {\n type MetadataIdentifier,\n parseW3cDtfDate,\n type ResolvedCollection,\n type ResolvedContributor,\n type ResolvedCover,\n type ResolvedMetadata,\n type ResolvedTitle,\n} from \"@prose-reader/archive-reader\"\nimport { resolvedAggregateRating } from \"../../utils/aggregateRating.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { GOOGLE_BOOKS_IDENTIFIER_SCHEME } from \"./identifier.ts\"\nimport type {\n GoogleBooksImageLinks,\n GoogleBooksIndustryIdentifier,\n GoogleBooksSeriesInfo,\n GoogleBooksVolume,\n} from \"./parse.ts\"\n\nexport const GOOGLE_BOOKS_MAX_SUBJECTS = 25\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nconst resolvedIdentifierScheme = (type: string | undefined): string =>\n type === \"ISBN_10\" || type === \"ISBN_13\" ? \"ISBN\" : (type ?? \"Unknown\")\n\nconst dedupeIdentifiers = (\n identifiers: ReadonlyArray<MetadataIdentifier>,\n): ReadonlyArray<MetadataIdentifier> | undefined =>\n emptyToUndefined(\n identifiers.filter(\n (identifier, index, all) =>\n all.findIndex(\n (other) =>\n other.value.trim().toLowerCase() ===\n identifier.value.trim().toLowerCase() &&\n other.scheme.trim().toLowerCase() ===\n identifier.scheme.trim().toLowerCase(),\n ) === index,\n ),\n )\n\nconst resolvedIndustryIdentifiers = (\n values: ReadonlyArray<GoogleBooksIndustryIdentifier>,\n): ReadonlyArray<MetadataIdentifier> =>\n values.map((identifier) =>\n omitUndefined({\n value: identifier.identifier,\n scheme: resolvedIdentifierScheme(identifier.type),\n }),\n )\n\n/**\n * Google states the title and the subtitle as separate fields, so they stay\n * separate: composing them for display is the consumer's decision.\n */\nconst resolvedTitles = (\n title: string | undefined,\n subtitle: string | undefined,\n): ReadonlyArray<ResolvedTitle> | undefined =>\n emptyToUndefined([\n ...(title !== undefined ? [{ value: title }] : []),\n ...(subtitle !== undefined\n ? [{ value: subtitle, type: \"subtitle\" as const }]\n : []),\n ])\n\n/**\n * The volume's place in its series, which Google identifies by id alone — it\n * never states the series name, and inventing one would be fabrication.\n */\nconst resolvedSeries = (\n seriesInfo: GoogleBooksSeriesInfo | undefined,\n): ReadonlyArray<ResolvedCollection> | undefined =>\n emptyToUndefined(\n (seriesInfo?.volumeSeries ?? []).flatMap<ResolvedCollection>((series) => {\n const entry = omitUndefined({\n identifiers:\n series.seriesId !== undefined\n ? [\n {\n value: series.seriesId,\n scheme: GOOGLE_BOOKS_IDENTIFIER_SCHEME,\n },\n ]\n : undefined,\n position: series.orderNumber,\n })\n\n return Object.keys(entry).length > 0 ? [entry] : []\n }),\n )\n\nconst absoluteHttpUrl = (value: string | undefined): string | undefined => {\n if (value === undefined) return undefined\n\n try {\n const url = new URL(value)\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined\n\n if (url.protocol === \"http:\") url.protocol = \"https:\"\n\n return url.toString()\n } catch {\n return undefined\n }\n}\n\n/**\n * Prefer the largest cover Google announced and force HTTPS. Preserve the\n * query exactly: changing Google's `zoom`, `edge` or signed `imgtk` parameters\n * can turn a valid cover into its \"image not available\" placeholder.\n */\nexport const googleBooksCoverUrl = (\n imageLinks: GoogleBooksImageLinks | undefined,\n): string | undefined => {\n if (imageLinks === undefined) return undefined\n\n const candidates = [\n imageLinks.extraLarge,\n imageLinks.large,\n imageLinks.medium,\n imageLinks.small,\n imageLinks.thumbnail,\n imageLinks.smallThumbnail,\n ]\n\n for (const candidate of candidates) {\n const absolute = absoluteHttpUrl(candidate)\n\n if (absolute !== undefined) return absolute\n }\n\n return undefined\n}\n\nconst resolvedContributors = (\n authors: ReadonlyArray<string>,\n): ReadonlyArray<ResolvedContributor> | undefined =>\n emptyToUndefined(\n authors.map<ResolvedContributor>((name) => ({\n name,\n roles: [\"author\"],\n })),\n )\n\nexport type ResolveGoogleBooksVolumeOptions = {\n /** Exact Google Books identifier that this response confirmed. */\n readonly matchedIdentifier?: MetadataIdentifier\n /** ISBN query that this response confirmed, even if the payload omits it. */\n readonly matchedIsbn?: string\n}\n\n/** Normalizes one Google Books volume into the cross-format vocabulary. */\nexport const resolveGoogleBooksVolume = (\n volume: GoogleBooksVolume,\n options: ResolveGoogleBooksVolumeOptions = {},\n): ResolvedMetadata => {\n const { id, volumeInfo, ...unhandledVolume } = volume\n const {\n title,\n subtitle,\n authors,\n publisher,\n publishedDate,\n description,\n industryIdentifiers,\n pageCount,\n categories,\n averageRating,\n ratingsCount,\n language,\n imageLinks,\n seriesInfo,\n // read by `googleBooksVolumeUrl`, which builds the match's url\n infoLink: _infoLink,\n canonicalVolumeLink: _canonicalVolumeLink,\n ...unhandledVolumeInfo\n } = volumeInfo ?? {}\n\n // Naming every parsed field is the contract: adding one to the parsed\n // shapes fails here until someone decides what becomes of it.\n unhandledVolume satisfies Record<string, never>\n unhandledVolumeInfo satisfies Record<string, never>\n\n const sourceIdentifiers = resolvedIndustryIdentifiers(\n industryIdentifiers ?? [],\n )\n const identifiers = dedupeIdentifiers([\n ...(options.matchedIdentifier !== undefined\n ? [options.matchedIdentifier]\n : []),\n ...(options.matchedIsbn !== undefined\n ? [{ value: options.matchedIsbn, scheme: \"ISBN\" }]\n : []),\n ...(id !== undefined\n ? [{ value: id, scheme: GOOGLE_BOOKS_IDENTIFIER_SCHEME }]\n : []),\n ...sourceIdentifiers,\n ])\n const edition = omitUndefined({\n date: parseW3cDtfDate(publishedDate),\n publisher,\n })\n const coverUri = googleBooksCoverUrl(imageLinks)\n const cover: ResolvedCover | undefined =\n coverUri !== undefined\n ? {\n uri: coverUri,\n confidence: \"derived\",\n }\n : undefined\n const subjects = [...new Set(categories ?? [])].slice(\n 0,\n GOOGLE_BOOKS_MAX_SUBJECTS,\n )\n\n const series = resolvedSeries(seriesInfo)\n\n return omitUndefined({\n titles: resolvedTitles(title, subtitle),\n description,\n belongsTo: series !== undefined ? { series } : undefined,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n languages: language !== undefined ? [language] : undefined,\n subjects: emptyToUndefined(subjects),\n contributors: resolvedContributors(authors ?? []),\n numberOfPages: pageCount,\n aggregateRating: resolvedAggregateRating(averageRating, ratingsCount),\n identifiers,\n cover,\n })\n}\n\n/** The human-facing Google Books page for a candidate. */\nexport const googleBooksVolumeUrl = (\n volume: GoogleBooksVolume,\n): string | undefined =>\n absoluteHttpUrl(volume.volumeInfo?.canonicalVolumeLink) ??\n absoluteHttpUrl(volume.volumeInfo?.infoLink) ??\n (volume.id !== undefined\n ? `https://books.google.com/books?id=${encodeURIComponent(volume.id)}`\n : undefined)\n","import { identifierValue } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { isJsonRecord, readRecordArray } from \"../../utils/json.ts\"\nimport { retryWithBackoff } from \"../../utils/retryWithBackoff.ts\"\nimport {\n MetadataProviderResponseError,\n responseErrorStatus,\n} from \"../responseError.ts\"\nimport { googleBooksLookupFromInput } from \"./identifier.ts\"\nimport { type GoogleBooksVolume, parseGoogleBooksVolume } from \"./parse.ts\"\nimport {\n googleBooksVolumeUrl,\n type ResolveGoogleBooksVolumeOptions,\n resolveGoogleBooksVolume,\n} from \"./resolve.ts\"\n\nconst GOOGLE_BOOKS_MAX_RESULTS = 40\nconst GOOGLE_BOOKS_EXACT_OVERSAMPLE = 1\nconst GOOGLE_BOOKS_TITLE_OVERSAMPLE = 4\nconst GOOGLE_BOOKS_REQUEST_ATTEMPTS = 3\nconst GOOGLE_BOOKS_INITIAL_RETRY_DELAY_MS = 1_000\nconst GOOGLE_BOOKS_MAX_RETRY_DELAY_MS = 3_000\nconst GOOGLE_BOOKS_RETRYABLE_STATUSES = new Set([500, 502, 503, 504])\n\ntype GoogleBooksSearchOptions = {\n readonly apiKey: string\n readonly baseUrl: string\n readonly fetch: typeof globalThis.fetch\n}\n\ntype GoogleBooksVolumeRecord = {\n readonly volume: GoogleBooksVolume\n readonly raw: unknown\n}\n\nconst QUERY_BREAKING_CHARACTERS = /[\"\\\\]+/g\nconst FIELD_OPERATOR_SEPARATOR = /:/g\nconst TERM_EXCLUSION_PREFIX = /(^|\\s)-+/g\nconst REPEATED_WHITESPACE = /\\s+/g\n\n/**\n * Terms reach Google's field operators unquoted on purpose: `intitle:\"…\"`\n * demands the phrase verbatim, which a title carrying a volume marker, a\n * scanner tag or a translated subtitle never satisfies, and no query\n * relaxation follows to recover with — the lookup would report nothing for a\n * book Google holds. Loose ranking here, precision in the scorer.\n *\n * Unquoted, though, the term is parsed as query syntax rather than carried as\n * a literal, so anything Google would read as an instruction goes: a colon\n * would promote the rest of a scanner-derived title into another field\n * (`Dune inauthor:Asimov` searching authors), a leading hyphen would exclude\n * the word behind it (`BLAME! -Master Edition-` demanding books *without*\n * \"Master\"), and both mutations can return results, which also robs the\n * title-only fallback of its turn.\n */\nconst searchTerm = (value: string): string =>\n value\n .replace(QUERY_BREAKING_CHARACTERS, \" \")\n .replace(FIELD_OPERATOR_SEPARATOR, \" \")\n .replace(TERM_EXCLUSION_PREFIX, \" \")\n .replace(REPEATED_WHITESPACE, \" \")\n .trim()\n\nconst isSearchable = (term: string): boolean => term.length > 0\n\nconst titleSearchTerms = (\n input: FetchMetadataInput,\n): { readonly title: string; readonly author?: string } | undefined => {\n const title = searchTerm(input.title ?? \"\")\n\n if (title.length === 0) return undefined\n\n const author = input.authors?.map(searchTerm).find(isSearchable)\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\nconst parseVolumeRecord = (\n payload: unknown,\n): GoogleBooksVolumeRecord | undefined => {\n const volume = parseGoogleBooksVolume(payload)\n\n return volume !== undefined ? { volume, raw: payload } : undefined\n}\n\nconst parseVolumeRecords = (\n payload: unknown,\n): ReadonlyArray<GoogleBooksVolumeRecord> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"items\").flatMap((raw) => {\n const record = parseVolumeRecord(raw)\n\n return record !== undefined ? [record] : []\n })\n}\n\nconst toCandidates = (\n records: ReadonlyArray<GoogleBooksVolumeRecord>,\n resolveOptions: ResolveGoogleBooksVolumeOptions = {},\n): ReadonlyArray<MetadataCandidate> =>\n records.map(({ volume, raw }) => ({\n metadata: resolveGoogleBooksVolume(volume, resolveOptions),\n id: volume.id,\n url: googleBooksVolumeUrl(volume),\n raw,\n }))\n\n/**\n * Creates the Google Books lookup pipeline. Lookup order is an exact volume\n * identifier, ISBN, then title plus first author with a title-only fallback.\n * A title search asks for several pages worth of `limit` because Google ranks\n * it, not the matcher: the edition the caller described can sit below the ones\n * Google finds most popular, and `fetchMetadata` trims to `limit` after scoring.\n */\nexport const createGoogleBooksSearch = (\n options: GoogleBooksSearchOptions,\n): MetadataProvider[\"search\"] => {\n const volumesUrl = `${options.baseUrl.replace(/\\/+$/, \"\")}/volumes`\n\n const request = async (\n url: URL,\n context: MetadataProviderContext,\n notFoundIsEmpty = false,\n ): Promise<Response | undefined> => {\n url.searchParams.set(\"key\", options.apiKey)\n\n return retryWithBackoff(\n async () => {\n const response = await options.fetch(url.toString(), {\n signal: context.signal,\n headers: { Accept: \"application/json\" },\n })\n\n if (notFoundIsEmpty && response.status === 404) return undefined\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Google Books lookup failed with status ${response.status}`,\n )\n }\n\n return response\n },\n {\n attempts: GOOGLE_BOOKS_REQUEST_ATTEMPTS,\n initialDelayMs: GOOGLE_BOOKS_INITIAL_RETRY_DELAY_MS,\n maxDelayMs: GOOGLE_BOOKS_MAX_RETRY_DELAY_MS,\n signal: context.signal,\n shouldRetry: (error) => {\n const status = responseErrorStatus(error)\n\n return (\n status === undefined || GOOGLE_BOOKS_RETRYABLE_STATUSES.has(status)\n )\n },\n },\n )\n }\n\n const findByVolumeId = async (\n id: string,\n context: MetadataProviderContext,\n ): Promise<GoogleBooksVolumeRecord | undefined> => {\n const response = await request(\n new URL(`${volumesUrl}/${encodeURIComponent(id)}`),\n context,\n true,\n )\n\n if (response === undefined) return undefined\n\n const record = parseVolumeRecord(await response.json())\n\n if (record?.volume.id !== id) {\n throw new Error(\n `Google Books response did not contain the requested volume ${id}`,\n )\n }\n\n return record\n }\n\n const searchVolumes = async (\n query: string,\n context: MetadataProviderContext,\n oversample: number,\n ): Promise<ReadonlyArray<GoogleBooksVolumeRecord>> => {\n const pageSize = Math.min(\n Math.ceil(context.limit * oversample),\n GOOGLE_BOOKS_MAX_RESULTS,\n )\n\n if (pageSize <= 0) return []\n\n const url = new URL(volumesUrl)\n\n url.searchParams.set(\"q\", query)\n url.searchParams.set(\"maxResults\", String(pageSize))\n url.searchParams.set(\"orderBy\", \"relevance\")\n url.searchParams.set(\"printType\", \"books\")\n\n const response = await request(url, context)\n\n return response !== undefined\n ? parseVolumeRecords(await response.json())\n : []\n }\n\n return async (input, context) => {\n const googleBooks = googleBooksLookupFromInput(input)\n\n if (googleBooks !== undefined) {\n const record = await findByVolumeId(googleBooks.id, context)\n\n if (record !== undefined) {\n return toCandidates([record], {\n matchedIdentifier: googleBooks.identifier,\n })\n }\n }\n\n const isbn = identifierValue(input.identifiers, \"ISBN\")\n\n if (isbn !== undefined) {\n const volumes = await searchVolumes(\n `isbn:${isbn}`,\n context,\n GOOGLE_BOOKS_EXACT_OVERSAMPLE,\n )\n\n if (volumes.length > 0) {\n return toCandidates(volumes, { matchedIsbn: isbn })\n }\n }\n\n const terms = titleSearchTerms(input)\n\n if (terms === undefined) return []\n\n const titleQuery = `intitle:${terms.title}`\n const preciseQuery =\n terms.author !== undefined\n ? `${titleQuery} inauthor:${terms.author}`\n : titleQuery\n const volumes = await searchVolumes(\n preciseQuery,\n context,\n GOOGLE_BOOKS_TITLE_OVERSAMPLE,\n )\n\n if (volumes.length > 0 || terms.author === undefined) {\n return toCandidates(volumes)\n }\n\n return toCandidates(\n await searchVolumes(titleQuery, context, GOOGLE_BOOKS_TITLE_OVERSAMPLE),\n )\n }\n}\n","import type { MetadataProvider } from \"../../types/provider.ts\"\nimport { createGoogleBooksSearch } from \"./search.ts\"\n\nexport const GOOGLE_BOOKS_PROVIDER_ID = \"googleBooks\"\n\nconst DEFAULT_BASE_URL = \"https://www.googleapis.com/books/v1\"\n\nexport type GoogleBooksProviderOptions = {\n /** API key identifying the application to Google Books. */\n readonly apiKey: string\n /** API root. Defaults to `https://www.googleapis.com/books/v1`. */\n readonly baseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n}\n\n/**\n * Google Books' public volume catalog. An API key is required. Lookup order:\n * an exact `GoogleBooks` id/official URL, ISBN, then title plus first author\n * with a title-only fallback. Google relevance order is preserved here; the\n * shared fetcher scores every returned edition against the caller's input.\n */\nexport const createGoogleBooksProvider = (\n options: GoogleBooksProviderOptions,\n): MetadataProvider => {\n const apiKey = options.apiKey.trim()\n\n if (apiKey.length === 0) {\n throw new Error(\"createGoogleBooksProvider requires a non-empty apiKey\")\n }\n\n return {\n id: GOOGLE_BOOKS_PROVIDER_ID,\n name: \"Google Books\",\n search: createGoogleBooksSearch({\n apiKey,\n baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,\n fetch: options.fetch ?? globalThis.fetch,\n }),\n }\n}\n","import {\n identifierValue,\n type KnownMetadataIdentifierScheme,\n type MetadataIdentifier,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\n\nexport const PROJECT_GUTENBERG_IDENTIFIER_SCHEME =\n \"ProjectGutenberg\" satisfies KnownMetadataIdentifierScheme\n\nexport type ProjectGutenbergLookup = {\n readonly id: string\n /** The book's exact identifier spelling, echoed only after PG confirms it. */\n readonly identifier: MetadataIdentifier\n}\n\n/**\n * Recognizes Project Gutenberg's own numeric identifier and official URL\n * forms. Calling providers decide explicitly how to use this crosswalk; the\n * shared scorer never reinterprets arbitrary URLs.\n */\nexport const projectGutenbergLookupFromInput = (\n input: FetchMetadataInput,\n): ProjectGutenbergLookup | undefined => {\n for (const identifier of input.identifiers ?? []) {\n const id = identifierValue(\n [identifier],\n PROJECT_GUTENBERG_IDENTIFIER_SCHEME,\n )\n\n if (id !== undefined) {\n return {\n id,\n identifier: { value: identifier.value, scheme: identifier.scheme },\n }\n }\n }\n\n return undefined\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\n\n/**\n * One entry of an Open Library `search.json` response, field names mirroring\n * the API (same rule as the archive parsers: a parsed source looks like its\n * source document). Every field is optional — the API omits what it doesn't\n * have, and the `fields` parameter decides what it may return at all.\n *\n * @see https://openlibrary.org/dev/docs/api/search\n */\nexport type OpenLibraryDoc = {\n /** Work key, e.g. `/works/OL45883W`. */\n readonly key?: string\n readonly title?: string\n readonly subtitle?: string\n readonly author_name?: ReadonlyArray<string>\n readonly first_publish_year?: number\n /** MARC 21 language codes (`eng`, `fre`), not BCP 47. */\n readonly language?: ReadonlyArray<string>\n readonly subject?: ReadonlyArray<string>\n readonly number_of_pages_median?: number\n /** Mean of the work's star ratings, on the catalog's 1–5 scale. */\n readonly ratings_average?: number\n readonly ratings_count?: number\n /** Cover id, addressing `covers.openlibrary.org`. */\n readonly cover_i?: number\n /** Project Gutenberg ebook ids attached to the work's editions. */\n readonly id_project_gutenberg?: ReadonlyArray<string>\n}\n\nconst parseDoc = (record: Record<string, unknown>): OpenLibraryDoc => ({\n key: readString(record, \"key\"),\n title: readString(record, \"title\"),\n subtitle: readString(record, \"subtitle\"),\n author_name: readStringArray(record, \"author_name\"),\n first_publish_year: readNumber(record, \"first_publish_year\"),\n language: readStringArray(record, \"language\"),\n subject: readStringArray(record, \"subject\"),\n number_of_pages_median: readNumber(record, \"number_of_pages_median\"),\n ratings_average: readNumber(record, \"ratings_average\"),\n ratings_count: readNumber(record, \"ratings_count\"),\n cover_i: readNumber(record, \"cover_i\"),\n id_project_gutenberg: readStringArray(record, \"id_project_gutenberg\"),\n})\n\n/**\n * Reads the `docs` of a `search.json` payload. A payload that isn't a record,\n * or carries no `docs` array, yields an empty list: an unexpected response\n * shape is \"this catalog has nothing for us\", not a crash.\n */\nexport const parseOpenLibrarySearchResponse = (\n payload: unknown,\n): ReadonlyArray<OpenLibraryDoc> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"docs\").map(parseDoc)\n}\n","/**\n * MARC 21 language codes → BCP 47 primary subtags, for the codes a general\n * book catalog actually returns. Open Library reports `language` as MARC\n * (`eng`, `fre`), while {@link ResolvedMetadata.languages} is BCP 47 by\n * contract, so the mapping is part of normalizing — not a nicety.\n *\n * Both MARC variants are listed where they differ (bibliographic `fre` and\n * terminology `fra`), since records in the wild carry either.\n */\nconst MARC_TO_BCP47: Readonly<Record<string, string>> = {\n afr: \"af\",\n alb: \"sq\",\n amh: \"am\",\n ara: \"ar\",\n arm: \"hy\",\n asm: \"as\",\n aze: \"az\",\n baq: \"eu\",\n bel: \"be\",\n ben: \"bn\",\n bos: \"bs\",\n bul: \"bg\",\n bur: \"my\",\n cat: \"ca\",\n ces: \"cs\",\n chi: \"zh\",\n cym: \"cy\",\n cze: \"cs\",\n dan: \"da\",\n deu: \"de\",\n dut: \"nl\",\n ell: \"el\",\n eng: \"en\",\n epo: \"eo\",\n est: \"et\",\n eus: \"eu\",\n fao: \"fo\",\n fas: \"fa\",\n fin: \"fi\",\n fra: \"fr\",\n fre: \"fr\",\n geo: \"ka\",\n ger: \"de\",\n gle: \"ga\",\n glg: \"gl\",\n gre: \"el\",\n guj: \"gu\",\n heb: \"he\",\n hin: \"hi\",\n hrv: \"hr\",\n hun: \"hu\",\n hye: \"hy\",\n ice: \"is\",\n ind: \"id\",\n isl: \"is\",\n ita: \"it\",\n jpn: \"ja\",\n kan: \"kn\",\n kat: \"ka\",\n kaz: \"kk\",\n khm: \"km\",\n kor: \"ko\",\n lao: \"lo\",\n lat: \"la\",\n lav: \"lv\",\n lit: \"lt\",\n ltz: \"lb\",\n mac: \"mk\",\n mal: \"ml\",\n mar: \"mr\",\n may: \"ms\",\n mkd: \"mk\",\n mlt: \"mt\",\n mon: \"mn\",\n msa: \"ms\",\n mya: \"my\",\n nep: \"ne\",\n nld: \"nl\",\n nno: \"nn\",\n nob: \"nb\",\n nor: \"no\",\n ori: \"or\",\n pan: \"pa\",\n per: \"fa\",\n pol: \"pl\",\n por: \"pt\",\n rom: \"ro\",\n ron: \"ro\",\n rum: \"ro\",\n rus: \"ru\",\n scc: \"sr\",\n scr: \"hr\",\n sin: \"si\",\n slk: \"sk\",\n slo: \"sk\",\n slv: \"sl\",\n snd: \"sd\",\n spa: \"es\",\n sqi: \"sq\",\n srp: \"sr\",\n swa: \"sw\",\n swe: \"sv\",\n tam: \"ta\",\n tel: \"te\",\n tgl: \"tl\",\n tha: \"th\",\n tur: \"tr\",\n ukr: \"uk\",\n urd: \"ur\",\n uzb: \"uz\",\n vie: \"vi\",\n wel: \"cy\",\n yid: \"yi\",\n zho: \"zh\",\n}\n\n/**\n * MARC placeholders that say \"no single language\": keeping them would put a\n * non-language in a BCP 47 field, so they resolve to nothing.\n */\nconst NON_LANGUAGE_CODES = new Set([\"mul\", \"und\", \"zxx\", \"sgn\"])\n\n/**\n * Normalizes one MARC language code to its BCP 47 primary subtag. Unknown\n * codes (the long tail of MARC) pass through lowercased rather than being\n * dropped — a language we can't name is still a language the record stated.\n */\nexport const marcLanguageToBcp47 = (code: string): string | undefined => {\n const normalized = code.trim().toLowerCase()\n\n if (normalized.length === 0) return undefined\n if (NON_LANGUAGE_CODES.has(normalized)) return undefined\n\n return MARC_TO_BCP47[normalized] ?? normalized\n}\n","import type {\n KnownMetadataIdentifierScheme,\n MetadataIdentifier,\n ResolvedMetadata,\n} from \"@prose-reader/archive-reader\"\nimport { resolvedAggregateRating } from \"../../utils/aggregateRating.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { marcLanguageToBcp47 } from \"./marcLanguage.ts\"\nimport type { OpenLibraryDoc } from \"./parse.ts\"\n\n/**\n * A popular work carries hundreds of subject headings, most of them long-tail\n * noise (\"Accessible book\", \"Protected DAISY\"), which would dwarf the entity a\n * consumer persists. The API puts the meaningful ones first.\n */\nexport const OPEN_LIBRARY_MAX_SUBJECTS = 25\n\nexport const OPEN_LIBRARY_IDENTIFIER_SCHEME =\n \"OpenLibrary\" satisfies KnownMetadataIdentifierScheme\nconst PROJECT_GUTENBERG_IDENTIFIER_SCHEME =\n \"ProjectGutenberg\" satisfies KnownMetadataIdentifierScheme\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Normalizes one search hit into the cross-format vocabulary. Two choices\n * worth stating:\n *\n * - **`title` and `subtitle` stay separate**, as the catalog states them:\n * `title` is the work's title and the subtitle is a `titles` entry. The\n * matcher composes them when it compares, so nothing is lost by not\n * pre-joining them here.\n * - **An ISBN identifier is added only for an ISBN lookup**, using the queried\n * value — the API answered \"this work has that ISBN\", a fact about the\n * record. A title-search hit describes a *work*, whose editions each have\n * their own ISBN, so picking one would be fabrication.\n */\nexport const resolveOpenLibraryDoc = (\n doc: OpenLibraryDoc,\n options: {\n readonly coversBaseUrl: string\n /** The ISBN this record was looked up by, when it was. */\n readonly isbn?: string\n /** The source identifier that an exact Gutenberg-id lookup confirmed. */\n readonly matchedProjectGutenbergIdentifier?: MetadataIdentifier\n },\n): ResolvedMetadata => {\n const {\n title,\n subtitle,\n language,\n key,\n author_name,\n first_publish_year,\n subject,\n number_of_pages_median,\n ratings_average,\n ratings_count,\n cover_i,\n id_project_gutenberg,\n ...unhandled\n } = doc\n\n // Naming every parsed field is the contract: adding one to\n // `OpenLibraryDoc` fails here until someone decides what becomes of it.\n unhandled satisfies Record<string, never>\n\n const titles = emptyToUndefined([\n ...(title !== undefined ? [{ value: title }] : []),\n ...(subtitle !== undefined\n ? [{ value: subtitle, type: \"subtitle\" as const }]\n : []),\n ])\n\n const languages = emptyToUndefined([\n ...new Set(\n (language ?? [])\n .map(marcLanguageToBcp47)\n .filter((value): value is string => value !== undefined),\n ),\n ])\n\n const identifiers = [\n ...(options.isbn !== undefined\n ? [{ value: options.isbn, scheme: \"ISBN\" }]\n : []),\n ...(options.matchedProjectGutenbergIdentifier !== undefined\n ? [options.matchedProjectGutenbergIdentifier]\n : []),\n ...(id_project_gutenberg ?? []).map((value) => ({\n value,\n scheme: PROJECT_GUTENBERG_IDENTIFIER_SCHEME,\n })),\n ...(key !== undefined\n ? [{ value: key, scheme: OPEN_LIBRARY_IDENTIFIER_SCHEME }]\n : []),\n ]\n\n return omitUndefined({\n titles,\n cover:\n cover_i !== undefined\n ? {\n // absolute, not container-relative: a catalog addresses its\n // covers in its own space, with nothing to rebase onto\n uri: `${options.coversBaseUrl}/b/id/${cover_i}-L.jpg`,\n mediaType: \"image/jpeg\",\n confidence: \"derived\" as const,\n }\n : undefined,\n publication:\n first_publish_year !== undefined\n ? { original: { date: { year: first_publish_year } } }\n : undefined,\n languages,\n subjects: emptyToUndefined(\n (subject ?? []).slice(0, OPEN_LIBRARY_MAX_SUBJECTS),\n ),\n contributors: emptyToUndefined(\n (author_name ?? []).map((name) => ({\n name,\n roles: [\"author\" as const],\n })),\n ),\n numberOfPages: number_of_pages_median,\n aggregateRating: resolvedAggregateRating(ratings_average, ratings_count),\n identifiers: emptyToUndefined(identifiers),\n })\n}\n","import { identifierValue } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport {\n type ProjectGutenbergLookup,\n projectGutenbergLookupFromInput,\n} from \"../projectGutenberg/identifier.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { type OpenLibraryDoc, parseOpenLibrarySearchResponse } from \"./parse.ts\"\nimport { resolveOpenLibraryDoc } from \"./resolve.ts\"\n\nexport const OPEN_LIBRARY_PROVIDER_ID = \"openLibrary\"\n\nconst DEFAULT_BASE_URL = \"https://openlibrary.org\"\nconst DEFAULT_COVERS_BASE_URL = \"https://covers.openlibrary.org\"\n\n/**\n * Only what {@link resolveOpenLibraryDoc} maps: `search.json` returns every\n * edition key and IA identifier by default, so `fields` is the difference\n * between a few hundred bytes per hit and a few hundred kilobytes.\n */\nconst SEARCH_FIELDS = [\n \"key\",\n \"title\",\n \"subtitle\",\n \"author_name\",\n \"first_publish_year\",\n \"language\",\n \"subject\",\n \"number_of_pages_median\",\n \"ratings_average\",\n \"ratings_count\",\n \"cover_i\",\n \"id_project_gutenberg\",\n].join(\",\")\n\nexport type OpenLibraryProviderOptions = {\n /** API origin. Defaults to `https://openlibrary.org`. */\n readonly baseUrl?: string\n /** Cover service origin. Defaults to `https://covers.openlibrary.org`. */\n readonly coversBaseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /**\n * Open Library's API etiquette asks for an identifying `User-Agent` (app\n * name + contact) and throttles anonymous traffic harder.\n */\n readonly userAgent?: string\n}\n\nconst searchTerms = (\n input: FetchMetadataInput,\n): { readonly title: string; readonly author?: string } | undefined => {\n const title = input.title?.trim()\n\n if (title === undefined || title.length === 0) return undefined\n\n const author = input.authors?.find((value) => value.trim().length > 0)?.trim()\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\n/**\n * Open Library — the Internet Archive's open bibliographic catalog. Free, no\n * key, covers books broadly (much less so comics and manga).\n *\n * ```ts\n * const provider = createOpenLibraryProvider({\n * userAgent: \"MyReader/1.0 (contact@example.com)\",\n * })\n * ```\n *\n * Lookup strategy, at most four requests: an **ISBN search** when the book\n * states one; an exact **Project Gutenberg id search** when an identifier is\n * an official Gutenberg URL; then a precise **title (+ first author) search**,\n * followed by a free-text fallback when Open Library indexed part of the title\n * as a subtitle. A query with none of those terms yields no candidates rather\n * than a fishing expedition.\n */\nexport const createOpenLibraryProvider = (\n options: OpenLibraryProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n const coversBaseUrl = options.coversBaseUrl ?? DEFAULT_COVERS_BASE_URL\n\n const searchDocs = async (\n terms: Record<string, string>,\n context: MetadataProviderContext,\n ): Promise<ReadonlyArray<OpenLibraryDoc>> => {\n const url = new URL(\"/search.json\", baseUrl)\n\n for (const [key, value] of Object.entries(terms)) {\n url.searchParams.set(key, value)\n }\n\n url.searchParams.set(\"fields\", SEARCH_FIELDS)\n url.searchParams.set(\"limit\", String(context.limit))\n\n // read lazily so a consumer stubbing the global afterwards still wins\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(url.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/json\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Open Library search failed with status ${response.status}`,\n )\n }\n\n return parseOpenLibrarySearchResponse(await response.json())\n }\n\n const toCandidates = (\n docs: ReadonlyArray<OpenLibraryDoc>,\n options: {\n readonly isbn?: string\n readonly confirmedProjectGutenberg?: ProjectGutenbergLookup\n } = {},\n ): ReadonlyArray<MetadataCandidate> =>\n docs.map((doc) => ({\n metadata: resolveOpenLibraryDoc(doc, {\n coversBaseUrl,\n isbn: options.isbn,\n matchedProjectGutenbergIdentifier:\n options.confirmedProjectGutenberg?.identifier,\n }),\n id: doc.key,\n url: doc.key !== undefined ? `${baseUrl}${doc.key}` : undefined,\n raw: doc,\n }))\n\n return {\n id: OPEN_LIBRARY_PROVIDER_ID,\n name: \"Open Library\",\n search: async (input, context) => {\n const isbn = identifierValue(input.identifiers, \"ISBN\")\n const projectGutenberg = projectGutenbergLookupFromInput(input)\n\n if (isbn !== undefined) {\n const docs = await searchDocs({ isbn }, context)\n\n if (docs.length > 0) return toCandidates(docs, { isbn })\n }\n\n if (projectGutenberg !== undefined) {\n const docs = await searchDocs(\n { q: `id_project_gutenberg:${projectGutenberg.id}` },\n context,\n )\n\n if (docs.length > 0) {\n return toCandidates(docs, {\n confirmedProjectGutenberg: projectGutenberg,\n })\n }\n }\n\n const terms = searchTerms(input)\n\n if (terms === undefined) return []\n\n const docs = await searchDocs(terms, context)\n\n if (docs.length > 0) return toCandidates(docs)\n\n const q =\n terms.author !== undefined\n ? `${terms.title} ${terms.author}`\n : terms.title\n\n return toCandidates(await searchDocs({ q }, context))\n },\n }\n}\n","import type { XmlElement, XmlNodeBase } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\n\nexport type ProjectGutenbergContributor = {\n readonly name: string\n /** MARC relator code, or `aut` for `dcterms:creator`. */\n readonly role: string\n}\n\nexport type ProjectGutenbergCover = {\n readonly uri: string\n readonly mediaType?: string\n}\n\n/** The subset of one official per-eBook RDF record that we consume. */\nexport type ProjectGutenbergRecord = {\n readonly id: string\n readonly title?: string\n readonly publisher?: string\n readonly issued?: string\n /** Original print publication statement (`MARC 260`). */\n readonly originalPublication?: string\n readonly rights?: string\n readonly description?: string\n readonly summary?: string\n readonly languages?: ReadonlyArray<string>\n readonly subjects?: ReadonlyArray<string>\n readonly bookshelves?: ReadonlyArray<string>\n readonly contributors?: ReadonlyArray<ProjectGutenbergContributor>\n readonly cover?: ProjectGutenbergCover\n}\n\nconst elementLocalName = (name: string): string =>\n name.includes(\":\") ? name.slice(name.lastIndexOf(\":\") + 1) : name\n\nconst isXmlElement = (node: XmlNodeBase): node is XmlElement =>\n node.type === \"element\"\n\nconst childrenNamedLocal = (\n parent: XmlElement,\n localName: string,\n): ReadonlyArray<XmlElement> =>\n parent.children.filter(\n (child): child is XmlElement =>\n isXmlElement(child) &&\n elementLocalName(child.name).toLowerCase() === localName.toLowerCase(),\n )\n\nconst childNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement | undefined => childrenNamedLocal(parent, localName)[0]\n\nconst text = (element: XmlElement | undefined): string | undefined => {\n const trimmed = element?.val.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst attributeNamedLocal = (\n element: XmlElement,\n localName: string,\n): string | undefined => {\n for (const [name, value] of Object.entries(element.attr)) {\n if (elementLocalName(name).toLowerCase() !== localName.toLowerCase()) {\n continue\n }\n\n const trimmed = value.trim()\n\n if (trimmed.length > 0) return trimmed\n }\n\n return undefined\n}\n\nconst valuesFromDescriptions = (\n ebook: XmlElement,\n localName: string,\n): ReadonlyArray<string> =>\n childrenNamedLocal(ebook, localName).flatMap((container) => {\n const description = childNamedLocal(container, \"Description\")\n const value = text(childNamedLocal(description ?? container, \"value\"))\n\n return value !== undefined ? [value] : []\n })\n\nconst projectGutenbergIdFromEbook = (ebook: XmlElement): string | undefined => {\n const about = attributeNamedLocal(ebook, \"about\")\n const id =\n about !== undefined ? /(?:^|\\/)ebooks\\/(\\d+)$/.exec(about)?.[1] : undefined\n\n if (id === undefined) return undefined\n\n const normalized = id.replace(/^0+(?=\\d)/, \"\")\n\n return normalized !== \"0\" ? normalized : undefined\n}\n\nconst contributorsFromEbook = (\n ebook: XmlElement,\n): ReadonlyArray<ProjectGutenbergContributor> => {\n const contributors: ProjectGutenbergContributor[] = []\n\n for (const child of ebook.children) {\n if (!isXmlElement(child)) continue\n\n const isCreator = child.name.toLowerCase() === \"dcterms:creator\"\n const isMarcRelator = child.name.toLowerCase().startsWith(\"marcrel:\")\n\n if (!isCreator && !isMarcRelator) continue\n\n const name = text(\n childNamedLocal(childNamedLocal(child, \"agent\") ?? child, \"name\"),\n )\n\n if (name === undefined) continue\n\n contributors.push({\n name,\n role: isCreator ? \"aut\" : elementLocalName(child.name).toLowerCase(),\n })\n }\n\n return contributors\n}\n\nconst subjectValuesFromEbook = (ebook: XmlElement): ReadonlyArray<string> =>\n childrenNamedLocal(ebook, \"subject\").flatMap((container) => {\n const description = childNamedLocal(container, \"Description\")\n const memberOf =\n description !== undefined\n ? attributeNamedLocal(\n childNamedLocal(description, \"memberOf\") ?? description,\n \"resource\",\n )\n : undefined\n const value = text(\n description !== undefined\n ? childNamedLocal(description, \"value\")\n : undefined,\n )\n\n return value !== undefined && memberOf?.endsWith(\"/LCSH\") === true\n ? [value]\n : []\n })\n\nconst coverFromEbook = (\n ebook: XmlElement,\n): ProjectGutenbergCover | undefined => {\n const covers = childrenNamedLocal(ebook, \"hasFormat\").flatMap(\n (formatContainer) => {\n const file = childNamedLocal(formatContainer, \"file\")\n\n if (file === undefined) return []\n\n const uri = attributeNamedLocal(file, \"about\")\n const mediaType = childrenNamedLocal(file, \"format\")\n .map((format) => {\n const description = childNamedLocal(format, \"Description\")\n\n return text(childNamedLocal(description ?? format, \"value\"))\n })\n .find((value) => value?.startsWith(\"image/\") === true)\n\n if (\n uri === undefined ||\n mediaType === undefined ||\n !/\\.cover\\.(?:medium|small)\\.[a-z0-9]+(?:$|[?#])/i.test(uri)\n ) {\n return []\n }\n\n return [{ uri, mediaType }]\n },\n )\n\n return (\n covers.find((cover) => /\\.cover\\.medium\\./i.test(cover.uri)) ?? covers[0]\n )\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Parses one official Project Gutenberg per-eBook RDF document. Unsupported\n * XML yields `undefined`; malformed XML throws with a provider-specific label.\n */\nexport const parseProjectGutenbergRdf = (\n xml: string,\n): ProjectGutenbergRecord | undefined => {\n let document: XmlDocument\n\n try {\n document = new XmlDocument(xml)\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause)\n throw new Error(`Project Gutenberg RDF is malformed: ${message}`, { cause })\n }\n\n if (elementLocalName(document.name).toLowerCase() !== \"rdf\") {\n return undefined\n }\n\n const ebook = childNamedLocal(document, \"ebook\")\n\n if (ebook === undefined) return undefined\n\n const id = projectGutenbergIdFromEbook(ebook)\n\n if (id === undefined) return undefined\n\n const languages = [...new Set(valuesFromDescriptions(ebook, \"language\"))]\n const subjects = [...new Set(subjectValuesFromEbook(ebook))]\n const bookshelves = [...new Set(valuesFromDescriptions(ebook, \"bookshelf\"))]\n const contributors = contributorsFromEbook(ebook)\n\n return {\n id,\n title: text(childNamedLocal(ebook, \"title\")),\n publisher: text(childNamedLocal(ebook, \"publisher\")),\n issued: text(childNamedLocal(ebook, \"issued\")),\n originalPublication: text(childNamedLocal(ebook, \"marc260\")),\n rights: text(childNamedLocal(ebook, \"rights\")),\n description: text(childNamedLocal(ebook, \"description\")),\n summary: text(childNamedLocal(ebook, \"marc520\")),\n languages: emptyToUndefined(languages),\n subjects: emptyToUndefined(subjects),\n bookshelves: emptyToUndefined(bookshelves),\n contributors: emptyToUndefined(contributors),\n cover: coverFromEbook(ebook),\n }\n}\n","import type {\n MetadataIdentifier,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedCover,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedPublication,\n} from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { PROJECT_GUTENBERG_IDENTIFIER_SCHEME } from \"./identifier.ts\"\nimport type { ProjectGutenbergRecord } from \"./parse.ts\"\n\nexport const PROJECT_GUTENBERG_MAX_SUBJECTS = 25\n\nconst MARC_RELATOR_TO_ROLE: Readonly<\n Partial<Record<string, ResolvedContributorRole>>\n> = {\n art: \"artist\",\n aut: \"author\",\n clr: \"colorist\",\n ctb: \"contributor\",\n edt: \"editor\",\n ill: \"illustrator\",\n nrt: \"narrator\",\n trl: \"translator\",\n}\n\nconst resolvedContributors = (\n record: ProjectGutenbergRecord,\n): ReadonlyArray<ResolvedContributor> | undefined => {\n const contributors = new Map<string, { name: string; roles: string[] }>()\n\n for (const contributor of record.contributors ?? []) {\n const key = contributor.name.trim().toLowerCase()\n const role =\n MARC_RELATOR_TO_ROLE[contributor.role.trim().toLowerCase()] ??\n contributor.role\n const existing = contributors.get(key)\n\n if (existing === undefined) {\n contributors.set(key, { name: contributor.name, roles: [role] })\n } else if (!existing.roles.includes(role)) {\n existing.roles.push(role)\n }\n }\n\n return contributors.size > 0 ? [...contributors.values()] : undefined\n}\n\nconst resolvedIssuedDate = (\n issued: string | undefined,\n): ResolvedDate | undefined => {\n if (issued === undefined) return undefined\n\n const match = /^(\\d{4})(?:-(\\d{2})(?:-(\\d{2}))?)?$/.exec(issued.trim())\n\n if (match === null) return undefined\n\n const year = Number(match[1])\n const month = match[2] !== undefined ? Number(match[2]) : undefined\n const day = match[3] !== undefined ? Number(match[3]) : undefined\n\n if (\n !Number.isInteger(year) ||\n year <= 0 ||\n (month !== undefined && (month < 1 || month > 12)) ||\n (day !== undefined && (day < 1 || day > 31))\n ) {\n return undefined\n }\n\n return {\n year,\n ...(month !== undefined ? { month } : {}),\n ...(day !== undefined ? { day } : {}),\n }\n}\n\nconst marc260Subfields = (\n statement: string,\n code: string,\n): ReadonlyArray<string> =>\n statement\n .split(\"$\")\n .slice(1)\n .flatMap((subfield) =>\n subfield[0]?.toLowerCase() === code.toLowerCase()\n ? [subfield.slice(1).trim()]\n : [],\n )\n .filter((value) => value.length > 0)\n\nconst originalPublicationFromMarc260 = (\n statement: string | undefined,\n): ResolvedPublication | undefined => {\n if (statement === undefined) return undefined\n\n const publisherValues = marc260Subfields(statement, \"b\")\n const rawPublisher =\n publisherValues.length === 1 ? publisherValues[0] : undefined\n const publisher = rawPublisher?.replace(/[\\s,;:/]+$/u, \"\").trim()\n const dateValues = marc260Subfields(statement, \"c\")\n const years = [\n ...new Set(\n dateValues.flatMap((value) =>\n [...value.matchAll(/\\d{4}/g)].flatMap((match) =>\n match[0] !== undefined ? [Number(match[0])] : [],\n ),\n ),\n ),\n ]\n const date = years.length === 1 ? { year: years[0] } : undefined\n const details = omitUndefined({\n date,\n publisher:\n publisher !== undefined &&\n publisher.length > 0 &&\n !/^\\[?s\\.n\\.?\\]?$/i.test(publisher)\n ? publisher\n : undefined,\n })\n\n return details.date !== undefined || details.publisher !== undefined\n ? details\n : undefined\n}\n\nconst absoluteHttpUrl = (\n value: string,\n baseUrl: string,\n): string | undefined => {\n try {\n const url = new URL(value, baseUrl)\n\n return url.protocol === \"http:\" || url.protocol === \"https:\"\n ? url.toString()\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nexport const resolveProjectGutenbergRecord = (\n record: ProjectGutenbergRecord,\n options: {\n readonly baseUrl: string\n readonly matchedIdentifier: MetadataIdentifier\n },\n): ResolvedMetadata => {\n const {\n id,\n title,\n publisher,\n issued,\n originalPublication,\n rights,\n description,\n summary,\n languages,\n subjects: recordSubjects,\n bookshelves,\n contributors: _contributors,\n cover: recordCover,\n ...unhandled\n } = record\n\n // Naming every parsed field is the contract: adding one to\n // `ProjectGutenbergRecord` fails here until someone decides what becomes\n // of it. `contributors` is read by `resolvedContributors` below.\n unhandled satisfies Record<string, never>\n\n const identifiers = [\n options.matchedIdentifier,\n { value: id, scheme: PROJECT_GUTENBERG_IDENTIFIER_SCHEME },\n ].filter(\n (identifier, index, all) =>\n all.findIndex(\n (other) =>\n other.value.trim().toLowerCase() ===\n identifier.value.trim().toLowerCase() &&\n other.scheme.trim().toLowerCase() ===\n identifier.scheme.trim().toLowerCase(),\n ) === index,\n )\n const subjects = [\n ...new Set([...(recordSubjects ?? []), ...(bookshelves ?? [])]),\n ].slice(0, PROJECT_GUTENBERG_MAX_SUBJECTS)\n const coverUri =\n recordCover !== undefined\n ? absoluteHttpUrl(recordCover.uri, options.baseUrl)\n : undefined\n const cover: ResolvedCover | undefined =\n coverUri !== undefined\n ? {\n uri: coverUri,\n mediaType: recordCover?.mediaType,\n confidence: \"derived\",\n }\n : undefined\n const edition = omitUndefined({\n date: resolvedIssuedDate(issued),\n publisher,\n })\n const original = originalPublicationFromMarc260(originalPublication)\n const publication = omitUndefined({\n original,\n edition:\n edition.date !== undefined || edition.publisher !== undefined\n ? edition\n : undefined,\n })\n\n return omitUndefined({\n titles: title !== undefined ? [{ value: title }] : undefined,\n publication:\n publication.original !== undefined || publication.edition !== undefined\n ? publication\n : undefined,\n description: summary ?? description,\n rights,\n languages: emptyToUndefined([...new Set(languages ?? [])]),\n subjects: emptyToUndefined(subjects),\n contributors: resolvedContributors(record),\n identifiers,\n cover,\n })\n}\n","import type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { projectGutenbergLookupFromInput } from \"./identifier.ts\"\nimport { parseProjectGutenbergRdf } from \"./parse.ts\"\nimport { resolveProjectGutenbergRecord } from \"./resolve.ts\"\n\nexport const PROJECT_GUTENBERG_PROVIDER_ID = \"projectGutenberg\"\n\nconst DEFAULT_BASE_URL = \"https://www.gutenberg.org\"\n\nexport type ProjectGutenbergProviderOptions = {\n /** Catalog origin. Defaults to `https://www.gutenberg.org`. */\n readonly baseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /** Optional identifying user agent sent with the RDF request. */\n readonly userAgent?: string\n}\n\n/**\n * Project Gutenberg's official catalog, queried through its per-eBook RDF\n * records. The provider performs exact identifier lookups only: official\n * Gutenberg URLs and numeric `ProjectGutenberg` identifiers are accepted;\n * titles are deliberately never used to crawl or fuzzily search the site.\n */\nexport const createProjectGutenbergProvider = (\n options: ProjectGutenbergProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n\n const search = async (\n input: FetchMetadataInput,\n context: MetadataProviderContext,\n ) => {\n const lookup = projectGutenbergLookupFromInput(input)\n\n if (lookup === undefined) return []\n\n const recordUrl = new URL(\n `/cache/epub/${lookup.id}/pg${lookup.id}.rdf`,\n baseUrl,\n )\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(recordUrl.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/rdf+xml, application/xml;q=0.9\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (response.status === 404) return []\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Project Gutenberg metadata lookup failed with status ${response.status}`,\n )\n }\n\n const record = parseProjectGutenbergRdf(await response.text())\n\n if (record === undefined || record.id !== lookup.id) {\n throw new Error(\n `Project Gutenberg RDF did not contain the requested eBook ${lookup.id}`,\n )\n }\n\n const bookUrl = new URL(`/ebooks/${lookup.id}`, baseUrl).toString()\n\n return [\n {\n id: record.id,\n url: bookUrl,\n raw: record,\n metadata: resolveProjectGutenbergRecord(record, {\n baseUrl,\n matchedIdentifier: lookup.identifier,\n }),\n },\n ]\n }\n\n return {\n id: PROJECT_GUTENBERG_PROVIDER_ID,\n name: \"Project Gutenberg\",\n search,\n }\n}\n","import type { FetchMetadataInput } from \"../types/fetchMetadataInput.ts\"\n\n/**\n * Whether a catalog has anything to go on, so a lookup with nothing to ask\n * about costs no round trip. Publication details, a language, or a page count\n * narrow a search but cannot start one.\n */\nexport const hasSearchTerms = (input: FetchMetadataInput): boolean =>\n [input.title, input.series].some(\n (value) => value !== undefined && value.trim().length > 0,\n ) ||\n (input.identifiers ?? []).some(\n (identifier) => identifier.value.trim().length > 0,\n ) ||\n (input.authors ?? []).some((author) => author.trim().length > 0)\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/utils/metadataAuthors.ts","../src/utils/toIsbn13.ts","../src/match/similarity.ts","../src/match/scoreMetadataCandidate.ts","../src/providers/responseError.ts","../src/report.ts","../src/utils/omitUndefined.ts","../src/fetchMetadata.ts","../src/metadataInputFromResolvedArchive.ts","../src/utils/json.ts","../src/utils/retryWithBackoff.ts","../src/providers/googleBooks/identifier.ts","../src/providers/googleBooks/parse.ts","../src/utils/aggregateRating.ts","../src/providers/googleBooks/resolve.ts","../src/providers/googleBooks/search.ts","../src/providers/googleBooks/createGoogleBooksProvider.ts","../src/providers/projectGutenberg/identifier.ts","../src/providers/openLibrary/parse.ts","../src/providers/openLibrary/marcLanguage.ts","../src/providers/openLibrary/resolve.ts","../src/providers/openLibrary/createOpenLibraryProvider.ts","../src/providers/projectGutenberg/parse.ts","../src/providers/projectGutenberg/resolve.ts","../src/providers/projectGutenberg/createProjectGutenbergProvider.ts","../src/utils/hasSearchTerms.ts"],"sourcesContent":["import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\n\n/**\n * The names to match a publication on: those credited as `author`, else — for\n * a publication crediting nobody as one, like a comic archive listing only a\n * penciler — every contributor. Order is preserved (sources list the lead\n * creator first) and duplicates are dropped.\n */\nexport const metadataAuthors = (\n metadata: ResolvedMetadata,\n): ReadonlyArray<string> => {\n const contributors = metadata.contributors ?? []\n const authors = contributors.filter((contributor) =>\n contributor.roles.includes(\"author\"),\n )\n const names = (authors.length > 0 ? authors : contributors)\n .map((contributor) => contributor.name.trim())\n .filter((name) => name.length > 0)\n\n return [...new Set(names)]\n}\n","import { normalizeIsbn } from \"@prose-reader/archive-reader\"\n\nconst ISBN_13 = /^\\d{13}$/\nconst ISBN_10 = /^\\d{9}[\\dX]$/\n\n/**\n * Put an ISBN in its 13-digit form so the two printed on the same book compare\n * equal. A book carries an ISBN-10 in its OPF and an ISBN-13 in a catalog\n * record all the time; comparing them verbatim would read as \"different\n * books\", which — ISBN being a decisive match signal — is the most damaging\n * false negative the matcher can produce.\n *\n * Input is put through `normalizeIsbn`, the same normalization used for ISBN\n * identifiers, so the hyphenated and prefixed forms a human types\n * (`978-0-441-01359-3`, `ISBN 0-441-01359-7`) are recognized rather than\n * silently dropped.\n *\n * Returns `undefined` when nothing ISBN-shaped can be recovered (check digits\n * are not verified: a wrong one still identifies the intended book).\n */\nexport const toIsbn13 = (isbn: string): string | undefined => {\n const normalized = normalizeIsbn(isbn)\n\n if (normalized === undefined) return undefined\n if (ISBN_13.test(normalized)) return normalized\n if (!ISBN_10.test(normalized)) return undefined\n\n // ISBN-10 → ISBN-13: prefix the 978 Bookland code, drop the ISBN-10 check\n // digit, recompute the EAN-13 one (weights 1,3,1,3…).\n const core = `978${normalized.slice(0, 9)}`\n let sum = 0\n\n for (let index = 0; index < core.length; index++) {\n // charCodeAt over indexing: `noUncheckedIndexedAccess` types `core[i]` as\n // possibly undefined even though the length is known here\n const digit = core.charCodeAt(index) - 48\n\n sum += index % 2 === 0 ? digit : digit * 3\n }\n\n return `${core}${(10 - (sum % 10)) % 10}`\n}\n","const DIACRITICS = /[\\u0300-\\u036f]/g\nconst NON_ALPHANUMERIC = /[^\\p{L}\\p{N}]+/gu\n\n/** `Les Misérables` ≡ `les miserables`, `Dune: Messiah` ≡ `dune messiah`. */\nexport const normalizeForComparison = (value: string): string =>\n value\n .normalize(\"NFD\")\n .replace(DIACRITICS, \"\")\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \" \")\n .trim()\n\nconst bigramCounts = (value: string): Map<string, number> => {\n const counts = new Map<string, number>()\n\n for (let index = 0; index < value.length - 1; index++) {\n const bigram = value.slice(index, index + 2)\n\n counts.set(bigram, (counts.get(bigram) ?? 0) + 1)\n }\n\n return counts\n}\n\n/**\n * Sørensen–Dice coefficient over character bigrams, `0` to `1`.\n *\n * Bigrams rather than an edit distance: titles disagree by whole words far\n * more often than by typos. Spaces are stripped so `Spider-Man` and\n * `Spider Man` stay exact.\n */\nexport const textSimilarity = (a: string, b: string): number => {\n const left = normalizeForComparison(a).replaceAll(\" \", \"\")\n const right = normalizeForComparison(b).replaceAll(\" \", \"\")\n\n if (left.length === 0 || right.length === 0) return 0\n if (left === right) return 1\n // nothing to compare: a single character has no bigram\n if (left.length < 2 || right.length < 2) return 0\n\n const leftBigrams = bigramCounts(left)\n let shared = 0\n\n for (const [bigram, count] of bigramCounts(right)) {\n shared += Math.min(count, leftBigrams.get(bigram) ?? 0)\n }\n\n return (2 * shared) / (left.length - 1 + (right.length - 1))\n}\n\n/** Spaced dashes only, so `Spider-Man` stays one word. */\nconst SUBTITLE_SEPARATOR = /\\s*[:;]\\s*|\\s+[-–—]\\s+/\n\nconst splitTitle = (value: string): { main: string; hasSubtitle: boolean } => {\n const [head = \"\", ...rest] = value.split(SUBTITLE_SEPARATOR)\n const main = head.trim()\n const subtitle = rest.join(\" \").trim()\n\n if (main.length === 0) return { main: value, hasSubtitle: false }\n\n return { main, hasSubtitle: subtitle.length > 0 }\n}\n\n/** Below an exact match: the omitted half could have been a volume title. */\nconst SUBTITLE_REPAIR_CEILING = 0.9\n\ntype TitleDivision = \"volume\" | \"part\" | \"book\"\nconst TITLE_DIVISIONS: ReadonlyArray<TitleDivision> = [\"volume\", \"part\", \"book\"]\n\nconst TITLE_DIVISION_PATTERN =\n /\\b(volumes?|vols?|parts?|books?)\\s+([\\p{L}\\p{N}]+)\\b/gu\n\nconst TITLE_NUMBER_WORDS: Readonly<Record<string, number>> = {\n one: 1,\n first: 1,\n two: 2,\n second: 2,\n three: 3,\n third: 3,\n four: 4,\n fourth: 4,\n five: 5,\n fifth: 5,\n six: 6,\n sixth: 6,\n seven: 7,\n seventh: 7,\n eight: 8,\n eighth: 8,\n nine: 9,\n ninth: 9,\n ten: 10,\n tenth: 10,\n eleven: 11,\n eleventh: 11,\n twelve: 12,\n twelfth: 12,\n}\n\nconst ROMAN_DIGIT_VALUES: Readonly<Record<string, number>> = {\n i: 1,\n v: 5,\n x: 10,\n l: 50,\n c: 100,\n d: 500,\n m: 1000,\n}\n\nconst parseRomanNumeral = (value: string): number | undefined => {\n if (!/^m{0,3}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$/i.test(value))\n return undefined\n\n let total = 0\n let previous = 0\n\n for (const symbol of [...value.toLowerCase()].reverse()) {\n const current = ROMAN_DIGIT_VALUES[symbol]\n\n if (current === undefined) return undefined\n\n if (current < previous) total -= current\n else {\n total += current\n previous = current\n }\n }\n\n return total > 0 ? total : undefined\n}\n\nconst parseTitleDivisionNumber = (value: string): number | undefined => {\n if (/^\\d+$/.test(value)) return Number.parseInt(value, 10)\n\n return TITLE_NUMBER_WORDS[value] ?? parseRomanNumeral(value)\n}\n\nconst titleDivision = (value: string): TitleDivision | undefined => {\n if (value.startsWith(\"vol\")) return \"volume\"\n if (value.startsWith(\"part\")) return \"part\"\n if (value.startsWith(\"book\")) return \"book\"\n\n return undefined\n}\n\nconst explicitTitleDivisions = (\n value: string,\n): ReadonlyMap<TitleDivision, ReadonlySet<number>> => {\n const divisions = new Map<TitleDivision, Set<number>>()\n\n for (const match of normalizeForComparison(value).matchAll(\n TITLE_DIVISION_PATTERN,\n )) {\n const division =\n match[1] !== undefined ? titleDivision(match[1]) : undefined\n const number =\n match[2] !== undefined ? parseTitleDivisionNumber(match[2]) : undefined\n\n if (division === undefined || number === undefined) continue\n\n const numbers = divisions.get(division) ?? new Set<number>()\n numbers.add(number)\n divisions.set(division, numbers)\n }\n\n return divisions\n}\n\nconst titleDivisionsContradict = (a: string, b: string): boolean => {\n const left = explicitTitleDivisions(a)\n const right = explicitTitleDivisions(b)\n\n for (const division of TITLE_DIVISIONS) {\n const leftNumbers = left.get(division)\n const rightNumbers = right.get(division)\n\n if (leftNumbers === undefined || rightNumbers === undefined) continue\n if ([...leftNumbers].every((number) => !rightNumbers.has(number)))\n return true\n }\n\n return false\n}\n\n/**\n * Every volume, part or book number a title states explicitly, whichever\n * division word introduced it — a catalog states the same fact as a series\n * `position`, where the division word is implicit.\n */\nexport const titleDivisionNumbers = (value: string): ReadonlySet<number> => {\n const numbers = new Set<number>()\n\n for (const division of explicitTitleDivisions(value).values()) {\n for (const number of division) numbers.add(number)\n }\n\n return numbers\n}\n\n/**\n * {@link textSimilarity}, with `Dune` matched against `Dune: a novel`.\n *\n * Only when **one** side states a subtitle: that asymmetry is the same title\n * catalogued at a different depth. When both state one, both catalogers meant\n * it — `Dune: Book One` and `Dune: Messiah` are two books.\n * Explicit volume, part, or book numbers are semantic identity: when both\n * sides state different ones, the title score is `0` however similar the rest.\n */\nexport const titleSimilarity = (a: string, b: string): number => {\n if (titleDivisionsContradict(a, b)) return 0\n\n const full = textSimilarity(a, b)\n const left = splitTitle(a)\n const right = splitTitle(b)\n\n if (left.hasSubtitle === right.hasSubtitle) return full\n\n return Math.max(\n full,\n textSimilarity(left.main, right.main) * SUBTITLE_REPAIR_CEILING,\n )\n}\n\n/**\n * The best {@link titleSimilarity} over the forms one candidate title takes —\n * bare, or composed with its subtitle.\n *\n * A contradiction in **any** form still scores `0`: `Irina` agreeing with\n * `Irina: … Vol. 2` is not evidence that the candidate's own `Vol. 1` does.\n * Taking the plain maximum would let the shorter form launder a stated\n * disagreement about which volume this is.\n */\nexport const bestTitleSimilarity = (\n query: string,\n forms: ReadonlyArray<string>,\n): { readonly score: number; readonly candidate: string } | undefined => {\n let best: { score: number; candidate: string } | undefined\n\n for (const form of forms) {\n if (titleDivisionsContradict(query, form))\n return { score: 0, candidate: form }\n\n const score = titleSimilarity(query, form)\n\n if (best === undefined || score > best.score)\n best = { score, candidate: form }\n }\n\n return best\n}\n\nconst sortedTokens = (value: string): string =>\n normalizeForComparison(value)\n .split(\" \")\n .filter((token) => token.length > 0)\n .sort()\n .join(\" \")\n\n/** {@link textSimilarity} on sorted tokens: `Herbert, Frank` ≡ `Frank Herbert`. */\nexport const personNameSimilarity = (a: string, b: string): number =>\n textSimilarity(sortedTokens(a), sortedTokens(b))\n","import {\n identifierValue,\n isIsbnBearingScheme,\n type MetadataIdentifier,\n mainTitle,\n type ResolvedMetadata,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../types/fetchMetadataInput.ts\"\nimport type { MetadataMatchField, MetadataMatchSignal } from \"../types/match.ts\"\nimport { metadataAuthors } from \"../utils/metadataAuthors.ts\"\nimport { toIsbn13 } from \"../utils/toIsbn13.ts\"\nimport {\n bestTitleSimilarity,\n personNameSimilarity,\n textSimilarity,\n titleDivisionNumbers,\n} from \"./similarity.ts\"\n\n/**\n * Intent, not statistics: identifiers *are* the book, a title is what a human\n * recognizes it by, everything else corroborates. An edition detail that\n * differs must never sink an otherwise convincing match on its own.\n */\nexport const METADATA_MATCH_WEIGHTS = {\n isbn: 1,\n gtin: 1,\n identifiers: 0.6,\n title: 0.8,\n authors: 0.5,\n series: 0.3,\n publishedYear: 0.2,\n publisher: 0.15,\n languages: 0.15,\n numberOfPages: 0.15,\n} as const satisfies Record<MetadataMatchField, number>\n\n/**\n * Identity rather than description, so these settle the score outright in both\n * directions. Catalogs and books disagree on titles and page counts\n * constantly; they do not agree or disagree on an ISBN by accident.\n */\nconst DECISIVE_FIELDS: ReadonlySet<MetadataMatchField> = new Set([\n \"isbn\",\n \"gtin\",\n])\n\n/** Blank is absent — the vocabulary says so, a hand-built query may not. */\nconst stated = (value: string | undefined): string | undefined => {\n const trimmed = value?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst identifiersHaveSameValue = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n const aValue = stated(a.value)?.toLowerCase()\n\n return aValue !== undefined && aValue === stated(b.value)?.toLowerCase()\n}\n\nconst identifierScheme = (\n identifier: MetadataIdentifier,\n): string | undefined =>\n identifier.scheme.trim().toLowerCase() === \"unknown\"\n ? undefined\n : stated(identifier.scheme)?.toLowerCase()\n\nconst identifierLabel = (identifier: MetadataIdentifier): string =>\n `${identifier.scheme}:${identifier.value}`\n\n/**\n * An `Unknown` scheme is not a contradiction; a `DOI` and an `ISBN` carrying\n * the same digits are.\n */\nconst identifiersAgree = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (!identifiersHaveSameValue(a, b)) return false\n const aScheme = identifierScheme(a)\n const bScheme = identifierScheme(b)\n\n if (aScheme === undefined || bScheme === undefined) return true\n\n return aScheme === bScheme\n}\n\n/** Only a known or consumer-authored scope makes a value unambiguous identity. */\nconst identifiersAgreeDecisively = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (!identifiersHaveSameValue(a, b)) return false\n const aScheme = identifierScheme(a)\n const bScheme = identifierScheme(b)\n\n return aScheme !== undefined && aScheme === bScheme\n}\n\nconst primaryLanguageSubtag = (language: string): string =>\n language.trim().toLowerCase().split(\"-\")[0] ?? \"\"\n\n/** Reprints shift the year around, so a near one still corroborates. */\nconst publicationYearScore = (\n queryYear: number,\n candidateYear: number,\n): number => {\n const distance = Math.abs(queryYear - candidateYear)\n\n if (distance === 0) return 1\n if (distance === 1) return 0.6\n if (distance <= 3) return 0.3\n\n return 0\n}\n\n/**\n * Proximity, not equality — front matter and ads move the count: identical is\n * `1`, 25% apart `0.5`, half as long `0`.\n */\nconst numberOfPagesScore = (query: number, candidate: number): number => {\n const largest = Math.max(query, candidate)\n\n if (largest <= 0) return 0\n\n return Math.max(0, 1 - (Math.abs(query - candidate) / largest) * 2)\n}\n\nconst bestSimilarity = (\n values: ReadonlyArray<string>,\n candidates: ReadonlyArray<string>,\n compare: (a: string, b: string) => number,\n): number =>\n values.reduce(\n (best, value) =>\n candidates.reduce(\n (innerBest, candidate) =>\n Math.max(innerBest, compare(value, candidate)),\n best,\n ),\n 0,\n )\n\ntype ComparedValues = {\n readonly score: number\n readonly query: string\n readonly candidate: string\n}\n\nconst bestStringComparison = (\n rawQueryValue: string | undefined,\n rawCandidateValues: ReadonlyArray<string | undefined>,\n compare: (a: string, b: string) => number = textSimilarity,\n): ComparedValues | undefined => {\n const query = stated(rawQueryValue)\n\n if (query === undefined) return undefined\n\n let best: ComparedValues | undefined\n\n for (const rawCandidate of rawCandidateValues) {\n const candidate = stated(rawCandidate)\n\n if (candidate === undefined) continue\n\n const comparison = { score: compare(query, candidate), query, candidate }\n\n if (best === undefined || comparison.score > best.score) best = comparison\n }\n\n return best\n}\n\n/**\n * A comparison string, not a value: a book states `Dune: A Novel` in one\n * `dc:title` where a catalog states a title and a subtitle, and the query\n * side has whichever spelling its source used. Composing the two for the\n * comparison alone keeps that asymmetry from costing score, while the\n * candidate's own metadata stays exactly as the catalog stated it.\n */\nconst subtitledTitle = (candidate: ResolvedMetadata): string | undefined => {\n const main = mainTitle(candidate)\n const subtitle = candidate.titles?.find(\n (title) => title.type === \"subtitle\",\n )?.value\n\n return main !== undefined && subtitle !== undefined\n ? `${main}: ${subtitle}`\n : undefined\n}\n\n/**\n * A book says `BLAME! Vol. 2` in its title where a catalog says `BLAME!` with\n * a series `position` of 2 — the same fact in two shapes. Comparing the\n * shapes against each other keeps a numbered query from accepting the wrong\n * volume of the right series, which the title comparison alone cannot see\n * once the number lives in structured data instead of the title string.\n */\nconst seriesPositionContradicts = (\n queryTitle: string,\n candidate: ResolvedMetadata,\n): boolean => {\n const queryNumbers = titleDivisionNumbers(queryTitle)\n\n if (queryNumbers.size === 0) return false\n\n const positions = (candidate.belongsTo?.series ?? []).flatMap((series) =>\n series.position !== undefined ? [series.position] : [],\n )\n\n return (\n positions.length > 0 &&\n positions.every((position) => !queryNumbers.has(position))\n )\n}\n\nconst bestPublicationYearComparison = (\n query: number | undefined,\n candidates: ReadonlyArray<number | undefined>,\n): ComparedValues | undefined => {\n if (query === undefined) return undefined\n\n let best: ComparedValues | undefined\n\n for (const candidate of candidates) {\n if (candidate === undefined) continue\n\n const comparison = {\n score: publicationYearScore(query, candidate),\n query: String(query),\n candidate: String(candidate),\n }\n\n if (best === undefined || comparison.score > best.score) best = comparison\n }\n\n return best\n}\n\nexport type ScoredMetadataCandidate = {\n readonly score: number\n readonly signals: ReadonlyArray<MetadataMatchSignal>\n}\n\n/**\n * How well a candidate agrees with the query, field by field, and why. Every\n * provider is scored by these rules, so their scores stay comparable:\n *\n * - **Only comparable fields count** — a field is compared when both sides\n * state it, so what the book doesn't know can neither raise nor lower a\n * candidate, and nothing in common scores `0`.\n * - **The aggregate is a weighted average** ({@link METADATA_MATCH_WEIGHTS}),\n * putting a rich query and a sparse one on the same scale.\n * - **Confirmed identity settles it** — an agreeing ISBN, GTIN, or\n * scheme-scoped identifier pins the score to `1`. A contradictory ISBN or\n * GTIN pins it to `0`; two disjoint provider-specific identifier lists are\n * merely unrelated.\n */\nexport const scoreMetadataCandidate = (\n query: FetchMetadataInput,\n candidate: ResolvedMetadata,\n): ScoredMetadataCandidate => {\n const signals: MetadataMatchSignal[] = []\n\n const addSignal = (\n field: MetadataMatchField,\n values: ComparedValues | undefined,\n ) => {\n if (values === undefined) return\n\n signals.push({\n field,\n weight: METADATA_MATCH_WEIGHTS[field],\n score: Math.min(1, Math.max(0, values.score)),\n query: values.query,\n candidate: values.candidate,\n })\n }\n\n const compareStrings = (\n field: MetadataMatchField,\n rawQueryValue: string | undefined,\n rawCandidateValue: string | undefined,\n compare: (a: string, b: string) => number = textSimilarity,\n ) => {\n const queryValue = stated(rawQueryValue)\n const candidateValue = stated(rawCandidateValue)\n\n if (queryValue === undefined || candidateValue === undefined) return\n\n addSignal(field, {\n score: compare(queryValue, candidateValue),\n query: queryValue,\n candidate: candidateValue,\n })\n }\n\n const queryIsbn = identifierValue(query.identifiers, \"ISBN\")\n const candidateIsbn = identifierValue(candidate.identifiers, \"ISBN\")\n const queryIsbn13 = queryIsbn !== undefined ? toIsbn13(queryIsbn) : undefined\n const candidateIsbn13 =\n candidateIsbn !== undefined ? toIsbn13(candidateIsbn) : undefined\n\n const comparedIsbn =\n queryIsbn !== undefined &&\n candidateIsbn !== undefined &&\n queryIsbn13 !== undefined &&\n candidateIsbn13 !== undefined\n\n if (comparedIsbn) {\n addSignal(\"isbn\", {\n score: queryIsbn13 === candidateIsbn13 ? 1 : 0,\n query: queryIsbn,\n candidate: candidateIsbn,\n })\n }\n\n const queryGtin = identifierValue(query.identifiers, \"GTIN\")\n const candidateGtin = identifierValue(candidate.identifiers, \"GTIN\")\n\n if (!comparedIsbn && queryGtin !== undefined && candidateGtin !== undefined) {\n addSignal(\"gtin\", {\n score: queryGtin === candidateGtin ? 1 : 0,\n query: queryGtin,\n candidate: candidateGtin,\n })\n }\n\n // ISBN and GTIN have normalized comparisons above. Comparing their raw\n // spellings again would emit a false generic mismatch for hyphenated ISBNs.\n const queryIdentifiers = (query.identifiers ?? []).filter(\n (identifier) => !isIsbnBearingScheme(identifier.scheme),\n )\n const candidateIdentifiers = (candidate.identifiers ?? []).filter(\n (identifier) => !isIsbnBearingScheme(identifier.scheme),\n )\n const decisiveIdentifierAgreement = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) =>\n identifiersAgreeDecisively(identifier, other),\n ),\n )\n\n if (queryIdentifiers.length > 0 && candidateIdentifiers.length > 0) {\n const shared = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) => identifiersAgree(identifier, other)),\n )\n\n addSignal(\"identifiers\", {\n score: shared ? 1 : 0,\n query: queryIdentifiers.map(identifierLabel).join(\", \"),\n candidate: candidateIdentifiers.map(identifierLabel).join(\", \"),\n })\n }\n\n const queryTitle = stated(query.title)\n const candidateTitleForms = [\n mainTitle(candidate),\n subtitledTitle(candidate),\n ].flatMap((form) => {\n const value = stated(form)\n\n return value !== undefined ? [value] : []\n })\n\n if (queryTitle !== undefined && candidateTitleForms.length > 0) {\n const compared = bestTitleSimilarity(queryTitle, candidateTitleForms)\n\n if (compared !== undefined) {\n addSignal(\"title\", {\n score: seriesPositionContradicts(queryTitle, candidate)\n ? 0\n : compared.score,\n query: queryTitle,\n candidate: compared.candidate,\n })\n }\n }\n addSignal(\n \"publisher\",\n bestStringComparison(query.publisher, [\n candidate.publication?.original?.publisher,\n candidate.publication?.edition?.publisher,\n ]),\n )\n compareStrings(\"series\", query.series, candidate.belongsTo?.series?.[0]?.name)\n\n const candidateAuthors = metadataAuthors(candidate)\n const queryAuthors = (query.authors ?? []).flatMap((author) => {\n const name = stated(author)\n\n return name !== undefined ? [name] : []\n })\n\n if (queryAuthors.length > 0 && candidateAuthors.length > 0) {\n addSignal(\"authors\", {\n score: bestSimilarity(\n queryAuthors,\n candidateAuthors,\n personNameSimilarity,\n ),\n query: queryAuthors.join(\", \"),\n candidate: candidateAuthors.join(\", \"),\n })\n }\n\n addSignal(\n \"publishedYear\",\n bestPublicationYearComparison(query.publishedYear, [\n candidate.publication?.original?.date?.year,\n candidate.publication?.edition?.date?.year,\n ]),\n )\n\n const queryLanguages = query.languages ?? []\n const candidateLanguages = candidate.languages ?? []\n\n if (queryLanguages.length > 0 && candidateLanguages.length > 0) {\n const candidateSubtags = new Set(\n candidateLanguages.map(primaryLanguageSubtag),\n )\n\n addSignal(\"languages\", {\n score: queryLanguages.some((language) =>\n candidateSubtags.has(primaryLanguageSubtag(language)),\n )\n ? 1\n : 0,\n query: queryLanguages.join(\", \"),\n candidate: candidateLanguages.join(\", \"),\n })\n }\n\n if (\n query.numberOfPages !== undefined &&\n candidate.numberOfPages !== undefined\n ) {\n addSignal(\"numberOfPages\", {\n score: numberOfPagesScore(query.numberOfPages, candidate.numberOfPages),\n query: String(query.numberOfPages),\n candidate: String(candidate.numberOfPages),\n })\n }\n\n const decisive = signals.filter((signal) => DECISIVE_FIELDS.has(signal.field))\n\n // Contradiction wins over agreement: refusing a publication whose stated\n // identity disagrees is the recoverable mistake. Without this the average is\n // merciful to exactly the wrong candidate — a different edition agreeing on\n // title and author scores (0.8 + 0.5) / 2.3 ≈ 0.57, accepted.\n if (decisive.some((signal) => signal.score === 0))\n return { score: 0, signals }\n if (\n decisiveIdentifierAgreement ||\n decisive.some((signal) => signal.score === 1)\n )\n return { score: 1, signals }\n\n const totalWeight = signals.reduce(\n (total, signal) => total + signal.weight,\n 0,\n )\n\n if (totalWeight === 0) return { score: 0, signals }\n\n const weighted = signals.reduce(\n (total, signal) => total + signal.score * signal.weight,\n 0,\n )\n\n return { score: weighted / totalWeight, signals }\n}\n","/**\n * Thrown by a provider when a catalog answered with a failing status. Carrying\n * it is what lets `fetchMetadata` report *why* a provider dropped out: `429`\n * is \"ask again later\", `503` is \"the catalog is down\", and a consumer decides\n * differently for each.\n *\n * ```ts\n * if (!response.ok) {\n * throw new MetadataProviderResponseError(response.status, \"My Catalog search failed\")\n * }\n * ```\n */\nexport class MetadataProviderResponseError extends Error {\n readonly status: number\n\n constructor(status: number, message?: string) {\n super(message ?? `Provider responded with status ${status}`)\n\n this.name = \"MetadataProviderResponseError\"\n this.status = status\n }\n}\n\n/**\n * The HTTP status behind a thrown error, when there is one.\n *\n * Structural rather than `instanceof`, so a provider built on another HTTP\n * client works unadapted. No status means it was never a response — network,\n * parse, bug — which is itself the answer.\n */\nexport const responseErrorStatus = (error: unknown): number | undefined => {\n if (typeof error !== \"object\" || error === null) return undefined\n if (!(\"status\" in error)) return undefined\n\n const { status } = error\n\n if (typeof status !== \"number\" || !Number.isInteger(status)) return undefined\n\n // an unrelated `status` field means something else entirely\n return status >= 100 && status <= 599 ? status : undefined\n}\n","import { Report as SharedReport } from \"@prose-reader/shared\"\n\n// Spelled out rather than read from package.json: node runs this source\n// directly in development, where a JSON import needs an import attribute.\nconst NAME = \"@prose-reader/metadata-fetcher\"\n\n// No explicit `enabled`: inherit the shared root, which auto-configures from\n// the `globalThis.__PROSE_READER_DEBUG` flag.\nexport const Report = SharedReport.namespace(NAME, undefined, {\n color: \"#b06642\",\n})\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * results stay sparse (an absent key and an `undefined` value are the same\n * statement: \"nothing was found for this field\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","import { scoreMetadataCandidate } from \"./match/scoreMetadataCandidate.ts\"\nimport { responseErrorStatus } from \"./providers/responseError.ts\"\nimport { Report } from \"./report.ts\"\nimport type {\n FailedMetadataProvider,\n FetchedMetadata,\n FetchedMetadataSource,\n} from \"./types/fetchedMetadata.ts\"\nimport type { FetchMetadataInput } from \"./types/fetchMetadataInput.ts\"\nimport type { MetadataMatch } from \"./types/match.ts\"\nimport type { MetadataCandidate, MetadataProvider } from \"./types/provider.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\nexport type FetchMetadataOptions = {\n /**\n * The catalogs to ask, queried concurrently. Their order is the tie-break\n * precedence: equally-scored matches rank in the order declared here.\n */\n readonly providers: ReadonlyArray<MetadataProvider>\n /**\n * Hard cap on the matches kept **per provider**, best first; also the\n * page-size hint each provider receives. Defaults to `5`.\n */\n readonly limit?: number\n /**\n * Score a match must reach to be `accepted`. Defaults to `0.5`. Below it,\n * matches are still returned and ranked, for a consumer to offer as \"did\n * you mean?\".\n */\n readonly minScore?: number\n /**\n * Keep each provider's own record on `match.raw`. Defaults to `false`: it is\n * provenance most consumers don't need, and can dwarf what they persist.\n */\n readonly includeRaw?: boolean\n readonly signal?: AbortSignal\n}\n\nconst DEFAULT_LIMIT = 5\nconst DEFAULT_MIN_SCORE = 0.5\nconst FETCHED_METADATA_VERSION = 5\n\nconst toMatch = ({\n candidate,\n providerId,\n score,\n signals,\n accepted,\n includeRaw,\n}: {\n candidate: MetadataCandidate\n providerId: string\n score: number\n signals: MetadataMatch[\"signals\"]\n accepted: boolean\n includeRaw: boolean\n}): MetadataMatch =>\n omitUndefined({\n providerId,\n score,\n signals,\n accepted,\n metadata: candidate.metadata,\n id: candidate.id,\n url: candidate.url,\n raw: includeRaw ? candidate.raw : undefined,\n })\n\n/**\n * Asks every provider what it knows about a book, scores the answers against\n * what you gave it, and returns the ranked candidates with the evidence behind\n * each score. The input contains only fields understood for lookup and\n * matching; candidates retain archive-reader's rich `ResolvedMetadata`\n * vocabulary, including catalog-only covers, descriptions and subjects.\n *\n * ```ts\n * import { resolveArchive } from \"@prose-reader/archive-reader\"\n * import {\n * createGoogleBooksProvider,\n * createOpenLibraryProvider,\n * createProjectGutenbergProvider,\n * fetchMetadata,\n * metadataInputFromResolvedArchive,\n * } from \"@prose-reader/metadata-fetcher\"\n *\n * const resolved = await resolveArchive(archive)\n * const fetched = await fetchMetadata(metadataInputFromResolvedArchive(resolved), {\n * providers: [\n * createProjectGutenbergProvider(),\n * createGoogleBooksProvider({ apiKey: \"your-api-key\" }),\n * createOpenLibraryProvider(),\n * ],\n * })\n *\n * // what the best accepted candidate says, and why we believe it\n * const match = fetched.matches.find(({ accepted }) => accepted)\n * match?.metadata.cover?.uri\n * match?.signals // [{ field: \"isbn\", score: 1, … }, …]\n * ```\n *\n * Error policy mirrors `resolveArchive`: a provider that throws never fails\n * the fetch — it is logged through the debug `Report` and listed in\n * `failedProviders`. An abort is the exception: cancelling through `signal`\n * rejects, since the caller asked for it.\n */\nexport const fetchMetadata = async (\n input: FetchMetadataInput,\n options: FetchMetadataOptions,\n): Promise<FetchedMetadata> => {\n const limit = options.limit ?? DEFAULT_LIMIT\n const minScore = options.minScore ?? DEFAULT_MIN_SCORE\n const includeRaw = options.includeRaw === true\n\n const results = await Promise.all(\n options.providers.map(async (provider) => {\n try {\n const candidates = await provider.search(input, {\n limit,\n signal: options.signal,\n })\n\n return { provider, candidates, failure: undefined }\n } catch (error) {\n // the caller pulled the plug: their cancellation, their rejection\n if (options.signal?.aborted === true) throw error\n\n // catalogs in the wild are flaky: a failing provider never fails the\n // fetch, it just doesn't contribute. The status, when the catalog\n // answered with one, is what tells a rate limit from an outage.\n Report.error(`fetchMetadata: provider \"${provider.id}\" failed`, error)\n\n return {\n provider,\n candidates: undefined,\n failure: omitUndefined({\n id: provider.id,\n status: responseErrorStatus(error),\n }),\n }\n }\n }),\n )\n\n const failedProviders: FailedMetadataProvider[] = results.flatMap((result) =>\n result.failure !== undefined ? [result.failure] : [],\n )\n\n const sources: Record<string, FetchedMetadataSource> = {}\n const matches: MetadataMatch[] = []\n\n for (const { provider, candidates } of results) {\n if (candidates === undefined) continue\n\n const providerMatches = candidates\n .map((candidate) => {\n const { score, signals } = scoreMetadataCandidate(\n input,\n candidate.metadata,\n )\n\n return toMatch({\n candidate,\n providerId: provider.id,\n score,\n signals,\n accepted: score >= minScore,\n includeRaw,\n })\n })\n // `sort` is stable, so equally-scored candidates keep the order the\n // provider ranked them in\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n\n // ids key `sources`, so a duplicate would silently drop a provider's\n // detail — the kind of thing that costs an hour to notice\n if (sources[provider.id] !== undefined) {\n Report.warn(\n `fetchMetadata: several providers share the id \"${provider.id}\"`,\n )\n }\n\n sources[provider.id] = {\n provider: { id: provider.id, name: provider.name },\n matches: providerMatches,\n }\n\n matches.push(...providerMatches)\n }\n\n // again stable: providers were walked in declaration order, so that order\n // breaks ties between equally convincing catalogs\n const rankedMatches = [...matches].sort((a, b) => b.score - a.score)\n\n return {\n version: FETCHED_METADATA_VERSION,\n matches: rankedMatches,\n sources,\n failedProviders,\n }\n}\n","import { mainTitle, type ResolvedArchive } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"./types/fetchMetadataInput.ts\"\nimport { metadataAuthors } from \"./utils/metadataAuthors.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * Projects archive-reader's rich metadata entity into the fields understood by\n * metadata lookup and matching. Edition publication details take precedence\n * because embedded publication metadata normally describes that concrete\n * edition; original-publication details are the fallback.\n */\nexport const metadataInputFromResolvedArchive = (\n resolved: Pick<ResolvedArchive, \"metadata\">,\n): FetchMetadataInput => {\n const { metadata } = resolved\n const authors = metadataAuthors(metadata)\n const identifiers = metadata.identifiers?.map(({ value, scheme }) => ({\n value,\n scheme,\n }))\n\n return omitUndefined({\n title: mainTitle(metadata),\n authors: authors.length > 0 ? authors : undefined,\n identifiers:\n identifiers !== undefined && identifiers.length > 0\n ? identifiers\n : undefined,\n series: metadata.belongsTo?.series?.[0]?.name,\n publisher:\n metadata.publication?.edition?.publisher ??\n metadata.publication?.original?.publisher,\n publishedYear:\n metadata.publication?.edition?.date?.year ??\n metadata.publication?.original?.date?.year,\n languages: metadata.languages,\n numberOfPages: metadata.numberOfPages,\n })\n}\n","/**\n * Readers for `JSON.parse` output. A catalog's payload is third-party data, so\n * it enters as `unknown` and every field goes through a guard: a renamed or\n * nulled field reads as absent instead of crashing the fetch, or producing a\n * candidate with `undefined` where a string was promised.\n */\nexport const isJsonRecord = (\n value: unknown,\n): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nexport const readString = (\n record: Record<string, unknown>,\n key: string,\n): string | undefined => {\n const value = record[key]\n\n if (typeof value !== \"string\") return undefined\n\n const trimmed = value.trim()\n\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport const readNumber = (\n record: Record<string, unknown>,\n key: string,\n): number | undefined => {\n const value = record[key]\n\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined\n}\n\nexport const readStringArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<string> | undefined => {\n const value = record[key]\n\n if (!Array.isArray(value)) return undefined\n\n const strings = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n\n return strings.length > 0 ? strings : undefined\n}\n\nexport const readRecordArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<Record<string, unknown>> => {\n const value = record[key]\n\n if (!Array.isArray(value)) return []\n\n return value.filter(isJsonRecord)\n}\n","export type RetryWithBackoffOptions = {\n /** Total calls, including the initial one. Must be a positive integer. */\n readonly attempts: number\n /** Delay before the second call. Later delays double. */\n readonly initialDelayMs: number\n /** Upper bound for one delay, including jitter. */\n readonly maxDelayMs?: number\n /** Adds up to one base delay of randomness. Defaults to `true`. */\n readonly jitter?: boolean\n /** Decides whether a failed call is safe and useful to repeat. */\n readonly shouldRetry: (error: unknown, attempt: number) => boolean\n /** Stops future attempts and cancels a wait between attempts. */\n readonly signal?: AbortSignal\n}\n\nconst abortReason = (signal: AbortSignal): unknown =>\n signal.reason ?? new DOMException(\"The operation was aborted\", \"AbortError\")\n\nconst throwIfAborted = (signal: AbortSignal | undefined): void => {\n if (signal?.aborted === true) throw abortReason(signal)\n}\n\nconst wait = (\n delayMs: number,\n signal: AbortSignal | undefined,\n): Promise<void> => {\n throwIfAborted(signal)\n\n return new Promise((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timeout)\n reject(signal !== undefined ? abortReason(signal) : undefined)\n }\n const timeout = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort)\n resolve()\n }, delayMs)\n\n signal?.addEventListener(\"abort\", onAbort, { once: true })\n })\n}\n\nconst validateOptions = (options: RetryWithBackoffOptions): void => {\n if (!Number.isInteger(options.attempts) || options.attempts < 1) {\n throw new RangeError(\"attempts must be a positive integer\")\n }\n\n if (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs < 0) {\n throw new RangeError(\"initialDelayMs must be a non-negative finite number\")\n }\n\n if (\n options.maxDelayMs !== undefined &&\n (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < 0)\n ) {\n throw new RangeError(\"maxDelayMs must be a non-negative finite number\")\n }\n}\n\nconst retryDelay = (\n failedAttempt: number,\n options: RetryWithBackoffOptions,\n): number => {\n const exponentialDelay = options.initialDelayMs * 2 ** (failedAttempt - 1)\n const jitteredDelay =\n options.jitter === false\n ? exponentialDelay\n : exponentialDelay * (1 + Math.random())\n\n return Math.min(jitteredDelay, options.maxDelayMs ?? Number.MAX_SAFE_INTEGER)\n}\n\n/**\n * Repeats a failing asynchronous operation with abortable exponential backoff.\n * `attempts` is the total call count, not the number of retries. The original\n * error is rethrown when it is not retryable or the final attempt fails.\n */\nexport const retryWithBackoff = async <Value>(\n operation: (attempt: number) => Promise<Value>,\n options: RetryWithBackoffOptions,\n): Promise<Value> => {\n validateOptions(options)\n\n for (let attempt = 1; attempt <= options.attempts; attempt += 1) {\n throwIfAborted(options.signal)\n\n try {\n return await operation(attempt)\n } catch (error) {\n if (\n attempt === options.attempts ||\n options.signal?.aborted === true ||\n !options.shouldRetry(error, attempt)\n ) {\n throw error\n }\n\n await wait(retryDelay(attempt, options), options.signal)\n }\n }\n\n throw new Error(\"retryWithBackoff reached an invalid state\")\n}\n","import {\n identifierValue,\n type KnownMetadataIdentifierScheme,\n type MetadataIdentifier,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\n\nexport const GOOGLE_BOOKS_IDENTIFIER_SCHEME =\n \"GoogleBooks\" satisfies KnownMetadataIdentifierScheme\n\nexport type GoogleBooksLookup = {\n readonly id: string\n /** The book's exact identifier spelling, echoed only after Google confirms it. */\n readonly identifier: MetadataIdentifier\n}\n\n/**\n * Recognizes an authored Google Books volume id and the official Google Books\n * website/API URL forms. Arbitrary scheme-less strings are not reinterpreted\n * as ids: callers with a raw id should label it `GoogleBooks`.\n */\nexport const googleBooksLookupFromInput = (\n input: FetchMetadataInput,\n): GoogleBooksLookup | undefined => {\n for (const identifier of input.identifiers ?? []) {\n const id = identifierValue([identifier], GOOGLE_BOOKS_IDENTIFIER_SCHEME)\n\n if (id !== undefined) {\n return {\n id,\n identifier: { value: identifier.value, scheme: identifier.scheme },\n }\n }\n }\n\n return undefined\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\n\nexport type GoogleBooksIndustryIdentifier = {\n readonly type?: string\n readonly identifier: string\n}\n\nexport type GoogleBooksImageLinks = {\n readonly smallThumbnail?: string\n readonly thumbnail?: string\n readonly small?: string\n readonly medium?: string\n readonly large?: string\n readonly extraLarge?: string\n}\n\nexport type GoogleBooksVolumeSeries = {\n readonly seriesId?: string\n readonly orderNumber?: number\n}\n\n/**\n * Only the volume's place in its series is retained. `bookDisplayNumber` and\n * `shortSeriesBookTitle` are display strings by Google's own definition — the\n * sequence is `orderNumber` — and the payload never states the series name,\n * only an id, so a name would have to be invented.\n */\nexport type GoogleBooksSeriesInfo = {\n readonly volumeSeries?: ReadonlyArray<GoogleBooksVolumeSeries>\n}\n\n/**\n * The subset of a Google Books `volumeInfo` record that has a normalized home.\n * Every field remains optional because the public catalog is sparse in practice.\n */\nexport type GoogleBooksVolumeInfo = {\n readonly title?: string\n readonly subtitle?: string\n readonly authors?: ReadonlyArray<string>\n readonly publisher?: string\n readonly publishedDate?: string\n readonly description?: string\n readonly industryIdentifiers?: ReadonlyArray<GoogleBooksIndustryIdentifier>\n readonly pageCount?: number\n readonly categories?: ReadonlyArray<string>\n /** Mean review rating; Google states it on a 1–5 scale, rounded to a half. */\n readonly averageRating?: number\n readonly ratingsCount?: number\n readonly language?: string\n readonly imageLinks?: GoogleBooksImageLinks\n readonly infoLink?: string\n readonly canonicalVolumeLink?: string\n readonly seriesInfo?: GoogleBooksSeriesInfo\n}\n\n/** One Google Books volume, retaining the source's nested field names. */\nexport type GoogleBooksVolume = {\n readonly id?: string\n readonly volumeInfo?: GoogleBooksVolumeInfo\n}\n\nconst readRecord = (\n record: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined => {\n const value = record[key]\n\n return isJsonRecord(value) ? value : undefined\n}\n\nconst parseIndustryIdentifiers = (\n volumeInfo: Record<string, unknown>,\n): ReadonlyArray<GoogleBooksIndustryIdentifier> | undefined => {\n const identifiers = readRecordArray(\n volumeInfo,\n \"industryIdentifiers\",\n ).flatMap((record) => {\n const identifier = readString(record, \"identifier\")\n\n return identifier !== undefined\n ? [\n omitUndefined({\n type: readString(record, \"type\"),\n identifier,\n }),\n ]\n : []\n })\n\n return identifiers.length > 0 ? identifiers : undefined\n}\n\nconst parseImageLinks = (\n volumeInfo: Record<string, unknown>,\n): GoogleBooksImageLinks | undefined => {\n const imageLinks = readRecord(volumeInfo, \"imageLinks\")\n\n if (imageLinks === undefined) return undefined\n\n const parsed = omitUndefined({\n smallThumbnail: readString(imageLinks, \"smallThumbnail\"),\n thumbnail: readString(imageLinks, \"thumbnail\"),\n small: readString(imageLinks, \"small\"),\n medium: readString(imageLinks, \"medium\"),\n large: readString(imageLinks, \"large\"),\n extraLarge: readString(imageLinks, \"extraLarge\"),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\nconst parseSeriesInfo = (\n volumeInfo: Record<string, unknown>,\n): GoogleBooksSeriesInfo | undefined => {\n const seriesInfo = readRecord(volumeInfo, \"seriesInfo\")\n\n if (seriesInfo === undefined) return undefined\n\n const volumeSeries = readRecordArray(seriesInfo, \"volumeSeries\").flatMap(\n (record) => {\n const parsed = omitUndefined({\n seriesId: readString(record, \"seriesId\"),\n orderNumber: readNumber(record, \"orderNumber\"),\n })\n\n return Object.keys(parsed).length > 0 ? [parsed] : []\n },\n )\n\n return volumeSeries.length > 0 ? { volumeSeries } : undefined\n}\n\nconst parseVolumeInfo = (\n record: Record<string, unknown>,\n): GoogleBooksVolumeInfo | undefined => {\n const volumeInfo = readRecord(record, \"volumeInfo\")\n\n if (volumeInfo === undefined) return undefined\n\n const parsed = omitUndefined({\n title: readString(volumeInfo, \"title\"),\n subtitle: readString(volumeInfo, \"subtitle\"),\n authors: readStringArray(volumeInfo, \"authors\"),\n publisher: readString(volumeInfo, \"publisher\"),\n publishedDate: readString(volumeInfo, \"publishedDate\"),\n description: readString(volumeInfo, \"description\"),\n industryIdentifiers: parseIndustryIdentifiers(volumeInfo),\n pageCount: readNumber(volumeInfo, \"pageCount\"),\n categories: readStringArray(volumeInfo, \"categories\"),\n averageRating: readNumber(volumeInfo, \"averageRating\"),\n ratingsCount: readNumber(volumeInfo, \"ratingsCount\"),\n language: readString(volumeInfo, \"language\"),\n imageLinks: parseImageLinks(volumeInfo),\n infoLink: readString(volumeInfo, \"infoLink\"),\n canonicalVolumeLink: readString(volumeInfo, \"canonicalVolumeLink\"),\n seriesInfo: parseSeriesInfo(volumeInfo),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\n/** Reads one public Google Books Volume resource from untrusted JSON. */\nexport const parseGoogleBooksVolume = (\n payload: unknown,\n): GoogleBooksVolume | undefined => {\n if (!isJsonRecord(payload)) return undefined\n\n const parsed = omitUndefined({\n id: readString(payload, \"id\"),\n volumeInfo: parseVolumeInfo(payload),\n })\n\n return Object.keys(parsed).length > 0 ? parsed : undefined\n}\n\n/** Reads the `items` array of a Google Books volumes search response. */\nexport const parseGoogleBooksVolumesResponse = (\n payload: unknown,\n): ReadonlyArray<GoogleBooksVolume> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"items\").flatMap((record) => {\n const volume = parseGoogleBooksVolume(record)\n\n return volume !== undefined ? [volume] : []\n })\n}\n","import type { ResolvedAggregateRating } from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"./omitUndefined.ts\"\n\nconst MAX_VALUE = 5\n\n/**\n * Builds the cross-format aggregate rating from a catalog's mean and count.\n * Both Google Books and Open Library state the mean on a 1–5 star scale, which\n * is the scale {@link ResolvedAggregateRating} promises, so neither needs\n * rescaling — a source on another scale divides before calling this.\n *\n * A mean outside 0–5, a count that isn't a non-negative integer, and a mean\n * announced over zero ratings are all dropped: a catalog contradicting itself\n * would otherwise render as a real star count.\n */\nexport const resolvedAggregateRating = (\n mean: number | undefined,\n count: number | undefined,\n): ResolvedAggregateRating | undefined => {\n if (mean === undefined || mean < 0 || mean > MAX_VALUE) return undefined\n\n const ratings =\n count !== undefined && Number.isInteger(count) && count >= 0\n ? count\n : undefined\n\n if (ratings === 0) return undefined\n\n return omitUndefined({ value: mean, count: ratings })\n}\n","import {\n type MetadataIdentifier,\n parseW3cDtfDate,\n type ResolvedCollection,\n type ResolvedContributor,\n type ResolvedCover,\n type ResolvedMetadata,\n type ResolvedTitle,\n} from \"@prose-reader/archive-reader\"\nimport { resolvedAggregateRating } from \"../../utils/aggregateRating.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { GOOGLE_BOOKS_IDENTIFIER_SCHEME } from \"./identifier.ts\"\nimport type {\n GoogleBooksImageLinks,\n GoogleBooksIndustryIdentifier,\n GoogleBooksSeriesInfo,\n GoogleBooksVolume,\n} from \"./parse.ts\"\n\nexport const GOOGLE_BOOKS_MAX_SUBJECTS = 25\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nconst resolvedIdentifierScheme = (type: string | undefined): string =>\n type === \"ISBN_10\" || type === \"ISBN_13\" ? \"ISBN\" : (type ?? \"Unknown\")\n\nconst dedupeIdentifiers = (\n identifiers: ReadonlyArray<MetadataIdentifier>,\n): ReadonlyArray<MetadataIdentifier> | undefined =>\n emptyToUndefined(\n identifiers.filter(\n (identifier, index, all) =>\n all.findIndex(\n (other) =>\n other.value.trim().toLowerCase() ===\n identifier.value.trim().toLowerCase() &&\n other.scheme.trim().toLowerCase() ===\n identifier.scheme.trim().toLowerCase(),\n ) === index,\n ),\n )\n\nconst resolvedIndustryIdentifiers = (\n values: ReadonlyArray<GoogleBooksIndustryIdentifier>,\n): ReadonlyArray<MetadataIdentifier> =>\n values.map((identifier) =>\n omitUndefined({\n value: identifier.identifier,\n scheme: resolvedIdentifierScheme(identifier.type),\n }),\n )\n\n/**\n * Google states the title and the subtitle as separate fields, so they stay\n * separate: composing them for display is the consumer's decision.\n */\nconst resolvedTitles = (\n title: string | undefined,\n subtitle: string | undefined,\n): ReadonlyArray<ResolvedTitle> | undefined =>\n emptyToUndefined([\n ...(title !== undefined ? [{ value: title }] : []),\n ...(subtitle !== undefined\n ? [{ value: subtitle, type: \"subtitle\" as const }]\n : []),\n ])\n\n/**\n * The volume's place in its series, which Google identifies by id alone — it\n * never states the series name, and inventing one would be fabrication.\n */\nconst resolvedSeries = (\n seriesInfo: GoogleBooksSeriesInfo | undefined,\n): ReadonlyArray<ResolvedCollection> | undefined =>\n emptyToUndefined(\n (seriesInfo?.volumeSeries ?? []).flatMap<ResolvedCollection>((series) => {\n const entry = omitUndefined({\n identifiers:\n series.seriesId !== undefined\n ? [\n {\n value: series.seriesId,\n scheme: GOOGLE_BOOKS_IDENTIFIER_SCHEME,\n },\n ]\n : undefined,\n position: series.orderNumber,\n })\n\n return Object.keys(entry).length > 0 ? [entry] : []\n }),\n )\n\nconst absoluteHttpUrl = (value: string | undefined): string | undefined => {\n if (value === undefined) return undefined\n\n try {\n const url = new URL(value)\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined\n\n if (url.protocol === \"http:\") url.protocol = \"https:\"\n\n return url.toString()\n } catch {\n return undefined\n }\n}\n\n/**\n * Prefer the largest cover Google announced and force HTTPS. Preserve the\n * query exactly: changing Google's `zoom`, `edge` or signed `imgtk` parameters\n * can turn a valid cover into its \"image not available\" placeholder.\n */\nexport const googleBooksCoverUrl = (\n imageLinks: GoogleBooksImageLinks | undefined,\n): string | undefined => {\n if (imageLinks === undefined) return undefined\n\n const candidates = [\n imageLinks.extraLarge,\n imageLinks.large,\n imageLinks.medium,\n imageLinks.small,\n imageLinks.thumbnail,\n imageLinks.smallThumbnail,\n ]\n\n for (const candidate of candidates) {\n const absolute = absoluteHttpUrl(candidate)\n\n if (absolute !== undefined) return absolute\n }\n\n return undefined\n}\n\nconst resolvedContributors = (\n authors: ReadonlyArray<string>,\n): ReadonlyArray<ResolvedContributor> | undefined =>\n emptyToUndefined(\n authors.map<ResolvedContributor>((name) => ({\n name,\n roles: [\"author\"],\n })),\n )\n\nexport type ResolveGoogleBooksVolumeOptions = {\n /** Exact Google Books identifier that this response confirmed. */\n readonly matchedIdentifier?: MetadataIdentifier\n /** ISBN query that this response confirmed, even if the payload omits it. */\n readonly matchedIsbn?: string\n}\n\n/** Normalizes one Google Books volume into the cross-format vocabulary. */\nexport const resolveGoogleBooksVolume = (\n volume: GoogleBooksVolume,\n options: ResolveGoogleBooksVolumeOptions = {},\n): ResolvedMetadata => {\n const { id, volumeInfo, ...unhandledVolume } = volume\n const {\n title,\n subtitle,\n authors,\n publisher,\n publishedDate,\n description,\n industryIdentifiers,\n pageCount,\n categories,\n averageRating,\n ratingsCount,\n language,\n imageLinks,\n seriesInfo,\n // read by `googleBooksVolumeUrl`, which builds the match's url\n infoLink: _infoLink,\n canonicalVolumeLink: _canonicalVolumeLink,\n ...unhandledVolumeInfo\n } = volumeInfo ?? {}\n\n // Naming every parsed field is the contract: adding one to the parsed\n // shapes fails here until someone decides what becomes of it.\n unhandledVolume satisfies Record<string, never>\n unhandledVolumeInfo satisfies Record<string, never>\n\n const sourceIdentifiers = resolvedIndustryIdentifiers(\n industryIdentifiers ?? [],\n )\n const identifiers = dedupeIdentifiers([\n ...(options.matchedIdentifier !== undefined\n ? [options.matchedIdentifier]\n : []),\n ...(options.matchedIsbn !== undefined\n ? [{ value: options.matchedIsbn, scheme: \"ISBN\" }]\n : []),\n ...(id !== undefined\n ? [{ value: id, scheme: GOOGLE_BOOKS_IDENTIFIER_SCHEME }]\n : []),\n ...sourceIdentifiers,\n ])\n const edition = omitUndefined({\n date: parseW3cDtfDate(publishedDate),\n publisher,\n })\n const coverUri = googleBooksCoverUrl(imageLinks)\n const cover: ResolvedCover | undefined =\n coverUri !== undefined\n ? {\n uri: coverUri,\n confidence: \"derived\",\n }\n : undefined\n const subjects = [...new Set(categories ?? [])].slice(\n 0,\n GOOGLE_BOOKS_MAX_SUBJECTS,\n )\n\n const series = resolvedSeries(seriesInfo)\n\n return omitUndefined({\n titles: resolvedTitles(title, subtitle),\n description,\n belongsTo: series !== undefined ? { series } : undefined,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n languages: language !== undefined ? [language] : undefined,\n subjects: emptyToUndefined(subjects),\n contributors: resolvedContributors(authors ?? []),\n numberOfPages: pageCount,\n aggregateRating: resolvedAggregateRating(averageRating, ratingsCount),\n identifiers,\n cover,\n })\n}\n\n/** The human-facing Google Books page for a candidate. */\nexport const googleBooksVolumeUrl = (\n volume: GoogleBooksVolume,\n): string | undefined =>\n absoluteHttpUrl(volume.volumeInfo?.canonicalVolumeLink) ??\n absoluteHttpUrl(volume.volumeInfo?.infoLink) ??\n (volume.id !== undefined\n ? `https://books.google.com/books?id=${encodeURIComponent(volume.id)}`\n : undefined)\n","import { identifierValue } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { isJsonRecord, readRecordArray } from \"../../utils/json.ts\"\nimport { retryWithBackoff } from \"../../utils/retryWithBackoff.ts\"\nimport {\n MetadataProviderResponseError,\n responseErrorStatus,\n} from \"../responseError.ts\"\nimport { googleBooksLookupFromInput } from \"./identifier.ts\"\nimport { type GoogleBooksVolume, parseGoogleBooksVolume } from \"./parse.ts\"\nimport {\n googleBooksVolumeUrl,\n type ResolveGoogleBooksVolumeOptions,\n resolveGoogleBooksVolume,\n} from \"./resolve.ts\"\n\nconst GOOGLE_BOOKS_MAX_RESULTS = 40\nconst GOOGLE_BOOKS_EXACT_OVERSAMPLE = 1\nconst GOOGLE_BOOKS_TITLE_OVERSAMPLE = 4\nconst GOOGLE_BOOKS_REQUEST_ATTEMPTS = 3\nconst GOOGLE_BOOKS_INITIAL_RETRY_DELAY_MS = 1_000\nconst GOOGLE_BOOKS_MAX_RETRY_DELAY_MS = 3_000\nconst GOOGLE_BOOKS_RETRYABLE_STATUSES = new Set([500, 502, 503, 504])\n\ntype GoogleBooksSearchOptions = {\n readonly apiKey: string\n readonly baseUrl: string\n readonly fetch: typeof globalThis.fetch\n}\n\ntype GoogleBooksVolumeRecord = {\n readonly volume: GoogleBooksVolume\n readonly raw: unknown\n}\n\nconst QUERY_BREAKING_CHARACTERS = /[\"\\\\]+/g\nconst FIELD_OPERATOR_SEPARATOR = /:/g\nconst TERM_EXCLUSION_PREFIX = /(^|\\s)-+/g\nconst REPEATED_WHITESPACE = /\\s+/g\n\n/**\n * Terms reach Google's field operators unquoted on purpose: `intitle:\"…\"`\n * demands the phrase verbatim, which a title carrying a volume marker, a\n * scanner tag or a translated subtitle never satisfies, and no query\n * relaxation follows to recover with — the lookup would report nothing for a\n * book Google holds. Loose ranking here, precision in the scorer.\n *\n * Unquoted, though, the term is parsed as query syntax rather than carried as\n * a literal, so anything Google would read as an instruction goes: a colon\n * would promote the rest of a scanner-derived title into another field\n * (`Dune inauthor:Asimov` searching authors), a leading hyphen would exclude\n * the word behind it (`BLAME! -Master Edition-` demanding books *without*\n * \"Master\"), and both mutations can return results, which also robs the\n * title-only fallback of its turn.\n */\nconst searchTerm = (value: string): string =>\n value\n .replace(QUERY_BREAKING_CHARACTERS, \" \")\n .replace(FIELD_OPERATOR_SEPARATOR, \" \")\n .replace(TERM_EXCLUSION_PREFIX, \" \")\n .replace(REPEATED_WHITESPACE, \" \")\n .trim()\n\nconst isSearchable = (term: string): boolean => term.length > 0\n\nconst titleSearchTerms = (\n input: FetchMetadataInput,\n): { readonly title: string; readonly author?: string } | undefined => {\n const title = searchTerm(input.title ?? \"\")\n\n if (title.length === 0) return undefined\n\n const author = input.authors?.map(searchTerm).find(isSearchable)\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\nconst parseVolumeRecord = (\n payload: unknown,\n): GoogleBooksVolumeRecord | undefined => {\n const volume = parseGoogleBooksVolume(payload)\n\n return volume !== undefined ? { volume, raw: payload } : undefined\n}\n\nconst parseVolumeRecords = (\n payload: unknown,\n): ReadonlyArray<GoogleBooksVolumeRecord> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"items\").flatMap((raw) => {\n const record = parseVolumeRecord(raw)\n\n return record !== undefined ? [record] : []\n })\n}\n\nconst toCandidates = (\n records: ReadonlyArray<GoogleBooksVolumeRecord>,\n resolveOptions: ResolveGoogleBooksVolumeOptions = {},\n): ReadonlyArray<MetadataCandidate> =>\n records.map(({ volume, raw }) => ({\n metadata: resolveGoogleBooksVolume(volume, resolveOptions),\n id: volume.id,\n url: googleBooksVolumeUrl(volume),\n raw,\n }))\n\n/**\n * Creates the Google Books lookup pipeline. Lookup order is an exact volume\n * identifier, ISBN, then title plus first author with a title-only fallback.\n * A title search asks for several pages worth of `limit` because Google ranks\n * it, not the matcher: the edition the caller described can sit below the ones\n * Google finds most popular, and `fetchMetadata` trims to `limit` after scoring.\n */\nexport const createGoogleBooksSearch = (\n options: GoogleBooksSearchOptions,\n): MetadataProvider[\"search\"] => {\n const volumesUrl = `${options.baseUrl.replace(/\\/+$/, \"\")}/volumes`\n\n const request = async (\n url: URL,\n context: MetadataProviderContext,\n notFoundIsEmpty = false,\n ): Promise<Response | undefined> => {\n url.searchParams.set(\"key\", options.apiKey)\n\n return retryWithBackoff(\n async () => {\n const response = await options.fetch(url.toString(), {\n signal: context.signal,\n headers: { Accept: \"application/json\" },\n })\n\n if (notFoundIsEmpty && response.status === 404) return undefined\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Google Books lookup failed with status ${response.status}`,\n )\n }\n\n return response\n },\n {\n attempts: GOOGLE_BOOKS_REQUEST_ATTEMPTS,\n initialDelayMs: GOOGLE_BOOKS_INITIAL_RETRY_DELAY_MS,\n maxDelayMs: GOOGLE_BOOKS_MAX_RETRY_DELAY_MS,\n signal: context.signal,\n shouldRetry: (error) => {\n const status = responseErrorStatus(error)\n\n return (\n status === undefined || GOOGLE_BOOKS_RETRYABLE_STATUSES.has(status)\n )\n },\n },\n )\n }\n\n const findByVolumeId = async (\n id: string,\n context: MetadataProviderContext,\n ): Promise<GoogleBooksVolumeRecord | undefined> => {\n const response = await request(\n new URL(`${volumesUrl}/${encodeURIComponent(id)}`),\n context,\n true,\n )\n\n if (response === undefined) return undefined\n\n const record = parseVolumeRecord(await response.json())\n\n if (record?.volume.id !== id) {\n throw new Error(\n `Google Books response did not contain the requested volume ${id}`,\n )\n }\n\n return record\n }\n\n const searchVolumes = async (\n query: string,\n context: MetadataProviderContext,\n oversample: number,\n ): Promise<ReadonlyArray<GoogleBooksVolumeRecord>> => {\n const pageSize = Math.min(\n Math.ceil(context.limit * oversample),\n GOOGLE_BOOKS_MAX_RESULTS,\n )\n\n if (pageSize <= 0) return []\n\n const url = new URL(volumesUrl)\n\n url.searchParams.set(\"q\", query)\n url.searchParams.set(\"maxResults\", String(pageSize))\n url.searchParams.set(\"orderBy\", \"relevance\")\n url.searchParams.set(\"printType\", \"books\")\n\n const response = await request(url, context)\n\n return response !== undefined\n ? parseVolumeRecords(await response.json())\n : []\n }\n\n return async (input, context) => {\n const googleBooks = googleBooksLookupFromInput(input)\n\n if (googleBooks !== undefined) {\n const record = await findByVolumeId(googleBooks.id, context)\n\n if (record !== undefined) {\n return toCandidates([record], {\n matchedIdentifier: googleBooks.identifier,\n })\n }\n }\n\n const isbn = identifierValue(input.identifiers, \"ISBN\")\n\n if (isbn !== undefined) {\n const volumes = await searchVolumes(\n `isbn:${isbn}`,\n context,\n GOOGLE_BOOKS_EXACT_OVERSAMPLE,\n )\n\n if (volumes.length > 0) {\n return toCandidates(volumes, { matchedIsbn: isbn })\n }\n }\n\n const terms = titleSearchTerms(input)\n\n if (terms === undefined) return []\n\n const titleQuery = `intitle:${terms.title}`\n const preciseQuery =\n terms.author !== undefined\n ? `${titleQuery} inauthor:${terms.author}`\n : titleQuery\n const volumes = await searchVolumes(\n preciseQuery,\n context,\n GOOGLE_BOOKS_TITLE_OVERSAMPLE,\n )\n\n if (volumes.length > 0 || terms.author === undefined) {\n return toCandidates(volumes)\n }\n\n return toCandidates(\n await searchVolumes(titleQuery, context, GOOGLE_BOOKS_TITLE_OVERSAMPLE),\n )\n }\n}\n","import type { MetadataProvider } from \"../../types/provider.ts\"\nimport { createGoogleBooksSearch } from \"./search.ts\"\n\nexport const GOOGLE_BOOKS_PROVIDER_ID = \"googleBooks\"\n\nconst DEFAULT_BASE_URL = \"https://www.googleapis.com/books/v1\"\n\nexport type GoogleBooksProviderOptions = {\n /** API key identifying the application to Google Books. */\n readonly apiKey: string\n /** API root. Defaults to `https://www.googleapis.com/books/v1`. */\n readonly baseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n}\n\n/**\n * Google Books' public volume catalog. An API key is required. Lookup order:\n * an exact `GoogleBooks` id/official URL, ISBN, then title plus first author\n * with a title-only fallback. Google relevance order is preserved here; the\n * shared fetcher scores every returned edition against the caller's input.\n */\nexport const createGoogleBooksProvider = (\n options: GoogleBooksProviderOptions,\n): MetadataProvider => {\n const apiKey = options.apiKey.trim()\n\n if (apiKey.length === 0) {\n throw new Error(\"createGoogleBooksProvider requires a non-empty apiKey\")\n }\n\n return {\n id: GOOGLE_BOOKS_PROVIDER_ID,\n name: \"Google Books\",\n search: createGoogleBooksSearch({\n apiKey,\n baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,\n fetch: options.fetch ?? globalThis.fetch,\n }),\n }\n}\n","import {\n identifierValue,\n type KnownMetadataIdentifierScheme,\n type MetadataIdentifier,\n} from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\n\nexport const PROJECT_GUTENBERG_IDENTIFIER_SCHEME =\n \"ProjectGutenberg\" satisfies KnownMetadataIdentifierScheme\n\nexport type ProjectGutenbergLookup = {\n readonly id: string\n /** The book's exact identifier spelling, echoed only after PG confirms it. */\n readonly identifier: MetadataIdentifier\n}\n\n/**\n * Recognizes Project Gutenberg's own numeric identifier and official URL\n * forms. Calling providers decide explicitly how to use this crosswalk; the\n * shared scorer never reinterprets arbitrary URLs.\n */\nexport const projectGutenbergLookupFromInput = (\n input: FetchMetadataInput,\n): ProjectGutenbergLookup | undefined => {\n for (const identifier of input.identifiers ?? []) {\n const id = identifierValue(\n [identifier],\n PROJECT_GUTENBERG_IDENTIFIER_SCHEME,\n )\n\n if (id !== undefined) {\n return {\n id,\n identifier: { value: identifier.value, scheme: identifier.scheme },\n }\n }\n }\n\n return undefined\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\n\n/**\n * One entry of an Open Library `search.json` response, field names mirroring\n * the API (same rule as the archive parsers: a parsed source looks like its\n * source document). Every field is optional — the API omits what it doesn't\n * have, and the `fields` parameter decides what it may return at all.\n *\n * @see https://openlibrary.org/dev/docs/api/search\n */\nexport type OpenLibraryDoc = {\n /** Work key, e.g. `/works/OL45883W`. */\n readonly key?: string\n readonly title?: string\n readonly subtitle?: string\n readonly author_name?: ReadonlyArray<string>\n readonly first_publish_year?: number\n /** MARC 21 language codes (`eng`, `fre`), not BCP 47. */\n readonly language?: ReadonlyArray<string>\n readonly subject?: ReadonlyArray<string>\n readonly number_of_pages_median?: number\n /** Mean of the work's star ratings, on the catalog's 1–5 scale. */\n readonly ratings_average?: number\n readonly ratings_count?: number\n /** Cover id, addressing `covers.openlibrary.org`. */\n readonly cover_i?: number\n /** Project Gutenberg ebook ids attached to the work's editions. */\n readonly id_project_gutenberg?: ReadonlyArray<string>\n}\n\nconst parseDoc = (record: Record<string, unknown>): OpenLibraryDoc => ({\n key: readString(record, \"key\"),\n title: readString(record, \"title\"),\n subtitle: readString(record, \"subtitle\"),\n author_name: readStringArray(record, \"author_name\"),\n first_publish_year: readNumber(record, \"first_publish_year\"),\n language: readStringArray(record, \"language\"),\n subject: readStringArray(record, \"subject\"),\n number_of_pages_median: readNumber(record, \"number_of_pages_median\"),\n ratings_average: readNumber(record, \"ratings_average\"),\n ratings_count: readNumber(record, \"ratings_count\"),\n cover_i: readNumber(record, \"cover_i\"),\n id_project_gutenberg: readStringArray(record, \"id_project_gutenberg\"),\n})\n\n/**\n * Reads the `docs` of a `search.json` payload. A payload that isn't a record,\n * or carries no `docs` array, yields an empty list: an unexpected response\n * shape is \"this catalog has nothing for us\", not a crash.\n */\nexport const parseOpenLibrarySearchResponse = (\n payload: unknown,\n): ReadonlyArray<OpenLibraryDoc> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"docs\").map(parseDoc)\n}\n","/**\n * MARC 21 language codes → BCP 47 primary subtags, for the codes a general\n * book catalog actually returns. Open Library reports `language` as MARC\n * (`eng`, `fre`), while {@link ResolvedMetadata.languages} is BCP 47 by\n * contract, so the mapping is part of normalizing — not a nicety.\n *\n * Both MARC variants are listed where they differ (bibliographic `fre` and\n * terminology `fra`), since records in the wild carry either.\n */\nconst MARC_TO_BCP47: Readonly<Record<string, string>> = {\n afr: \"af\",\n alb: \"sq\",\n amh: \"am\",\n ara: \"ar\",\n arm: \"hy\",\n asm: \"as\",\n aze: \"az\",\n baq: \"eu\",\n bel: \"be\",\n ben: \"bn\",\n bos: \"bs\",\n bul: \"bg\",\n bur: \"my\",\n cat: \"ca\",\n ces: \"cs\",\n chi: \"zh\",\n cym: \"cy\",\n cze: \"cs\",\n dan: \"da\",\n deu: \"de\",\n dut: \"nl\",\n ell: \"el\",\n eng: \"en\",\n epo: \"eo\",\n est: \"et\",\n eus: \"eu\",\n fao: \"fo\",\n fas: \"fa\",\n fin: \"fi\",\n fra: \"fr\",\n fre: \"fr\",\n geo: \"ka\",\n ger: \"de\",\n gle: \"ga\",\n glg: \"gl\",\n gre: \"el\",\n guj: \"gu\",\n heb: \"he\",\n hin: \"hi\",\n hrv: \"hr\",\n hun: \"hu\",\n hye: \"hy\",\n ice: \"is\",\n ind: \"id\",\n isl: \"is\",\n ita: \"it\",\n jpn: \"ja\",\n kan: \"kn\",\n kat: \"ka\",\n kaz: \"kk\",\n khm: \"km\",\n kor: \"ko\",\n lao: \"lo\",\n lat: \"la\",\n lav: \"lv\",\n lit: \"lt\",\n ltz: \"lb\",\n mac: \"mk\",\n mal: \"ml\",\n mar: \"mr\",\n may: \"ms\",\n mkd: \"mk\",\n mlt: \"mt\",\n mon: \"mn\",\n msa: \"ms\",\n mya: \"my\",\n nep: \"ne\",\n nld: \"nl\",\n nno: \"nn\",\n nob: \"nb\",\n nor: \"no\",\n ori: \"or\",\n pan: \"pa\",\n per: \"fa\",\n pol: \"pl\",\n por: \"pt\",\n rom: \"ro\",\n ron: \"ro\",\n rum: \"ro\",\n rus: \"ru\",\n scc: \"sr\",\n scr: \"hr\",\n sin: \"si\",\n slk: \"sk\",\n slo: \"sk\",\n slv: \"sl\",\n snd: \"sd\",\n spa: \"es\",\n sqi: \"sq\",\n srp: \"sr\",\n swa: \"sw\",\n swe: \"sv\",\n tam: \"ta\",\n tel: \"te\",\n tgl: \"tl\",\n tha: \"th\",\n tur: \"tr\",\n ukr: \"uk\",\n urd: \"ur\",\n uzb: \"uz\",\n vie: \"vi\",\n wel: \"cy\",\n yid: \"yi\",\n zho: \"zh\",\n}\n\n/**\n * MARC placeholders that say \"no single language\": keeping them would put a\n * non-language in a BCP 47 field, so they resolve to nothing.\n */\nconst NON_LANGUAGE_CODES = new Set([\"mul\", \"und\", \"zxx\", \"sgn\"])\n\n/**\n * Normalizes one MARC language code to its BCP 47 primary subtag. Unknown\n * codes (the long tail of MARC) pass through lowercased rather than being\n * dropped — a language we can't name is still a language the record stated.\n */\nexport const marcLanguageToBcp47 = (code: string): string | undefined => {\n const normalized = code.trim().toLowerCase()\n\n if (normalized.length === 0) return undefined\n if (NON_LANGUAGE_CODES.has(normalized)) return undefined\n\n return MARC_TO_BCP47[normalized] ?? normalized\n}\n","import type {\n KnownMetadataIdentifierScheme,\n MetadataIdentifier,\n ResolvedMetadata,\n} from \"@prose-reader/archive-reader\"\nimport { resolvedAggregateRating } from \"../../utils/aggregateRating.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { marcLanguageToBcp47 } from \"./marcLanguage.ts\"\nimport type { OpenLibraryDoc } from \"./parse.ts\"\n\n/**\n * A popular work carries hundreds of subject headings, most of them long-tail\n * noise (\"Accessible book\", \"Protected DAISY\"), which would dwarf the entity a\n * consumer persists. The API puts the meaningful ones first.\n */\nexport const OPEN_LIBRARY_MAX_SUBJECTS = 25\n\nexport const OPEN_LIBRARY_IDENTIFIER_SCHEME =\n \"OpenLibrary\" satisfies KnownMetadataIdentifierScheme\nconst PROJECT_GUTENBERG_IDENTIFIER_SCHEME =\n \"ProjectGutenberg\" satisfies KnownMetadataIdentifierScheme\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Normalizes one search hit into the cross-format vocabulary. Two choices\n * worth stating:\n *\n * - **`title` and `subtitle` stay separate**, as the catalog states them:\n * `title` is the work's title and the subtitle is a `titles` entry. The\n * matcher composes them when it compares, so nothing is lost by not\n * pre-joining them here.\n * - **An ISBN identifier is added only for an ISBN lookup**, using the queried\n * value — the API answered \"this work has that ISBN\", a fact about the\n * record. A title-search hit describes a *work*, whose editions each have\n * their own ISBN, so picking one would be fabrication.\n */\nexport const resolveOpenLibraryDoc = (\n doc: OpenLibraryDoc,\n options: {\n readonly coversBaseUrl: string\n /** The ISBN this record was looked up by, when it was. */\n readonly isbn?: string\n /** The source identifier that an exact Gutenberg-id lookup confirmed. */\n readonly matchedProjectGutenbergIdentifier?: MetadataIdentifier\n },\n): ResolvedMetadata => {\n const {\n title,\n subtitle,\n language,\n key,\n author_name,\n first_publish_year,\n subject,\n number_of_pages_median,\n ratings_average,\n ratings_count,\n cover_i,\n id_project_gutenberg,\n ...unhandled\n } = doc\n\n // Naming every parsed field is the contract: adding one to\n // `OpenLibraryDoc` fails here until someone decides what becomes of it.\n unhandled satisfies Record<string, never>\n\n const titles = emptyToUndefined([\n ...(title !== undefined ? [{ value: title }] : []),\n ...(subtitle !== undefined\n ? [{ value: subtitle, type: \"subtitle\" as const }]\n : []),\n ])\n\n const languages = emptyToUndefined([\n ...new Set(\n (language ?? [])\n .map(marcLanguageToBcp47)\n .filter((value): value is string => value !== undefined),\n ),\n ])\n\n const identifiers = [\n ...(options.isbn !== undefined\n ? [{ value: options.isbn, scheme: \"ISBN\" }]\n : []),\n ...(options.matchedProjectGutenbergIdentifier !== undefined\n ? [options.matchedProjectGutenbergIdentifier]\n : []),\n ...(id_project_gutenberg ?? []).map((value) => ({\n value,\n scheme: PROJECT_GUTENBERG_IDENTIFIER_SCHEME,\n })),\n ...(key !== undefined\n ? [{ value: key, scheme: OPEN_LIBRARY_IDENTIFIER_SCHEME }]\n : []),\n ]\n\n return omitUndefined({\n titles,\n cover:\n cover_i !== undefined\n ? {\n // absolute, not container-relative: a catalog addresses its\n // covers in its own space, with nothing to rebase onto\n uri: `${options.coversBaseUrl}/b/id/${cover_i}-L.jpg`,\n mediaType: \"image/jpeg\",\n confidence: \"derived\" as const,\n }\n : undefined,\n publication:\n first_publish_year !== undefined\n ? { original: { date: { year: first_publish_year } } }\n : undefined,\n languages,\n subjects: emptyToUndefined(\n (subject ?? []).slice(0, OPEN_LIBRARY_MAX_SUBJECTS),\n ),\n contributors: emptyToUndefined(\n (author_name ?? []).map((name) => ({\n name,\n roles: [\"author\" as const],\n })),\n ),\n numberOfPages: number_of_pages_median,\n aggregateRating: resolvedAggregateRating(ratings_average, ratings_count),\n identifiers: emptyToUndefined(identifiers),\n })\n}\n","import { identifierValue } from \"@prose-reader/archive-reader\"\nimport type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport {\n type ProjectGutenbergLookup,\n projectGutenbergLookupFromInput,\n} from \"../projectGutenberg/identifier.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { type OpenLibraryDoc, parseOpenLibrarySearchResponse } from \"./parse.ts\"\nimport { resolveOpenLibraryDoc } from \"./resolve.ts\"\n\nexport const OPEN_LIBRARY_PROVIDER_ID = \"openLibrary\"\n\nconst DEFAULT_BASE_URL = \"https://openlibrary.org\"\nconst DEFAULT_COVERS_BASE_URL = \"https://covers.openlibrary.org\"\n\n/**\n * Only what {@link resolveOpenLibraryDoc} maps: `search.json` returns every\n * edition key and IA identifier by default, so `fields` is the difference\n * between a few hundred bytes per hit and a few hundred kilobytes.\n */\nconst SEARCH_FIELDS = [\n \"key\",\n \"title\",\n \"subtitle\",\n \"author_name\",\n \"first_publish_year\",\n \"language\",\n \"subject\",\n \"number_of_pages_median\",\n \"ratings_average\",\n \"ratings_count\",\n \"cover_i\",\n \"id_project_gutenberg\",\n].join(\",\")\n\nexport type OpenLibraryProviderOptions = {\n /** API origin. Defaults to `https://openlibrary.org`. */\n readonly baseUrl?: string\n /** Cover service origin. Defaults to `https://covers.openlibrary.org`. */\n readonly coversBaseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /**\n * Open Library's API etiquette asks for an identifying `User-Agent` (app\n * name + contact) and throttles anonymous traffic harder.\n */\n readonly userAgent?: string\n}\n\nconst searchTerms = (\n input: FetchMetadataInput,\n): { readonly title: string; readonly author?: string } | undefined => {\n const title = input.title?.trim()\n\n if (title === undefined || title.length === 0) return undefined\n\n const author = input.authors?.find((value) => value.trim().length > 0)?.trim()\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\n/**\n * Open Library — the Internet Archive's open bibliographic catalog. Free, no\n * key, covers books broadly (much less so comics and manga).\n *\n * ```ts\n * const provider = createOpenLibraryProvider({\n * userAgent: \"MyReader/1.0 (contact@example.com)\",\n * })\n * ```\n *\n * Lookup strategy, at most four requests: an **ISBN search** when the book\n * states one; an exact **Project Gutenberg id search** when an identifier is\n * an official Gutenberg URL; then a precise **title (+ first author) search**,\n * followed by a free-text fallback when Open Library indexed part of the title\n * as a subtitle. A query with none of those terms yields no candidates rather\n * than a fishing expedition.\n */\nexport const createOpenLibraryProvider = (\n options: OpenLibraryProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n const coversBaseUrl = options.coversBaseUrl ?? DEFAULT_COVERS_BASE_URL\n\n const searchDocs = async (\n terms: Record<string, string>,\n context: MetadataProviderContext,\n ): Promise<ReadonlyArray<OpenLibraryDoc>> => {\n const url = new URL(\"/search.json\", baseUrl)\n\n for (const [key, value] of Object.entries(terms)) {\n url.searchParams.set(key, value)\n }\n\n url.searchParams.set(\"fields\", SEARCH_FIELDS)\n url.searchParams.set(\"limit\", String(context.limit))\n\n // read lazily so a consumer stubbing the global afterwards still wins\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(url.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/json\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Open Library search failed with status ${response.status}`,\n )\n }\n\n return parseOpenLibrarySearchResponse(await response.json())\n }\n\n const toCandidates = (\n docs: ReadonlyArray<OpenLibraryDoc>,\n options: {\n readonly isbn?: string\n readonly confirmedProjectGutenberg?: ProjectGutenbergLookup\n } = {},\n ): ReadonlyArray<MetadataCandidate> =>\n docs.map((doc) => ({\n metadata: resolveOpenLibraryDoc(doc, {\n coversBaseUrl,\n isbn: options.isbn,\n matchedProjectGutenbergIdentifier:\n options.confirmedProjectGutenberg?.identifier,\n }),\n id: doc.key,\n url: doc.key !== undefined ? `${baseUrl}${doc.key}` : undefined,\n raw: doc,\n }))\n\n return {\n id: OPEN_LIBRARY_PROVIDER_ID,\n name: \"Open Library\",\n search: async (input, context) => {\n const isbn = identifierValue(input.identifiers, \"ISBN\")\n const projectGutenberg = projectGutenbergLookupFromInput(input)\n\n if (isbn !== undefined) {\n const docs = await searchDocs({ isbn }, context)\n\n if (docs.length > 0) return toCandidates(docs, { isbn })\n }\n\n if (projectGutenberg !== undefined) {\n const docs = await searchDocs(\n { q: `id_project_gutenberg:${projectGutenberg.id}` },\n context,\n )\n\n if (docs.length > 0) {\n return toCandidates(docs, {\n confirmedProjectGutenberg: projectGutenberg,\n })\n }\n }\n\n const terms = searchTerms(input)\n\n if (terms === undefined) return []\n\n const docs = await searchDocs(terms, context)\n\n if (docs.length > 0) return toCandidates(docs)\n\n const q =\n terms.author !== undefined\n ? `${terms.title} ${terms.author}`\n : terms.title\n\n return toCandidates(await searchDocs({ q }, context))\n },\n }\n}\n","import type { XmlElement, XmlNodeBase } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\n\nexport type ProjectGutenbergContributor = {\n readonly name: string\n /** MARC relator code, or `aut` for `dcterms:creator`. */\n readonly role: string\n}\n\nexport type ProjectGutenbergCover = {\n readonly uri: string\n readonly mediaType?: string\n}\n\n/** The subset of one official per-eBook RDF record that we consume. */\nexport type ProjectGutenbergRecord = {\n readonly id: string\n readonly title?: string\n readonly publisher?: string\n readonly issued?: string\n /** Original print publication statement (`MARC 260`). */\n readonly originalPublication?: string\n readonly rights?: string\n readonly description?: string\n readonly summary?: string\n readonly languages?: ReadonlyArray<string>\n readonly subjects?: ReadonlyArray<string>\n readonly bookshelves?: ReadonlyArray<string>\n readonly contributors?: ReadonlyArray<ProjectGutenbergContributor>\n readonly cover?: ProjectGutenbergCover\n}\n\nconst elementLocalName = (name: string): string =>\n name.includes(\":\") ? name.slice(name.lastIndexOf(\":\") + 1) : name\n\nconst isXmlElement = (node: XmlNodeBase): node is XmlElement =>\n node.type === \"element\"\n\nconst childrenNamedLocal = (\n parent: XmlElement,\n localName: string,\n): ReadonlyArray<XmlElement> =>\n parent.children.filter(\n (child): child is XmlElement =>\n isXmlElement(child) &&\n elementLocalName(child.name).toLowerCase() === localName.toLowerCase(),\n )\n\nconst childNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement | undefined => childrenNamedLocal(parent, localName)[0]\n\nconst text = (element: XmlElement | undefined): string | undefined => {\n const trimmed = element?.val.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst attributeNamedLocal = (\n element: XmlElement,\n localName: string,\n): string | undefined => {\n for (const [name, value] of Object.entries(element.attr)) {\n if (elementLocalName(name).toLowerCase() !== localName.toLowerCase()) {\n continue\n }\n\n const trimmed = value.trim()\n\n if (trimmed.length > 0) return trimmed\n }\n\n return undefined\n}\n\nconst valuesFromDescriptions = (\n ebook: XmlElement,\n localName: string,\n): ReadonlyArray<string> =>\n childrenNamedLocal(ebook, localName).flatMap((container) => {\n const description = childNamedLocal(container, \"Description\")\n const value = text(childNamedLocal(description ?? container, \"value\"))\n\n return value !== undefined ? [value] : []\n })\n\nconst projectGutenbergIdFromEbook = (ebook: XmlElement): string | undefined => {\n const about = attributeNamedLocal(ebook, \"about\")\n const id =\n about !== undefined ? /(?:^|\\/)ebooks\\/(\\d+)$/.exec(about)?.[1] : undefined\n\n if (id === undefined) return undefined\n\n const normalized = id.replace(/^0+(?=\\d)/, \"\")\n\n return normalized !== \"0\" ? normalized : undefined\n}\n\nconst contributorsFromEbook = (\n ebook: XmlElement,\n): ReadonlyArray<ProjectGutenbergContributor> => {\n const contributors: ProjectGutenbergContributor[] = []\n\n for (const child of ebook.children) {\n if (!isXmlElement(child)) continue\n\n const isCreator = child.name.toLowerCase() === \"dcterms:creator\"\n const isMarcRelator = child.name.toLowerCase().startsWith(\"marcrel:\")\n\n if (!isCreator && !isMarcRelator) continue\n\n const name = text(\n childNamedLocal(childNamedLocal(child, \"agent\") ?? child, \"name\"),\n )\n\n if (name === undefined) continue\n\n contributors.push({\n name,\n role: isCreator ? \"aut\" : elementLocalName(child.name).toLowerCase(),\n })\n }\n\n return contributors\n}\n\nconst subjectValuesFromEbook = (ebook: XmlElement): ReadonlyArray<string> =>\n childrenNamedLocal(ebook, \"subject\").flatMap((container) => {\n const description = childNamedLocal(container, \"Description\")\n const memberOf =\n description !== undefined\n ? attributeNamedLocal(\n childNamedLocal(description, \"memberOf\") ?? description,\n \"resource\",\n )\n : undefined\n const value = text(\n description !== undefined\n ? childNamedLocal(description, \"value\")\n : undefined,\n )\n\n return value !== undefined && memberOf?.endsWith(\"/LCSH\") === true\n ? [value]\n : []\n })\n\nconst coverFromEbook = (\n ebook: XmlElement,\n): ProjectGutenbergCover | undefined => {\n const covers = childrenNamedLocal(ebook, \"hasFormat\").flatMap(\n (formatContainer) => {\n const file = childNamedLocal(formatContainer, \"file\")\n\n if (file === undefined) return []\n\n const uri = attributeNamedLocal(file, \"about\")\n const mediaType = childrenNamedLocal(file, \"format\")\n .map((format) => {\n const description = childNamedLocal(format, \"Description\")\n\n return text(childNamedLocal(description ?? format, \"value\"))\n })\n .find((value) => value?.startsWith(\"image/\") === true)\n\n if (\n uri === undefined ||\n mediaType === undefined ||\n !/\\.cover\\.(?:medium|small)\\.[a-z0-9]+(?:$|[?#])/i.test(uri)\n ) {\n return []\n }\n\n return [{ uri, mediaType }]\n },\n )\n\n return (\n covers.find((cover) => /\\.cover\\.medium\\./i.test(cover.uri)) ?? covers[0]\n )\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Parses one official Project Gutenberg per-eBook RDF document. Unsupported\n * XML yields `undefined`; malformed XML throws with a provider-specific label.\n */\nexport const parseProjectGutenbergRdf = (\n xml: string,\n): ProjectGutenbergRecord | undefined => {\n let document: XmlDocument\n\n try {\n document = new XmlDocument(xml)\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause)\n throw new Error(`Project Gutenberg RDF is malformed: ${message}`, { cause })\n }\n\n if (elementLocalName(document.name).toLowerCase() !== \"rdf\") {\n return undefined\n }\n\n const ebook = childNamedLocal(document, \"ebook\")\n\n if (ebook === undefined) return undefined\n\n const id = projectGutenbergIdFromEbook(ebook)\n\n if (id === undefined) return undefined\n\n const languages = [...new Set(valuesFromDescriptions(ebook, \"language\"))]\n const subjects = [...new Set(subjectValuesFromEbook(ebook))]\n const bookshelves = [...new Set(valuesFromDescriptions(ebook, \"bookshelf\"))]\n const contributors = contributorsFromEbook(ebook)\n\n return {\n id,\n title: text(childNamedLocal(ebook, \"title\")),\n publisher: text(childNamedLocal(ebook, \"publisher\")),\n issued: text(childNamedLocal(ebook, \"issued\")),\n originalPublication: text(childNamedLocal(ebook, \"marc260\")),\n rights: text(childNamedLocal(ebook, \"rights\")),\n description: text(childNamedLocal(ebook, \"description\")),\n summary: text(childNamedLocal(ebook, \"marc520\")),\n languages: emptyToUndefined(languages),\n subjects: emptyToUndefined(subjects),\n bookshelves: emptyToUndefined(bookshelves),\n contributors: emptyToUndefined(contributors),\n cover: coverFromEbook(ebook),\n }\n}\n","import type {\n MetadataIdentifier,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedCover,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedPublication,\n} from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { PROJECT_GUTENBERG_IDENTIFIER_SCHEME } from \"./identifier.ts\"\nimport type { ProjectGutenbergRecord } from \"./parse.ts\"\n\nexport const PROJECT_GUTENBERG_MAX_SUBJECTS = 25\n\nconst MARC_RELATOR_TO_ROLE: Readonly<\n Partial<Record<string, ResolvedContributorRole>>\n> = {\n art: \"artist\",\n aut: \"author\",\n clr: \"colorist\",\n ctb: \"contributor\",\n edt: \"editor\",\n ill: \"illustrator\",\n nrt: \"narrator\",\n trl: \"translator\",\n}\n\nconst resolvedContributors = (\n record: ProjectGutenbergRecord,\n): ReadonlyArray<ResolvedContributor> | undefined => {\n const contributors = new Map<string, { name: string; roles: string[] }>()\n\n for (const contributor of record.contributors ?? []) {\n const key = contributor.name.trim().toLowerCase()\n const role =\n MARC_RELATOR_TO_ROLE[contributor.role.trim().toLowerCase()] ??\n contributor.role\n const existing = contributors.get(key)\n\n if (existing === undefined) {\n contributors.set(key, { name: contributor.name, roles: [role] })\n } else if (!existing.roles.includes(role)) {\n existing.roles.push(role)\n }\n }\n\n return contributors.size > 0 ? [...contributors.values()] : undefined\n}\n\nconst resolvedIssuedDate = (\n issued: string | undefined,\n): ResolvedDate | undefined => {\n if (issued === undefined) return undefined\n\n const match = /^(\\d{4})(?:-(\\d{2})(?:-(\\d{2}))?)?$/.exec(issued.trim())\n\n if (match === null) return undefined\n\n const year = Number(match[1])\n const month = match[2] !== undefined ? Number(match[2]) : undefined\n const day = match[3] !== undefined ? Number(match[3]) : undefined\n\n if (\n !Number.isInteger(year) ||\n year <= 0 ||\n (month !== undefined && (month < 1 || month > 12)) ||\n (day !== undefined && (day < 1 || day > 31))\n ) {\n return undefined\n }\n\n return {\n year,\n ...(month !== undefined ? { month } : {}),\n ...(day !== undefined ? { day } : {}),\n }\n}\n\nconst marc260Subfields = (\n statement: string,\n code: string,\n): ReadonlyArray<string> =>\n statement\n .split(\"$\")\n .slice(1)\n .flatMap((subfield) =>\n subfield[0]?.toLowerCase() === code.toLowerCase()\n ? [subfield.slice(1).trim()]\n : [],\n )\n .filter((value) => value.length > 0)\n\nconst originalPublicationFromMarc260 = (\n statement: string | undefined,\n): ResolvedPublication | undefined => {\n if (statement === undefined) return undefined\n\n const publisherValues = marc260Subfields(statement, \"b\")\n const rawPublisher =\n publisherValues.length === 1 ? publisherValues[0] : undefined\n const publisher = rawPublisher?.replace(/[\\s,;:/]+$/u, \"\").trim()\n const dateValues = marc260Subfields(statement, \"c\")\n const years = [\n ...new Set(\n dateValues.flatMap((value) =>\n [...value.matchAll(/\\d{4}/g)].flatMap((match) =>\n match[0] !== undefined ? [Number(match[0])] : [],\n ),\n ),\n ),\n ]\n const date = years.length === 1 ? { year: years[0] } : undefined\n const details = omitUndefined({\n date,\n publisher:\n publisher !== undefined &&\n publisher.length > 0 &&\n !/^\\[?s\\.n\\.?\\]?$/i.test(publisher)\n ? publisher\n : undefined,\n })\n\n return details.date !== undefined || details.publisher !== undefined\n ? details\n : undefined\n}\n\nconst absoluteHttpUrl = (\n value: string,\n baseUrl: string,\n): string | undefined => {\n try {\n const url = new URL(value, baseUrl)\n\n return url.protocol === \"http:\" || url.protocol === \"https:\"\n ? url.toString()\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nexport const resolveProjectGutenbergRecord = (\n record: ProjectGutenbergRecord,\n options: {\n readonly baseUrl: string\n readonly matchedIdentifier: MetadataIdentifier\n },\n): ResolvedMetadata => {\n const {\n id,\n title,\n publisher,\n issued,\n originalPublication,\n rights,\n description,\n summary,\n languages,\n subjects: recordSubjects,\n bookshelves,\n contributors: _contributors,\n cover: recordCover,\n ...unhandled\n } = record\n\n // Naming every parsed field is the contract: adding one to\n // `ProjectGutenbergRecord` fails here until someone decides what becomes\n // of it. `contributors` is read by `resolvedContributors` below.\n unhandled satisfies Record<string, never>\n\n const identifiers = [\n options.matchedIdentifier,\n { value: id, scheme: PROJECT_GUTENBERG_IDENTIFIER_SCHEME },\n ].filter(\n (identifier, index, all) =>\n all.findIndex(\n (other) =>\n other.value.trim().toLowerCase() ===\n identifier.value.trim().toLowerCase() &&\n other.scheme.trim().toLowerCase() ===\n identifier.scheme.trim().toLowerCase(),\n ) === index,\n )\n const subjects = [\n ...new Set([...(recordSubjects ?? []), ...(bookshelves ?? [])]),\n ].slice(0, PROJECT_GUTENBERG_MAX_SUBJECTS)\n const coverUri =\n recordCover !== undefined\n ? absoluteHttpUrl(recordCover.uri, options.baseUrl)\n : undefined\n const cover: ResolvedCover | undefined =\n coverUri !== undefined\n ? {\n uri: coverUri,\n mediaType: recordCover?.mediaType,\n confidence: \"derived\",\n }\n : undefined\n const edition = omitUndefined({\n date: resolvedIssuedDate(issued),\n publisher,\n })\n const original = originalPublicationFromMarc260(originalPublication)\n const publication = omitUndefined({\n original,\n edition:\n edition.date !== undefined || edition.publisher !== undefined\n ? edition\n : undefined,\n })\n\n return omitUndefined({\n titles: title !== undefined ? [{ value: title }] : undefined,\n publication:\n publication.original !== undefined || publication.edition !== undefined\n ? publication\n : undefined,\n description: summary ?? description,\n rights,\n languages: emptyToUndefined([...new Set(languages ?? [])]),\n subjects: emptyToUndefined(subjects),\n contributors: resolvedContributors(record),\n identifiers,\n cover,\n })\n}\n","import type { FetchMetadataInput } from \"../../types/fetchMetadataInput.ts\"\nimport type {\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { projectGutenbergLookupFromInput } from \"./identifier.ts\"\nimport { parseProjectGutenbergRdf } from \"./parse.ts\"\nimport { resolveProjectGutenbergRecord } from \"./resolve.ts\"\n\nexport const PROJECT_GUTENBERG_PROVIDER_ID = \"projectGutenberg\"\n\nconst DEFAULT_BASE_URL = \"https://www.gutenberg.org\"\n\nexport type ProjectGutenbergProviderOptions = {\n /** Catalog origin. Defaults to `https://www.gutenberg.org`. */\n readonly baseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /** Optional identifying user agent sent with the RDF request. */\n readonly userAgent?: string\n}\n\n/**\n * Project Gutenberg's official catalog, queried through its per-eBook RDF\n * records. The provider performs exact identifier lookups only: official\n * Gutenberg URLs and numeric `ProjectGutenberg` identifiers are accepted;\n * titles are deliberately never used to crawl or fuzzily search the site.\n */\nexport const createProjectGutenbergProvider = (\n options: ProjectGutenbergProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n\n const search = async (\n input: FetchMetadataInput,\n context: MetadataProviderContext,\n ) => {\n const lookup = projectGutenbergLookupFromInput(input)\n\n if (lookup === undefined) return []\n\n const recordUrl = new URL(\n `/cache/epub/${lookup.id}/pg${lookup.id}.rdf`,\n baseUrl,\n )\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(recordUrl.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/rdf+xml, application/xml;q=0.9\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (response.status === 404) return []\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Project Gutenberg metadata lookup failed with status ${response.status}`,\n )\n }\n\n const record = parseProjectGutenbergRdf(await response.text())\n\n if (record === undefined || record.id !== lookup.id) {\n throw new Error(\n `Project Gutenberg RDF did not contain the requested eBook ${lookup.id}`,\n )\n }\n\n const bookUrl = new URL(`/ebooks/${lookup.id}`, baseUrl).toString()\n\n return [\n {\n id: record.id,\n url: bookUrl,\n raw: record,\n metadata: resolveProjectGutenbergRecord(record, {\n baseUrl,\n matchedIdentifier: lookup.identifier,\n }),\n },\n ]\n }\n\n return {\n id: PROJECT_GUTENBERG_PROVIDER_ID,\n name: \"Project Gutenberg\",\n search,\n }\n}\n","import type { FetchMetadataInput } from \"../types/fetchMetadataInput.ts\"\n\n/**\n * Whether a catalog has anything to go on, so a lookup with nothing to ask\n * about costs no round trip. Publication details, a language, or a page count\n * narrow a search but cannot start one.\n */\nexport const hasSearchTerms = (input: FetchMetadataInput): boolean =>\n [input.title, input.series].some(\n (value) => value !== undefined && value.trim().length > 0,\n ) ||\n (input.identifiers ?? []).some(\n (identifier) => identifier.value.trim().length > 0,\n ) ||\n (input.authors ?? []).some((author) => author.trim().length > 0)\n"],"mappings":"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"}