Files
STTracker/plugins/hardcover/index.ts
T

67 lines
2.8 KiB
TypeScript

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<string, unknown> | null): AsyncGenerator<HcUserBook> {
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<SourceRecord> {
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<SourceRecord> {
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<WorkHint[]> {
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 } : {}),
}));
},
});