import { definePlugin } from "@sttracker/plugin-sdk/types"; import type { MediaType, PluginContext, SourceRecord, WorkHint } from "@sttracker/plugin-sdk/types"; import { gql, LISTS_QUERY, PAGE_SIZE, SEARCH_QUERY, USER_BOOKS_QUERY } from "./src/graphql.ts"; import { mapList, mapUserBook } from "./src/map.ts"; import type { HcList, HcUserBook } from "./src/map.ts"; async function* pageUserBooks(ctx: PluginContext, where: Record | null): AsyncGenerator { for (let offset = 0; ; offset += PAGE_SIZE) { const d = await gql<{ me: { user_books: HcUserBook[] }[] }>(ctx, USER_BOOKS_QUERY, { limit: PAGE_SIZE, offset, where }); const rows = d.me[0]?.user_books ?? []; yield* rows; if (rows.length < PAGE_SIZE) return; } } async function* pullChanges(ctx: PluginContext): AsyncGenerator { const cursor = await ctx.cursor.get(); const where = cursor ? { updated_at: { _gt: cursor } } : null; let max = cursor; for await (const ub of pageUserBooks(ctx, where)) { if (max === null || ub.updated_at > max) max = ub.updated_at; yield* mapUserBook(ub); } if (max !== null && max !== cursor) await ctx.cursor.set(max); } async function* pull(ctx: PluginContext): AsyncGenerator { let max: string | null = null; for await (const ub of pageUserBooks(ctx, null)) { if (max === null || ub.updated_at > max) max = ub.updated_at; yield* mapUserBook(ub); } const d = await gql<{ me: { lists: HcList[] }[] }>(ctx, LISTS_QUERY, {}); for (const l of d.me[0]?.lists ?? []) yield mapList(l); if (max !== null) await ctx.cursor.set(max); } interface SearchDoc { id: number | string; title: string; release_year?: number; author_names?: string[]; image?: { url?: string } } export default definePlugin({ manifest: { id: "hardcover", name: "Hardcover", version: "0.1.0", sdkVersion: 1, rateLimit: { perMinute: 50 }, mediaTypes: ["book"], capabilities: ["pull", "pullChanges", "lookup"], config: [{ key: "token", label: "Personal access token", type: "secret", required: true }], attribution: "Book data from Hardcover.", }, pull, pullChanges, async lookup(ctx, q: { title: string; mediaType?: MediaType; year?: number }): Promise { if (q.mediaType && q.mediaType !== "book") return []; const d = await gql<{ search: { results: { hits?: { document: SearchDoc }[] } | null } }>(ctx, SEARCH_QUERY, { q: q.title }); return (d.search.results?.hits ?? []).map(({ document: x }) => ({ title: x.title, mediaType: "book" as const, externalIds: { hardcover_book: String(x.id) }, ...(x.release_year ? { year: x.release_year } : {}), ...(x.author_names?.length ? { creators: x.author_names.map((name) => ({ name })) } : {}), ...(x.image?.url ? { coverUrl: x.image.url } : {}), })); }, });