67 lines
2.8 KiB
TypeScript
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 } : {}),
|
|
}));
|
|
},
|
|
});
|