Files
STTracker/plugins/hardcover/src/map.ts
T
2026-10-30 15:48:46 +01:00

103 lines
3.5 KiB
TypeScript

import { parsePartialDate } from "@sttracker/plugin-sdk/partial-date";
import type { EntryFields, SourceRecord, Status, WorkHint } from "@sttracker/plugin-sdk/types";
export interface HcBook {
id: number;
title: string;
release_year?: number | null;
contributions?: { author: { name: string } | null }[];
book_series?: { position: number | null; series: { id: number; name: string } }[];
}
export interface HcRead { id: number; started_at: string | null; finished_at: string | null; progress_pages: number | null }
export interface HcUserBook {
id: number;
status_id: number | null;
rating: number | null;
review_raw: string | null;
private_notes: string | null;
starred: boolean | null;
updated_at: string;
merged_at: string | null;
original_book_id: number | null;
book: HcBook;
edition: { id: number; isbn_13: string | null; isbn_10: string | null; asin: string | null } | null;
user_book_reads: HcRead[];
}
export interface HcList { id: number; name: string; description: string | null; list_books: { book: HcBook }[] }
export const STATUS: Record<number, Status> = { 1: "planned", 2: "in_progress", 3: "completed", 4: "paused", 5: "dropped", 6: "none" };
export function bookHint(b: HcBook): WorkHint {
const creators = (b.contributions ?? []).flatMap((c) => (c.author ? [{ name: c.author.name }] : []));
const h: WorkHint = { title: b.title, mediaType: "book", externalIds: { hardcover_book: String(b.id) } };
if (b.release_year != null) h.year = b.release_year;
if (creators.length) h.creators = creators;
return h;
}
function workHint(ub: HcUserBook): WorkHint {
const h = bookHint(ub.book);
const e = ub.edition;
if (e) {
h.externalIds.hardcover_edition = String(e.id);
if (e.isbn_13) h.externalIds.isbn13 = e.isbn_13;
if (e.isbn_10) h.externalIds.isbn10 = e.isbn_10;
if (e.asin) h.externalIds.asin = e.asin;
}
const s = ub.book.book_series?.[0];
if (s) {
h.parent = { title: s.series.name, mediaType: "book", externalIds: { hardcover_series: String(s.series.id) } };
if (s.position != null) h.position = { index: s.position };
}
return h;
}
export function* mapUserBook(ub: HcUserBook): Generator<SourceRecord> {
const work = workHint(ub);
const entry: EntryFields = {
status: STATUS[ub.status_id ?? 6] ?? "none",
rating: ub.rating == null ? null : Math.round(ub.rating * 20),
review: ub.review_raw,
notes: ub.private_notes,
favorite: ub.starred === true,
};
const key = `ub:${ub.id}`;
yield { kind: "entry", key, raw: ub, work, entry };
for (const r of ub.user_book_reads) {
yield {
kind: "engagement",
key: `read:${r.id}`,
raw: r,
work,
engagement: {
startedAt: parsePartialDate(r.started_at),
finishedAt: parsePartialDate(r.finished_at),
...(r.progress_pages != null ? { progress: { pages: r.progress_pages } } : {}),
},
};
}
if (ub.merged_at && ub.original_book_id != null && ub.original_book_id !== ub.book.id) {
yield {
kind: "entry",
key: `${key}@book:${ub.original_book_id}`,
raw: ub,
mergedIntoKey: key,
work: { title: ub.book.title, mediaType: "book", externalIds: { hardcover_book: String(ub.original_book_id) } },
entry,
};
}
}
export function mapList(l: HcList): SourceRecord {
return {
kind: "list",
key: `list:${l.id}`,
raw: l,
list: {
name: l.name,
...(l.description ? { description: l.description } : {}),
items: l.list_books.map((lb) => ({ work: bookHint(lb.book) })),
},
};
}