library-reads 0.1.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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["isString","isPlainObject","parseYaml","parse"],"sources":["../src/enrich.ts","../src/extras.ts","../src/libby.ts","../src/orchestrator.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { ReadEntry, ReadFormat } from './types.js';\n\n/** Distilled fields extracted from Open Library; the shape we cache and merge into ReadEntry. */\nexport interface EnrichmentData {\n coverUrl?: string;\n pageCount?: number;\n publishYear?: number;\n subjects?: string[];\n /** The work OLID, normalized (e.g. 'OL12345W'). */\n olid?: string;\n format?: ReadFormat;\n}\n\n/** One row in the cache file. */\nexport interface CacheEntry {\n /** ISO date string (YYYY-MM-DD) of when this entry was fetched. */\n fetchedAt: string;\n /** The lookup key we used: 'isbn:{isbn}', 'olid:{olid}', or 'title-author:{hash}'. */\n lookupKey: string;\n data: EnrichmentData;\n /**\n * True when the lookup definitively returned 404 (Open Library doesn't\n * have this book). Distinguishes \"we tried and it isn't there\" from\n * \"we got a sparse but valid record.\" Negative cache entries are\n * refetched on the same maxAgeDays schedule as positive ones; busting\n * a key forces a refetch regardless.\n */\n notFound?: boolean;\n /**\n * How this entry was matched ('exact' | 'fuzzy' | 'unmatched'). Written on\n * every cache entry created from now on; optional because entries written\n * before this field existed lack it (a cache hit on such an entry yields an\n * undefined matchQuality, and we do not refetch just to populate it).\n */\n matchQuality?: 'exact' | 'fuzzy' | 'unmatched';\n}\n\n/** The full cache file shape. Keys are the lookupKey values. */\nexport type Cache = Record<string, CacheEntry>;\n\nexport interface EditionPreferences {\n /**\n * Preferred language codes in order of preference (Open Library uses 3-letter codes:\n * 'eng', 'spa', 'fre', 'jpn', etc.). Tried sequentially; the picker uses the first\n * language that has at least one matching edition. Default: ['eng'].\n *\n * Example: a Spanish-speaking user might pass ['spa', 'eng'] to prefer Spanish\n * editions but fall back to English when no Spanish edition exists.\n */\n languages?: string[];\n /**\n * Prefer editions that have BOTH a cover image AND a page count. Default: true.\n * Set false if you want the original publication edition regardless of completeness.\n */\n preferComplete?: boolean;\n /**\n * Prefer the most recent edition among otherwise-equal candidates. Default: true.\n * Set false to prefer the original publication (oldest edition).\n */\n preferRecent?: boolean;\n}\n\nexport interface EnrichOptions {\n /** Required: Open Library asks for identifying requests. Format: 'package-name/version (email)'. */\n userAgent: string;\n /** The cache to read from and write to. Mutated by the function. */\n cache: Cache;\n /**\n * Refetch entries whose fetchedAt is older than this many days. Default 180.\n * Open Library data is mostly static but does get corrected occasionally.\n */\n maxAgeDays?: number;\n /** Force-refetch these lookup keys this run, even if cached. Use for surgical corrections. */\n bust?: string[];\n /** Disable cache reads (still writes for next build). Useful for debugging enrichment. */\n ignoreCache?: boolean;\n /** Injectable fetch for testability. Defaults to globalThis.fetch. */\n fetchImpl?: typeof globalThis.fetch;\n /** Minimum ms between requests. Default 1000 (1 req/sec). */\n rateLimitMs?: number;\n /**\n * Shared rate-limiter state. When provided, sequential `enrich` calls\n * coordinate so that the 1 req/sec budget is honored across the entire\n * batch, not just within each call. The orchestrator creates one and\n * passes the same reference to every call.\n *\n * Shape: `{ nextAllowedAt: number }` where nextAllowedAt is a millisecond\n * timestamp. Both reads and writes happen during a single enrich call.\n *\n * When omitted, each enrich call uses its own per-call state (correct in\n * isolation but unsafe in batches).\n */\n rateLimiterState?: { nextAllowedAt: number };\n /**\n * Preferences applied when picking a representative edition from a work's editions list.\n * Defaults are Anglocentric and lean toward complete + recent editions; override via this\n * option to prefer a different language, original publications, etc.\n *\n * Note: the cache stores the picked edition's data, not the raw responses. If you change\n * these preferences between builds, cached entries reflect the OLD preferences until you\n * bust them. The package does not auto-detect preference changes; pass\n * `bust: Object.keys(cache)` to refetch everything and see the new picks take effect.\n */\n editionPreferences?: EditionPreferences;\n}\n\nexport interface EnrichResult {\n /** The enriched entry. May equal the input if enrichment had no path or all fields failed. */\n entry: ReadEntry;\n /** Soft failures (404 from Open Library, fuzzy match fallback, missing fields, etc.). */\n warnings: string[];\n /**\n * How the entry was matched: 'exact' (ISBN/OLID), 'fuzzy' (title+author\n * search, primary or fallback), or 'unmatched' (no match at all). Undefined\n * when a transport failure left the outcome unknown, or when a cache hit came\n * from an entry written before this field existed. The orchestrator copies a\n * defined value onto the returned entry's `matchQuality`.\n */\n matchQuality?: 'exact' | 'fuzzy' | 'unmatched';\n}\n\n/**\n * The narrowed shape of an Open Library edition we read from a work's editions list,\n * an `/isbn` lookup, or a `/books` lookup. Only the fields we actually use.\n */\nexport interface OpenLibraryEdition {\n physical_format?: string;\n languages?: { key: string }[];\n covers?: number[];\n number_of_pages?: number;\n publish_date?: string;\n works?: { key: string }[];\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Map Open Library's free-form `physical_format` to our format enum. Imperfect by\n * design: anything with a value that is not clearly audio or electronic is treated\n * as physical; the user can override via extras. Missing/empty yields undefined.\n */\nexport function inferFormat(physicalFormat: string | undefined): ReadFormat | undefined {\n if (physicalFormat === undefined || physicalFormat.trim() === '') {\n return undefined;\n }\n const value = physicalFormat.toLowerCase();\n if (value.includes('audio') || value.includes('mp3') || value.includes('sound recording')) {\n return 'audiobook';\n }\n if (value.includes('ebook') || value.includes('electronic') || value.includes('online')) {\n return 'ebook';\n }\n return 'physical';\n}\n\n/** Extract a four-digit year from a free-form publish date, or undefined. */\nexport function extractYear(publishDate: string | undefined): number | undefined {\n if (publishDate === undefined) {\n return undefined;\n }\n const match = publishDate.match(/\\d{4}/);\n return match ? Number(match[0]) : undefined;\n}\n\n/** Construct the large-cover URL for an Open Library cover ID. */\nexport function coverUrlFromId(coverId: number): string {\n return `https://covers.openlibrary.org/b/id/${coverId}-L.jpg`;\n}\n\n/** Strip hyphens and whitespace from an ISBN; leave everything else as-is. */\nexport function normalizeIsbn(isbn: string): string {\n return isbn.replace(/[-\\s]/g, '');\n}\n\n/** Normalize a string for hashing: lowercase, collapse internal whitespace, trim. */\nfunction normalizeForHash(value: string): string {\n return value.toLowerCase().replace(/\\s+/g, ' ').trim();\n}\n\n/** 16-char sha256 prefix of the normalized 'title|author' key, for compact cache keys. */\nexport function hashTitleAuthor(title: string, author: string): string {\n const key = `${normalizeForHash(title)}|${normalizeForHash(author)}`;\n return createHash('sha256').update(key).digest('hex').slice(0, 16);\n}\n\n/** True when a cache entry is older than maxAgeDays and should be refetched. */\nexport function shouldRefetch(cacheEntry: CacheEntry, maxAgeDays: number): boolean {\n const fetchedAt = new Date(cacheEntry.fetchedAt).getTime();\n const ageDays = (Date.now() - fetchedAt) / (1000 * 60 * 60 * 24);\n return ageDays > maxAgeDays;\n}\n\n/** True when an edition is compatible with a language code (missing languages match anything). */\nfunction editionMatchesLanguage(edition: OpenLibraryEdition, code: string): boolean {\n if (!edition.languages || edition.languages.length === 0) {\n return true;\n }\n return edition.languages.some((language) => language.key === `/languages/${code}`);\n}\n\n/** True when an edition has both a cover and a page count. */\nfunction editionIsComplete(edition: OpenLibraryEdition): boolean {\n return (\n edition.covers !== undefined &&\n edition.covers.length > 0 &&\n edition.number_of_pages !== undefined\n );\n}\n\n/**\n * Pick a representative edition from a work's editions list, applying a sequence of\n * filters that each degrade gracefully: format match (when a format is declared),\n * language preference (first preferred language with any match wins), completeness,\n * then recency. Falls back to the first remaining edition when no year resolves a tie.\n *\n * The caller fills in all preference defaults before calling, so this function never\n * has to reason about unset fields.\n */\nexport function pickEdition(\n editions: OpenLibraryEdition[],\n preferences: Required<EditionPreferences>,\n entryFormat?: ReadFormat,\n): OpenLibraryEdition | undefined {\n if (editions.length === 0) {\n return undefined;\n }\n\n let candidates = editions;\n\n // 1. Format match (only when the entry declares a format).\n if (entryFormat !== undefined) {\n const matches = candidates.filter((e) => inferFormat(e.physical_format) === entryFormat);\n if (matches.length > 0) {\n candidates = matches;\n }\n }\n\n // 2. Language preference: first preferred language with at least one match wins.\n for (const code of preferences.languages) {\n const matches = candidates.filter((e) => editionMatchesLanguage(e, code));\n if (matches.length > 0) {\n candidates = matches;\n break;\n }\n }\n\n // 3. Completeness.\n if (preferences.preferComplete) {\n const matches = candidates.filter(editionIsComplete);\n if (matches.length > 0) {\n candidates = matches;\n }\n }\n\n // 4. Recency, among editions with a parseable year.\n const dated = candidates\n .map((edition) => ({ edition, year: extractYear(edition.publish_date) }))\n .filter(\n (item): item is { edition: OpenLibraryEdition; year: number } => item.year !== undefined,\n )\n .sort((a, b) => a.year - b.year);\n if (dated.length > 0) {\n return preferences.preferRecent ? dated[dated.length - 1].edition : dated[0].edition;\n }\n\n // 5. Fallback: the first remaining candidate.\n return candidates[0];\n}\n\n/**\n * Apply enrichment data to an entry without overwriting fields the entry already has.\n * User-provided title and author always win; Open Library fills only the gaps.\n */\nexport function mergeEnrichment(entry: ReadEntry, data: EnrichmentData): ReadEntry {\n const merged: ReadEntry = { ...entry };\n if (merged.coverUrl === undefined && data.coverUrl !== undefined) {\n merged.coverUrl = data.coverUrl;\n }\n if (merged.pageCount === undefined && data.pageCount !== undefined) {\n merged.pageCount = data.pageCount;\n }\n if (merged.publishYear === undefined && data.publishYear !== undefined) {\n merged.publishYear = data.publishYear;\n }\n if (merged.subjects === undefined && data.subjects !== undefined) {\n merged.subjects = data.subjects;\n }\n if (merged.olid === undefined && data.olid !== undefined) {\n merged.olid = data.olid;\n }\n if (merged.format === undefined && data.format !== undefined) {\n merged.format = data.format;\n }\n return merged;\n}\n\n/** Sleep for the given number of milliseconds. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Normalize an OLID for lookup keys and endpoint construction: trim and uppercase. */\nfunction normalizeOlid(olid: string): string {\n return olid.trim().toUpperCase();\n}\n\n/** Strip the `/works/` (or `/books/`, `/authors/`) prefix from a key, leaving the OLID. */\nfunction olidFromKey(key: string): string | undefined {\n const olid = key.split('/').pop();\n return olid && olid.length > 0 ? olid : undefined;\n}\n\n/** Narrow an unknown JSON value to the edition fields we read. */\nfunction asEdition(data: unknown): OpenLibraryEdition {\n if (!isPlainObject(data)) {\n return {};\n }\n const edition: OpenLibraryEdition = {};\n if (isString(data.physical_format)) {\n edition.physical_format = data.physical_format;\n }\n if (Array.isArray(data.covers)) {\n edition.covers = data.covers.filter((cover): cover is number => typeof cover === 'number');\n }\n if (typeof data.number_of_pages === 'number') {\n edition.number_of_pages = data.number_of_pages;\n }\n if (isString(data.publish_date)) {\n edition.publish_date = data.publish_date;\n }\n if (Array.isArray(data.languages)) {\n edition.languages = data.languages\n .filter(isPlainObject)\n .map((language) => language.key)\n .filter(isString)\n .map((key) => ({ key }));\n }\n if (Array.isArray(data.works)) {\n edition.works = data.works\n .filter(isPlainObject)\n .map((work) => work.key)\n .filter(isString)\n .map((key) => ({ key }));\n }\n return edition;\n}\n\n/** Narrow a `/works/{olid}/editions.json` response to an editions array. */\nfunction asEditionsList(data: unknown): OpenLibraryEdition[] {\n if (isPlainObject(data) && Array.isArray(data.entries)) {\n return data.entries.map(asEdition);\n }\n return [];\n}\n\n/** Narrow a work response to its subjects array, if any non-empty strings are present. */\nfunction asSubjects(data: unknown): string[] | undefined {\n if (isPlainObject(data) && Array.isArray(data.subjects)) {\n const subjects = data.subjects.filter(isString);\n return subjects.length > 0 ? subjects : undefined;\n }\n return undefined;\n}\n\n/** The first doc of a `/search.json` response, narrowed to the fields we read. */\ninterface SearchDoc {\n workOlid?: string;\n coverId?: number;\n subjects?: string[];\n}\n\n/** Narrow a `/search.json` response to its first doc's relevant fields. */\nfunction asSearchDoc(data: unknown): SearchDoc | undefined {\n if (!isPlainObject(data) || !Array.isArray(data.docs) || data.docs.length === 0) {\n return undefined;\n }\n const doc = data.docs[0];\n if (!isPlainObject(doc)) {\n return undefined;\n }\n const result: SearchDoc = {};\n if (isString(doc.key)) {\n result.workOlid = olidFromKey(doc.key);\n }\n if (typeof doc.cover_i === 'number') {\n result.coverId = doc.cover_i;\n }\n if (Array.isArray(doc.subject)) {\n const subjects = doc.subject.filter(isString);\n if (subjects.length > 0) {\n result.subjects = subjects;\n }\n }\n return result;\n}\n\n/** The work OLID linked from an edition's `works` array, if present. */\nfunction workOlidFromEdition(edition: OpenLibraryEdition): string | undefined {\n const key = edition.works?.[0]?.key;\n return key ? olidFromKey(key) : undefined;\n}\n\n/** Apply an edition's fields to the accumulating enrichment data, filling only gaps. */\nfunction applyEdition(edition: OpenLibraryEdition, data: EnrichmentData): void {\n if (data.coverUrl === undefined && edition.covers && edition.covers.length > 0) {\n data.coverUrl = coverUrlFromId(edition.covers[0]);\n }\n if (data.pageCount === undefined && edition.number_of_pages !== undefined) {\n data.pageCount = edition.number_of_pages;\n }\n if (data.publishYear === undefined) {\n const year = extractYear(edition.publish_date);\n if (year !== undefined) {\n data.publishYear = year;\n }\n }\n if (data.format === undefined) {\n const format = inferFormat(edition.physical_format);\n if (format !== undefined) {\n data.format = format;\n }\n }\n}\n\n/** Whether a string is present and non-whitespace. */\nfunction isNonEmpty(value: string | undefined): value is string {\n return value !== undefined && value.trim() !== '';\n}\n\nconst OPEN_LIBRARY = 'https://openlibrary.org';\n\n/**\n * Enrich a single ReadEntry with metadata from Open Library, using and updating the\n * provided cache. See the module docs and EnrichOptions for the lookup strategy.\n *\n * User-provided title and author always win; Open Library fills only missing fields.\n * The cache is mutated in place and is only written when every request in the lookup\n * sequence succeeded (a 2xx, parseable response). Transport and HTTP failures push a\n * warning, leave the cache untouched (so the next build retries), and return whatever\n * partial enrichment was gathered.\n */\nexport async function enrich(entry: ReadEntry, options: EnrichOptions): Promise<EnrichResult> {\n const warnings: string[] = [];\n const fetchImpl = options.fetchImpl ?? globalThis.fetch;\n const maxAgeDays = options.maxAgeDays ?? 180;\n const rateLimitMs = options.rateLimitMs ?? 1000;\n const bust = new Set(options.bust ?? []);\n const preferences: Required<EditionPreferences> = {\n languages: options.editionPreferences?.languages ?? ['eng'],\n preferComplete: options.editionPreferences?.preferComplete ?? true,\n preferRecent: options.editionPreferences?.preferRecent ?? true,\n };\n\n const label = entry.title;\n\n // Resolve the lookup key and the path we will take.\n let lookupKey: string;\n if (isNonEmpty(entry.olid)) {\n lookupKey = `olid:${normalizeOlid(entry.olid)}`;\n } else if (isNonEmpty(entry.isbn)) {\n lookupKey = `isbn:${normalizeIsbn(entry.isbn)}`;\n } else if (isNonEmpty(entry.title) && isNonEmpty(entry.author)) {\n lookupKey = `title-author:${hashTitleAuthor(entry.title, entry.author)}`;\n } else {\n warnings.push(`Entry '${label}': no enrichment path (missing olid, isbn, and title+author)`);\n return { entry, warnings, matchQuality: 'unmatched' };\n }\n\n const hasTitleAndAuthor = isNonEmpty(entry.title) && isNonEmpty(entry.author);\n\n // Cache read. A fresh entry that is not busted is reused as-is. A notFound\n // entry (data is {}) merges to a no-op, so the entry is returned unenriched\n // with no fetch and no warning. The one wrinkle is the ISBN-404 fallback\n // (see below): a fallback hit caches the result under the title-author key and\n // a notFound under the ISBN key, so when an ISBN is a known dead end we look to\n // the title-author key for a cached fallback result before giving up.\n const usable = (c: CacheEntry | undefined): c is CacheEntry =>\n c !== undefined && !bust.has(c.lookupKey) && !shouldRefetch(c, maxAgeDays);\n if (!options.ignoreCache) {\n const cached = options.cache[lookupKey];\n if (usable(cached)) {\n if (cached.notFound && lookupKey.startsWith('isbn:') && hasTitleAndAuthor) {\n const fallbackKey = `title-author:${hashTitleAuthor(entry.title, entry.author)}`;\n const fallback = options.cache[fallbackKey];\n if (usable(fallback)) {\n return {\n entry: mergeEnrichment(entry, fallback.data),\n warnings,\n matchQuality: fallback.matchQuality,\n };\n }\n }\n return {\n entry: mergeEnrichment(entry, cached.data),\n warnings,\n matchQuality: cached.matchQuality,\n };\n }\n }\n\n // Rate limiter: sleep until the next allowed time before each request. When a\n // shared rateLimiterState is provided, the budget is coordinated across calls;\n // otherwise a per-call state object is used (fresh on every call).\n const rateLimiter = options.rateLimiterState ?? { nextAllowedAt: 0 };\n let anyFailure = false;\n const data: EnrichmentData = {};\n\n const today = new Date().toISOString().slice(0, 10);\n /** Cache a successful (possibly sparse) enrichment under a key. */\n const writePositive = (key: string, quality: 'exact' | 'fuzzy'): void => {\n options.cache[key] = { fetchedAt: today, lookupKey: key, data, matchQuality: quality };\n };\n /** Cache a definitive 404 (\"Open Library has no record\") under a key. */\n const writeNotFound = (key: string): void => {\n options.cache[key] = {\n fetchedAt: today,\n lookupKey: key,\n data: {},\n notFound: true,\n matchQuality: 'unmatched',\n };\n };\n\n type Fetched = { ok: true; data: unknown } | { ok: false; notFound: boolean };\n\n /**\n * Fetch a JSON endpoint with the shared rate limit. A 404 is a definitive\n * \"not found\": it returns `{ ok: false, notFound: true }` without a warning\n * or setting anyFailure, because what a 404 means depends on the caller (a\n * primary lookup caches it as a dead end; a secondary lookup just yields no\n * extra data). Transport errors, other non-2xx, and parse failures push a\n * warning and set anyFailure so the positive cache is skipped and the next\n * build retries.\n */\n const fetchJson = async (url: string): Promise<Fetched> => {\n const now = Date.now();\n if (now < rateLimiter.nextAllowedAt) {\n await sleep(rateLimiter.nextAllowedAt - now);\n }\n rateLimiter.nextAllowedAt = Date.now() + rateLimitMs;\n\n let response: Response;\n try {\n response = await fetchImpl(url, { headers: { 'User-Agent': options.userAgent } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n warnings.push(`Entry '${label}': failed to reach Open Library (${message})`);\n anyFailure = true;\n return { ok: false, notFound: false };\n }\n if (response.status === 404) {\n return { ok: false, notFound: true };\n }\n if (!response.ok) {\n // Non-404 HTTP failure. The orchestrator's isTransportFailure() matches\n // this \"returned {status}\" template (and excludes 404); keep them in sync.\n warnings.push(`Entry '${label}': Open Library returned ${response.status} for ${url}`);\n anyFailure = true;\n return { ok: false, notFound: false };\n }\n try {\n return { ok: true, data: await response.json() };\n } catch {\n warnings.push(`Entry '${label}': Open Library returned an unparseable response for ${url}`);\n anyFailure = true;\n return { ok: false, notFound: false };\n }\n };\n\n /** Fetch a work and apply its subjects. Secondary lookup; a miss is non-fatal. */\n const fetchWork = async (workOlid: string): Promise<void> => {\n const result = await fetchJson(`${OPEN_LIBRARY}/works/${workOlid}.json`);\n if (!result.ok) {\n return;\n }\n data.olid = workOlid;\n const subjects = asSubjects(result.data);\n if (data.subjects === undefined && subjects !== undefined) {\n data.subjects = subjects;\n }\n };\n\n /** Fetch a work's editions list and apply a representative edition. Secondary; a miss is non-fatal. */\n const fetchRepresentativeEdition = async (workOlid: string): Promise<void> => {\n const result = await fetchJson(`${OPEN_LIBRARY}/works/${workOlid}/editions.json`);\n if (!result.ok) {\n return;\n }\n const picked = pickEdition(asEditionsList(result.data), preferences, entry.format);\n if (picked !== undefined) {\n applyEdition(picked, data);\n }\n };\n\n /**\n * Run the title+author search, applying any matched doc to `data`. Returns\n * 'found' (a doc matched and was applied), 'empty' (a clean response with no\n * usable doc: the book is not in Open Library), or 'failed' (a transport/HTTP/\n * parse problem already warned; leave the cache untouched for a retry). The\n * caller pushes the verify-this warning, since the wording differs between a\n * primary search and an ISBN-404 fallback.\n */\n const searchTitleAuthor = async (): Promise<'found' | 'empty' | 'failed'> => {\n const url =\n `${OPEN_LIBRARY}/search.json?title=${encodeURIComponent(entry.title)}` +\n `&author=${encodeURIComponent(entry.author)}&limit=1`;\n const result = await fetchJson(url);\n if (!result.ok) {\n return result.notFound ? 'empty' : 'failed';\n }\n const doc = asSearchDoc(result.data);\n if (doc === undefined) {\n return 'empty';\n }\n if (doc.subjects !== undefined) {\n data.subjects = doc.subjects;\n }\n if (doc.workOlid !== undefined) {\n data.olid = doc.workOlid;\n await fetchRepresentativeEdition(doc.workOlid);\n }\n if (data.coverUrl === undefined && doc.coverId !== undefined) {\n data.coverUrl = coverUrlFromId(doc.coverId);\n }\n return 'found';\n };\n\n const noRecord = `Entry '${label}': Open Library has no record for ${lookupKey}`;\n // The entry's overall match quality, distinct from any per-key cache flag. A\n // transport failure leaves it undefined: we genuinely don't know yet, and the\n // next build retries.\n let matchQuality: 'exact' | 'fuzzy' | 'unmatched' | undefined;\n\n if (isNonEmpty(entry.olid)) {\n const olid = normalizeOlid(entry.olid);\n const url = olid.endsWith('W')\n ? `${OPEN_LIBRARY}/works/${olid}.json`\n : `${OPEN_LIBRARY}/books/${olid}.json`;\n const result = await fetchJson(url);\n if (result.ok) {\n if (olid.endsWith('W')) {\n // Work OLID: subjects from the work, then a representative edition.\n data.olid = olid;\n const subjects = asSubjects(result.data);\n if (subjects !== undefined) {\n data.subjects = subjects;\n }\n await fetchRepresentativeEdition(olid);\n } else {\n // Edition OLID: the edition, then the linked work for subjects.\n const edition = asEdition(result.data);\n applyEdition(edition, data);\n const workOlid = workOlidFromEdition(edition);\n if (workOlid !== undefined) {\n await fetchWork(workOlid);\n }\n }\n matchQuality = 'exact';\n if (!anyFailure) {\n writePositive(lookupKey, 'exact');\n }\n } else if (result.notFound) {\n warnings.push(noRecord);\n writeNotFound(lookupKey);\n matchQuality = 'unmatched';\n }\n // Transport/HTTP/parse failure: already warned; leave the cache for a retry.\n } else if (isNonEmpty(entry.isbn)) {\n const isbn = normalizeIsbn(entry.isbn);\n const result = await fetchJson(`${OPEN_LIBRARY}/isbn/${isbn}.json`);\n if (result.ok) {\n const edition = asEdition(result.data);\n applyEdition(edition, data);\n const workOlid = workOlidFromEdition(edition);\n if (workOlid !== undefined) {\n await fetchWork(workOlid);\n }\n matchQuality = 'exact';\n if (!anyFailure) {\n writePositive(lookupKey, 'exact');\n }\n } else if (result.notFound) {\n // The exact ISBN is not in Open Library. Libby ships audiobook ISBNs that\n // Open Library rarely indexes, so fall back to title+author when we have\n // both. The two writes carry a dual intent, kept side by side: the entity\n // is cached under its title-author key, and the ISBN lookup is remembered\n // as a dead end so future builds skip it.\n if (hasTitleAndAuthor) {\n const fallbackKey = `title-author:${hashTitleAuthor(entry.title, entry.author)}`;\n const outcome = await searchTitleAuthor();\n if (outcome === 'found') {\n warnings.push(\n `Entry '${label}': ISBN not found in Open Library; fell back to title+author search`,\n );\n matchQuality = 'fuzzy';\n if (!anyFailure) {\n writePositive(fallbackKey, 'fuzzy');\n }\n writeNotFound(lookupKey);\n } else if (outcome === 'empty') {\n warnings.push(noRecord);\n writeNotFound(lookupKey);\n writeNotFound(fallbackKey);\n matchQuality = 'unmatched';\n }\n // outcome 'failed': transport problem on the fallback; leave for a retry.\n } else {\n warnings.push(noRecord);\n writeNotFound(lookupKey);\n matchQuality = 'unmatched';\n }\n }\n } else {\n const outcome = await searchTitleAuthor();\n if (outcome === 'found') {\n warnings.push(\n `Entry '${label}': matched by fuzzy title+author search; verify the result and consider adding an olid override`,\n );\n matchQuality = 'fuzzy';\n if (!anyFailure) {\n writePositive(lookupKey, 'fuzzy');\n }\n } else if (outcome === 'empty') {\n warnings.push(noRecord);\n writeNotFound(lookupKey);\n matchQuality = 'unmatched';\n }\n }\n\n return { entry: mergeEnrichment(entry, data), warnings, matchQuality };\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { RawExtrasEntry, ReadFormat, ReadStatus } from './types.js';\n\n/**\n * Result of parsing an extras file. Entries is the successfully-parsed and\n * validated entries; warnings is a list of soft failures collected during\n * parsing (malformed entries, unknown fields, bad dates).\n */\nexport interface ParseExtrasResult {\n entries: RawExtrasEntry[];\n warnings: string[];\n}\n\nconst READ_STATUSES: readonly ReadStatus[] = ['borrowed', 'reading', 'finished', 'abandoned'];\nconst READ_FORMATS: readonly ReadFormat[] = ['audiobook', 'ebook', 'physical'];\n\n/** Fields the schema knows about. Anything else triggers an unknown-field warning. */\nconst KNOWN_FIELDS = new Set<string>([\n 'isbn',\n 'olid',\n 'title',\n 'author',\n 'status',\n 'format',\n 'source',\n 'startedAt',\n 'finishedAt',\n 'borrowedAt',\n 'notes',\n 'private',\n]);\n\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nfunction isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\n/** A present, non-whitespace string. Whitespace-only counts as empty. */\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '';\n}\n\nfunction isBoolean(value: unknown): value is boolean {\n return typeof value === 'boolean';\n}\n\nfunction isPresent(value: unknown): boolean {\n return value !== undefined && value !== null;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isReadStatus(value: unknown): value is ReadStatus {\n return isString(value) && (READ_STATUSES as readonly string[]).includes(value);\n}\n\nfunction isReadFormat(value: unknown): value is ReadFormat {\n return isString(value) && (READ_FORMATS as readonly string[]).includes(value);\n}\n\n/** Render a value for a warning message: strings in single quotes, others as JSON. */\nfunction show(value: unknown): string {\n return isString(value) ? `'${value}'` : JSON.stringify(value);\n}\n\n/** Human-readable label for an entry in warnings: title, else isbn, else position. */\nfunction entryLabel(item: Record<string, unknown>, index: number): string {\n if (isNonEmptyString(item.title)) {\n return `'${item.title}'`;\n }\n if (isNonEmptyString(item.isbn)) {\n return `'${item.isbn}'`;\n }\n return `#${index + 1}`;\n}\n\n/** Describe the JSON-ish type of a value for the root-not-a-list warning. */\nfunction describeType(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (Array.isArray(value)) {\n return 'an array';\n }\n return `a ${typeof value}`;\n}\n\n/**\n * Validate a present date value against strict ISO YYYY-MM-DD, rejecting\n * rollover dates like 2026-13-45 and non-dates like 2026-02-30. On failure a\n * warning is pushed and undefined is returned.\n */\nfunction validateDate(\n value: unknown,\n field: string,\n label: string,\n warnings: string[],\n): string | undefined {\n if (isString(value) && ISO_DATE.test(value)) {\n const [year, month, day] = value.split('-').map(Number);\n const date = new Date(Date.UTC(year, month - 1, day));\n if (\n date.getUTCFullYear() === year &&\n date.getUTCMonth() === month - 1 &&\n date.getUTCDate() === day\n ) {\n return value;\n }\n }\n warnings.push(`Entry ${label}: '${field}' must be ISO YYYY-MM-DD, got ${show(value)}`);\n return undefined;\n}\n\n/**\n * Validate one raw item against the extras schema. Returns a RawExtrasEntry on\n * success, or undefined (with a warning pushed) when the item is rejected.\n * Unknown fields produce warnings but do not reject the entry.\n */\nfunction validateEntry(\n item: unknown,\n index: number,\n warnings: string[],\n): RawExtrasEntry | undefined {\n if (!isPlainObject(item)) {\n warnings.push(`Entry #${index + 1}: entry must be an object`);\n return undefined;\n }\n\n const label = entryLabel(item, index);\n\n if (!isReadStatus(item.status)) {\n if (isPresent(item.status)) {\n warnings.push(\n `Entry ${label}: 'status' must be one of ${READ_STATUSES.join(', ')}, got ${show(item.status)}`,\n );\n } else {\n warnings.push(`Entry ${label}: missing required field 'status'`);\n }\n return undefined;\n }\n const status = item.status;\n\n const hasIsbn = isNonEmptyString(item.isbn);\n const hasTitleAndAuthor = isNonEmptyString(item.title) && isNonEmptyString(item.author);\n if (!hasIsbn && !hasTitleAndAuthor) {\n warnings.push(`Entry ${label}: must have either 'isbn' or both 'title' and 'author'`);\n return undefined;\n }\n\n const entry: RawExtrasEntry = { status };\n\n for (const field of ['isbn', 'olid', 'title', 'author', 'notes'] as const) {\n const value = item[field];\n if (!isPresent(value)) {\n continue;\n }\n if (!isNonEmptyString(value)) {\n warnings.push(`Entry ${label}: '${field}' must be a non-empty string`);\n return undefined;\n }\n entry[field] = value;\n }\n\n if (isPresent(item.format)) {\n if (!isReadFormat(item.format)) {\n warnings.push(\n `Entry ${label}: 'format' must be one of ${READ_FORMATS.join(', ')}, got ${show(item.format)}`,\n );\n return undefined;\n }\n entry.format = item.format;\n }\n\n if (isPresent(item.source)) {\n if (!isNonEmptyString(item.source)) {\n warnings.push(`Entry ${label}: 'source' must be a non-empty string`);\n return undefined;\n }\n entry.source = item.source;\n }\n\n for (const field of ['startedAt', 'finishedAt', 'borrowedAt'] as const) {\n if (!isPresent(item[field])) {\n continue;\n }\n const validated = validateDate(item[field], field, label, warnings);\n if (validated === undefined) {\n return undefined;\n }\n entry[field] = validated;\n }\n\n if (isPresent(item.private)) {\n if (!isBoolean(item.private)) {\n warnings.push(`Entry ${label}: 'private' must be a boolean`);\n return undefined;\n }\n entry.private = item.private;\n }\n\n for (const key of Object.keys(item)) {\n if (!KNOWN_FIELDS.has(key)) {\n warnings.push(`Entry ${label}: unknown field '${key}'`);\n }\n }\n\n return entry;\n}\n\n/**\n * Parse the text content of an extras file into raw entries.\n *\n * The file must be a list (YAML sequence or JSON array) of entry objects.\n * Each entry is validated against the schema; entries that fail validation\n * are skipped with a warning rather than throwing. A completely malformed\n * file (invalid YAML/JSON, root not a list) returns empty entries with one\n * warning describing the failure.\n *\n * @param content the raw text of the extras file\n * @param format 'yaml' or 'json'\n * @returns a ParseExtrasResult with successfully-validated entries and warnings\n */\nexport function parseExtras(content: string, format: 'yaml' | 'json'): ParseExtrasResult {\n const warnings: string[] = [];\n\n let parsed: unknown;\n try {\n parsed = format === 'yaml' ? parseYaml(content) : JSON.parse(content);\n } catch (error) {\n const kind = format === 'yaml' ? 'YAML' : 'JSON';\n const message = error instanceof Error ? error.message : String(error);\n return { entries: [], warnings: [`Invalid ${kind}: ${message}`] };\n }\n\n if (!Array.isArray(parsed)) {\n return {\n entries: [],\n warnings: [`Extras root must be a list of entries, got ${describeType(parsed)}`],\n };\n }\n\n const entries: RawExtrasEntry[] = [];\n for (let i = 0; i < parsed.length; i++) {\n const entry = validateEntry(parsed[i], i, warnings);\n if (entry !== undefined) {\n entries.push(entry);\n }\n }\n\n return { entries, warnings };\n}\n","import { parse } from 'csv-parse/sync';\nimport type { RawLibbyEntry } from './types.js';\n\n/**\n * Result of parsing a Libby CSV. Entries is the successfully-parsed rows;\n * warnings is a list of soft failures (malformed rows, unparseable dates)\n * collected during parsing. Parsing does NOT throw on a single bad row;\n * one weird entry should not kill the whole build.\n */\nexport interface ParseLibbyResult {\n entries: RawLibbyEntry[];\n warnings: string[];\n}\n\n/** A single CSV row keyed by header name. Every field may be absent or empty. */\ntype LibbyRow = Partial<Record<string, string>>;\n\n/** The exact columns a Libby all-loans export is expected to have, in order. */\nconst EXPECTED_COLUMNS = [\n 'cover',\n 'title',\n 'author',\n 'publisher',\n 'isbn',\n 'timestamp',\n 'activity',\n 'library',\n 'details',\n] as const;\n\n/**\n * Pull the first non-empty line of the CSV and split it into trimmed,\n * lower-cased column names. Returns undefined when there is no such line.\n * Header names never contain commas, so a naive split is sufficient.\n */\nfunction extractHeaderColumns(csv: string): string[] | undefined {\n for (const line of csv.split(/\\r?\\n/)) {\n if (line.trim() === '') {\n continue;\n }\n return line.split(',').map((column) => column.trim().toLowerCase());\n }\n return undefined;\n}\n\n/** Full English month names to their two-digit number, for date normalization. */\nconst MONTHS = new Map<string, string>([\n ['January', '01'],\n ['February', '02'],\n ['March', '03'],\n ['April', '04'],\n ['May', '05'],\n ['June', '06'],\n ['July', '07'],\n ['August', '08'],\n ['September', '09'],\n ['October', '10'],\n ['November', '11'],\n ['December', '12'],\n]);\n\n/** Trim a field and treat an empty result as missing. */\nfunction clean(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * Normalize a Libby timestamp (\"Month DD, YYYY HH:MM\", US locale) to an ISO\n * date string (YYYY-MM-DD). The time is dropped. Returns undefined when the\n * value is missing or does not match the expected shape.\n */\nfunction toIsoDate(timestamp: string | undefined): string | undefined {\n if (timestamp === undefined) {\n return undefined;\n }\n const match = /^([A-Za-z]+)\\s+(\\d{1,2}),\\s+(\\d{4})\\b/.exec(timestamp.trim());\n if (match === null) {\n return undefined;\n }\n const [, monthName, day, year] = match;\n if (monthName === undefined || day === undefined || year === undefined) {\n return undefined;\n }\n const month = MONTHS.get(monthName);\n if (month === undefined) {\n return undefined;\n }\n return `${year}-${month}-${day.padStart(2, '0')}`;\n}\n\n/**\n * Parse the text of a Libby timeline CSV export into raw entries.\n *\n * Dates are normalized to ISO YYYY-MM-DD. Empty-string fields become undefined.\n * Trailing whitespace and blank lines are tolerated. Rows missing a title or\n * with an unparseable date are skipped with a warning rather than throwing.\n *\n * @param csv the raw text content of a libbytimeline-all-loans.csv file\n * @returns a ParseLibbyResult with successfully-parsed entries and any warnings\n */\nexport function parseLibbyCsv(csv: string): ParseLibbyResult {\n const entries: RawLibbyEntry[] = [];\n const warnings: string[] = [];\n\n const header = extractHeaderColumns(csv);\n if (header === undefined) {\n warnings.push(\n `Header missing: expected [${EXPECTED_COLUMNS.join(', ')}] but the CSV had no header row`,\n );\n return { entries, warnings };\n }\n const headerMatches =\n header.length === EXPECTED_COLUMNS.length &&\n header.every((column, index) => column === EXPECTED_COLUMNS[index]);\n if (!headerMatches) {\n warnings.push(\n `Header mismatch: expected [${EXPECTED_COLUMNS.join(', ')}] but found [${header.join(', ')}]`,\n );\n return { entries, warnings };\n }\n\n const parsed: unknown = parse(csv, {\n // Normalize header keys to trimmed lower case so a case-only difference in\n // the export header still maps each column to the field we read below.\n columns: (record: string[]) => record.map((column) => column.trim().toLowerCase()),\n skip_empty_lines: true,\n trim: false,\n relax_column_count: true,\n });\n const rows: LibbyRow[] = Array.isArray(parsed) ? (parsed as LibbyRow[]) : [];\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (row === undefined) {\n continue;\n }\n\n const title = clean(row.title);\n if (title === undefined) {\n warnings.push(`Skipping row ${i + 1}: missing title`);\n continue;\n }\n\n const borrowedAt = toIsoDate(row.timestamp);\n if (borrowedAt === undefined) {\n warnings.push(`Skipping \"${title}\": unparseable date ${JSON.stringify(row.timestamp ?? '')}`);\n continue;\n }\n\n entries.push({\n cover: clean(row.cover),\n title,\n author: clean(row.author),\n publisher: clean(row.publisher),\n isbn: clean(row.isbn),\n borrowedAt,\n activity: clean(row.activity) ?? '',\n library: clean(row.library),\n details: clean(row.details),\n });\n }\n\n return { entries, warnings };\n}\n","import { readFile, rename, rm, writeFile } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport { type Cache, type EditionPreferences, enrich } from './enrich.js';\nimport { parseExtras } from './extras.js';\nimport { parseLibbyCsv } from './libby.js';\nimport type { RawExtrasEntry, RawLibbyEntry, ReadEntry, ReadResult, ReadStatus } from './types.js';\n\nexport interface LibbyInput {\n path?: string;\n content?: string;\n fetch?: () => Promise<string>;\n}\n\nexport interface ExtrasInput {\n path?: string;\n content?: string;\n /** Required when using `content`; inferred from the path extension when using `path`. */\n format?: 'yaml' | 'json';\n fetch?: () => Promise<{ content: string; format: 'yaml' | 'json' }>;\n}\n\nexport interface CacheConfig {\n path: string;\n maxAgeDays?: number;\n bust?: string[];\n ignoreReads?: boolean;\n}\n\nexport interface GetReadsOptions {\n libby?: LibbyInput;\n extras?: ExtrasInput;\n cache?: CacheConfig;\n /** Required: Open Library asks for identifying requests. */\n userAgent: string;\n /** Skip Open Library enrichment entirely. Default false. */\n skipEnrichment?: boolean;\n /** Include entries marked `private: true`. Default false. */\n includePrivate?: boolean;\n /** Cap on returned entries after sort. Default unbounded. */\n limit?: number;\n /** Edition picker preferences passed through to the enricher. */\n editionPreferences?: EditionPreferences;\n /** Override fetch (testability). Passed through to the enricher. */\n fetchImpl?: typeof globalThis.fetch;\n /** Min ms between Open Library requests. Default 1000. */\n rateLimitMs?: number;\n}\n\n/** Map `.yaml`/`.yml` to yaml, `.json` to json, anything else to undefined. */\nfunction inferFormatFromPath(path: string): 'yaml' | 'json' | undefined {\n const ext = extname(path).toLowerCase();\n if (ext === '.yaml' || ext === '.yml') {\n return 'yaml';\n }\n if (ext === '.json') {\n return 'json';\n }\n return undefined;\n}\n\n/** Resolve a LibbyInput to its raw text content, dispatching on the mode provided. */\nasync function resolveLibbyInput(\n input: LibbyInput,\n): Promise<{ content?: string; warnings: string[] }> {\n if (input.content !== undefined) {\n return { content: input.content, warnings: [] };\n }\n if (input.path !== undefined) {\n return { content: await readFile(input.path, 'utf-8'), warnings: [] };\n }\n if (input.fetch !== undefined) {\n return { content: await input.fetch(), warnings: [] };\n }\n return { warnings: ['Libby input provided but none of `path`, `content`, or `fetch` was set'] };\n}\n\n/** Resolve an ExtrasInput to its raw content and format, dispatching on the mode provided. */\nasync function resolveExtrasInput(\n input: ExtrasInput,\n): Promise<{ content?: string; format?: 'yaml' | 'json'; warnings: string[] }> {\n if (input.content !== undefined) {\n if (input.format === undefined) {\n return {\n warnings: [\"Extras `content` requires an explicit `format` ('yaml' or 'json')\"],\n };\n }\n return { content: input.content, format: input.format, warnings: [] };\n }\n if (input.path !== undefined) {\n const format = inferFormatFromPath(input.path);\n if (format === undefined) {\n return {\n warnings: [\n `Extras path ${input.path} has an unknown extension; expected .yaml, .yml, or .json`,\n ],\n };\n }\n return { content: await readFile(input.path, 'utf-8'), format, warnings: [] };\n }\n if (input.fetch !== undefined) {\n const { content, format } = await input.fetch();\n return { content, format, warnings: [] };\n }\n return { warnings: ['Extras input provided but none of `path`, `content`, or `fetch` was set'] };\n}\n\n/**\n * Read and parse the cache file. A missing file is the normal first-build case\n * (empty cache, no warning). Malformed JSON or any other read error yields an\n * empty cache plus a warning, so a bad cache file never blocks a build.\n */\nasync function readCacheFile(path: string): Promise<{ cache: Cache; warnings: string[] }> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n return { cache: {}, warnings: [] };\n }\n const message = error instanceof Error ? error.message : String(error);\n return {\n cache: {},\n warnings: [`Cache file ${path} could not be read (${message}); starting fresh`],\n };\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n return { cache: parsed as Cache, warnings: [] };\n } catch {\n return { cache: {}, warnings: [`Cache file ${path} is not valid JSON; starting fresh`] };\n }\n}\n\n/**\n * Write the cache to disk atomically: write a sibling tmp file, then rename it\n * over the target (atomic on POSIX and modern Windows). On failure the tmp file\n * is cleaned up and the error is re-thrown.\n */\nasync function writeCacheFile(path: string, cache: Cache): Promise<void> {\n const tmpPath = `${path}.tmp`;\n try {\n await writeFile(tmpPath, JSON.stringify(cache, null, 2), 'utf-8');\n await rename(tmpPath, path);\n } catch (error) {\n await rm(tmpPath, { force: true });\n throw error;\n }\n}\n\n/** Convert a raw Libby row into a borrowed ReadEntry. sortDate is filled later. */\nfunction normalizeLibbyEntry(raw: RawLibbyEntry): ReadEntry {\n return {\n title: raw.title,\n author: raw.author ?? '',\n isbn: raw.isbn,\n status: 'borrowed',\n borrowedAt: raw.borrowedAt,\n library: raw.library,\n publisher: raw.publisher,\n source: 'library',\n sortDate: '',\n provenance: 'libby',\n };\n}\n\n/** Convert a raw extras entry into a ReadEntry, preserving every user field. sortDate filled later. */\nfunction normalizeExtrasEntry(raw: RawExtrasEntry): ReadEntry {\n return {\n title: raw.title ?? '',\n author: raw.author ?? '',\n isbn: raw.isbn,\n olid: raw.olid,\n status: raw.status,\n format: raw.format,\n startedAt: raw.startedAt,\n finishedAt: raw.finishedAt,\n borrowedAt: raw.borrowedAt,\n source: raw.source,\n notes: raw.notes,\n private: raw.private,\n sortDate: '',\n provenance: 'extras',\n };\n}\n\n/** Normalize an ISBN for matching: strip hyphens and whitespace, then lowercase. */\nfunction isbnMatchKey(isbn: string | undefined): string | undefined {\n if (isbn === undefined || isbn.trim() === '') {\n return undefined;\n }\n return isbn.replace(/[-\\s]/g, '').toLowerCase();\n}\n\n/**\n * Merge a Libby entry and an extras entry that share an ISBN into one entry.\n * The extras entry is canonical: its status wins (so a borrow can be promoted\n * to finished), and most fields prefer extras with Libby as fallback. Dates are\n * additive (a union), and Libby's library/publisher are always preserved.\n */\nfunction mergePair(libby: ReadEntry, extras: ReadEntry): ReadEntry {\n return {\n title: extras.title || libby.title,\n author: extras.author || libby.author,\n isbn: extras.isbn ?? libby.isbn,\n olid: extras.olid ?? libby.olid,\n status: extras.status,\n format: extras.format ?? libby.format,\n startedAt: extras.startedAt ?? libby.startedAt,\n finishedAt: extras.finishedAt ?? libby.finishedAt,\n borrowedAt: extras.borrowedAt ?? libby.borrowedAt,\n source: extras.source ?? libby.source,\n notes: extras.notes ?? libby.notes,\n private: extras.private ?? false,\n library: libby.library,\n publisher: libby.publisher,\n sortDate: '',\n provenance: 'extras',\n };\n}\n\n/**\n * Merge Libby and extras entries by normalized ISBN. Entries sharing an ISBN\n * are merged into one (extras canonical); entries without a counterpart, or\n * without an ISBN at all, pass through unchanged.\n *\n * `libbyCoverByEntry` carries the Libby cover fallback (see getReads). When a\n * merge consumes a Libby entry that had a fallback cover, the association is\n * forwarded to the new merged entry so the fallback survives the merge.\n */\nfunction mergeByIsbn(\n libby: ReadEntry[],\n extras: ReadEntry[],\n libbyCoverByEntry: Map<ReadEntry, string>,\n): { merged: ReadEntry[]; warnings: string[] } {\n const extrasByIsbn = new Map<string, ReadEntry>();\n for (const entry of extras) {\n const key = isbnMatchKey(entry.isbn);\n if (key !== undefined) {\n extrasByIsbn.set(key, entry);\n }\n }\n\n const consumed = new Set<string>();\n const merged: ReadEntry[] = [];\n\n for (const entry of libby) {\n const key = isbnMatchKey(entry.isbn);\n const match = key !== undefined ? extrasByIsbn.get(key) : undefined;\n if (key !== undefined && match !== undefined) {\n const mergedEntry = mergePair(entry, match);\n const libbyCover = libbyCoverByEntry.get(entry);\n if (libbyCover !== undefined) {\n libbyCoverByEntry.set(mergedEntry, libbyCover);\n }\n merged.push(mergedEntry);\n consumed.add(key);\n } else {\n merged.push(entry);\n }\n }\n\n for (const entry of extras) {\n const key = isbnMatchKey(entry.isbn);\n if (key !== undefined && consumed.has(key)) {\n continue;\n }\n merged.push(entry);\n }\n\n return { merged, warnings: [] };\n}\n\n/** Derive the canonical sort date for an entry from its status. */\nfunction computeSortDate(entry: ReadEntry): string {\n const status: ReadStatus = entry.status;\n if (status === 'finished' || status === 'abandoned') {\n return entry.finishedAt ?? entry.startedAt ?? '';\n }\n if (status === 'reading') {\n return entry.startedAt ?? '';\n }\n return entry.borrowedAt ?? '';\n}\n\n/**\n * Whether a warning signals a transport/HTTP failure (Open Library failing to\n * answer), as opposed to a definitive 404 (\"not found\"), a fuzzy match, or a\n * missing enrichment path. A 404 is a clean answer, not unavailability, so it\n * must NOT count toward the rollup. The enricher reports a primary 404 as\n * \"...has no record for...\" (not matched here), but a secondary 404 can still\n * surface as \"returned 404\", so the status matcher explicitly excludes 404.\n */\nfunction isTransportFailure(warning: string): boolean {\n if (warning.includes('failed to reach Open Library')) {\n return true;\n }\n if (warning.includes('Open Library returned an unparseable response')) {\n return true;\n }\n const httpMatch = /Open Library returned (\\d+) for/.exec(warning);\n return httpMatch !== null && httpMatch[1] !== '404';\n}\n\n/** Count the warnings that signal a transport/HTTP failure (excludes 404, fuzzy match, no path). */\nfunction countTransportFailures(warnings: string[]): number {\n return warnings.filter(isTransportFailure).length;\n}\n\n/**\n * The package's main entry point. Reads Libby and/or extras input, parses,\n * normalizes, enriches via Open Library, and returns a sorted, deduplicated,\n * privacy-filtered array of ReadEntry.\n *\n * At least one of `libby` or `extras` must be provided. Calling with neither\n * returns an empty result with a warning.\n *\n * @param options the source inputs, cache config, and behavior flags\n * @returns ReadResult with entries (sorted desc by sortDate), warnings, lastEntryDate\n */\nexport async function getReads(options: GetReadsOptions): Promise<ReadResult> {\n const warnings: string[] = [];\n\n if (options.libby === undefined && options.extras === undefined) {\n warnings.push('No input provided: pass at least one of `libby` or `extras`');\n return { entries: [], warnings };\n }\n\n // 1. Resolve inputs.\n let libbyContent: string | undefined;\n if (options.libby !== undefined) {\n const resolved = await resolveLibbyInput(options.libby);\n libbyContent = resolved.content;\n warnings.push(...resolved.warnings);\n }\n let extrasContent: string | undefined;\n let extrasFormat: 'yaml' | 'json' | undefined;\n if (options.extras !== undefined) {\n const resolved = await resolveExtrasInput(options.extras);\n extrasContent = resolved.content;\n extrasFormat = resolved.format;\n warnings.push(...resolved.warnings);\n }\n\n // 2. Parse.\n const libbyRaw: RawLibbyEntry[] = [];\n if (libbyContent !== undefined) {\n const parsed = parseLibbyCsv(libbyContent);\n libbyRaw.push(...parsed.entries);\n warnings.push(...parsed.warnings);\n }\n const extrasRaw: RawExtrasEntry[] = [];\n if (extrasContent !== undefined && extrasFormat !== undefined) {\n const parsed = parseExtras(extrasContent, extrasFormat);\n extrasRaw.push(...parsed.entries);\n warnings.push(...parsed.warnings);\n }\n\n // 3. Read cache (skipped when ignoreReads is set, but still written back later).\n let cache: Cache = {};\n if (options.cache !== undefined && options.cache.ignoreReads !== true) {\n const resolved = await readCacheFile(options.cache.path);\n cache = resolved.cache;\n warnings.push(...resolved.warnings);\n }\n\n // 4. Normalize. Stash each Libby row's cover URL keyed by the normalized\n // entry, so it can fall back into coverUrl after enrichment (it is NOT set\n // as coverUrl now, which would block Open Library from filling that field).\n const libbyCoverByEntry = new Map<ReadEntry, string>();\n const libbyEntries = libbyRaw.map((raw) => {\n const entry = normalizeLibbyEntry(raw);\n if (raw.cover !== undefined && raw.cover.trim() !== '') {\n libbyCoverByEntry.set(entry, raw.cover);\n }\n return entry;\n });\n\n // 5. Merge by ISBN (forwarding the cover fallback onto merged entries).\n const { merged, warnings: mergeWarnings } = mergeByIsbn(\n libbyEntries,\n extrasRaw.map(normalizeExtrasEntry),\n libbyCoverByEntry,\n );\n warnings.push(...mergeWarnings);\n\n // 6. Enrich, sharing rate-limiter state and tracking transport failures.\n let entries = merged;\n if (options.skipEnrichment !== true) {\n entries = await enrichAll(merged, cache, options, warnings, libbyCoverByEntry);\n }\n\n // 6b. Fall back to the Libby cover for any entry Open Library could not give\n // a cover (enrichment off, failed, or sparse). Open Library always got first\n // crack; this only fills a still-empty coverUrl.\n for (const entry of entries) {\n if (entry.coverUrl === undefined) {\n const libbyCover = libbyCoverByEntry.get(entry);\n if (libbyCover !== undefined) {\n entry.coverUrl = libbyCover;\n }\n }\n }\n\n // 7. Compute sortDate from status.\n for (const entry of entries) {\n entry.sortDate = computeSortDate(entry);\n }\n\n // 8. Filter private entries unless opted in.\n if (options.includePrivate !== true) {\n entries = entries.filter((entry) => entry.private !== true);\n }\n\n // 9. Sort descending by sortDate (lexicographic ISO comparison).\n entries.sort((a, b) => (a.sortDate < b.sortDate ? 1 : a.sortDate > b.sortDate ? -1 : 0));\n\n // 10. Limit.\n if (options.limit !== undefined) {\n entries = entries.slice(0, Math.max(0, options.limit));\n }\n\n // 11. Write the cache back atomically when a cache path was configured.\n if (options.cache !== undefined) {\n await writeCacheFile(options.cache.path, cache);\n }\n\n // 12. lastEntryDate is the max sortDate (entries are sorted descending).\n const lastEntryDate = entries.length > 0 ? entries[0].sortDate : undefined;\n\n return { entries, warnings, lastEntryDate };\n}\n\n/**\n * Enrich every entry with a shared rate limiter and a fetch wrapper that counts\n * requests, so the orchestrator can tell which entries actually hit the network\n * (cache hits and no-path entries never do). Prepends an \"Open Library appears\n * to be unavailable\" rollup when more than half of the real attempts failed at\n * the transport/HTTP layer.\n *\n * `enrich` returns a fresh entry object, so the Libby cover fallback association\n * is forwarded from each input entry to its enriched counterpart, keeping the\n * post-enrichment fallback pass keyed on the entries that are actually returned.\n */\nasync function enrichAll(\n merged: ReadEntry[],\n cache: Cache,\n options: GetReadsOptions,\n warnings: string[],\n libbyCoverByEntry: Map<ReadEntry, string>,\n): Promise<ReadEntry[]> {\n const rateLimiterState = { nextAllowedAt: 0 };\n const baseFetch = options.fetchImpl ?? globalThis.fetch;\n let fetchCount = 0;\n const countingFetch: typeof globalThis.fetch = (input, init) => {\n fetchCount++;\n return baseFetch(input, init);\n };\n\n let attempts = 0;\n let failedAttempts = 0;\n const enriched: ReadEntry[] = [];\n\n for (const entry of merged) {\n const before = fetchCount;\n const result = await enrich(entry, {\n userAgent: options.userAgent,\n cache,\n fetchImpl: countingFetch,\n rateLimiterState,\n rateLimitMs: options.rateLimitMs,\n editionPreferences: options.editionPreferences,\n maxAgeDays: options.cache?.maxAgeDays,\n bust: options.cache?.bust,\n });\n // Copy the match quality onto the entry. Undefined (transport failure, or a\n // cache hit from an entry written before the field existed) leaves it unset.\n if (result.matchQuality !== undefined) {\n result.entry.matchQuality = result.matchQuality;\n }\n enriched.push(result.entry);\n warnings.push(...result.warnings);\n\n // Forward the cover fallback onto the (possibly new) enriched object.\n const libbyCover = libbyCoverByEntry.get(entry);\n if (libbyCover !== undefined && result.entry !== entry) {\n libbyCoverByEntry.set(result.entry, libbyCover);\n }\n\n // An attempt is an entry that actually fetched something this run.\n if (fetchCount > before) {\n attempts += 1;\n if (countTransportFailures(result.warnings) > 0) {\n failedAttempts += 1;\n }\n }\n }\n\n if (attempts > 0 && failedAttempts * 2 > attempts) {\n warnings.unshift(\n `Open Library appears to be unavailable (${failedAttempts} of ${attempts} enrichment attempts failed); cached data was used where possible`,\n );\n }\n\n return enriched;\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "library-reads",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read a Libby export and optional manual entries into a typed array of recent reads, with Open Library enrichment.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"dev": "tsdown --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome lint .",
|
|
28
|
+
"format": "biome format --write .",
|
|
29
|
+
"check": "biome check .",
|
|
30
|
+
"prepublishOnly": "pnpm run check && pnpm run typecheck && pnpm run test && pnpm run build && pnpm exec publint && pnpm exec attw --pack . --profile esm-only"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"libby",
|
|
34
|
+
"library",
|
|
35
|
+
"reading",
|
|
36
|
+
"books",
|
|
37
|
+
"openlibrary",
|
|
38
|
+
"rss-alternative"
|
|
39
|
+
],
|
|
40
|
+
"author": "Anthony Liddle <anthony@anthonyliddle.dev> (https://anthonyliddle.dev)",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/anthony-liddle/library-reads.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/anthony-liddle/library-reads/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/anthony-liddle/library-reads#readme",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22"
|
|
52
|
+
},
|
|
53
|
+
"packageManager": "pnpm@10.33.2",
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
59
|
+
"@biomejs/biome": "2.4.16",
|
|
60
|
+
"@types/node": "^22.19.19",
|
|
61
|
+
"publint": "^0.3.21",
|
|
62
|
+
"tsdown": "^0.22.1",
|
|
63
|
+
"tsx": "^4.22.3",
|
|
64
|
+
"typescript": "6.0.3",
|
|
65
|
+
"vitest": "4.1.7"
|
|
66
|
+
},
|
|
67
|
+
"pnpm": {
|
|
68
|
+
"overrides": {
|
|
69
|
+
"@arethetypeswrong/core>fflate": "0.8.2"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"csv-parse": "^6.2.1",
|
|
74
|
+
"yaml": "^2.9.0"
|
|
75
|
+
}
|
|
76
|
+
}
|