Files
memorium/lib/hardcover.ts
Max Richter e0bfbdd719 feat: integrate Hardcover API for books
- Add lib/hardcover.ts with GraphQL client for Hardcover API
- Add routes/api/books/[name].ts for creating books via Hardcover ID
- Add routes/api/books/enhance/[name].ts for enhancing books
- Add routes/api/hardcover/query.ts for searching books
- Add routes/books/[name].tsx and index.tsx for book pages
2026-02-10 18:19:10 +01:00

141 lines
3.4 KiB
TypeScript

import { createCache } from "@lib/cache.ts";
const HARDCOVER_API_URL = "https://api.hardcover.app/v1/graphql";
const CACHE_INTERVAL = 1000 * 60 * 60 * 24 * 7;
const cache = createCache("hardcover", { expires: CACHE_INTERVAL });
export interface HardcoverBookResult {
id: string;
slug: string;
title: string;
subtitle?: string;
author_names: string[];
cover_color?: string;
description?: string;
isbn?: string;
isbn13?: string;
release_year?: string;
series_names?: string[];
rating?: number;
ratings_count?: number;
pages?: number;
image?: string;
}
async function hardcoverFetch(query: string, variables: Record<string, unknown> = {}) {
const response = await fetch(HARDCOVER_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": Deno.env.get("HARDCOVER_API_TOKEN") || "",
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`Hardcover API error: ${response.statusText}`);
}
const json = await response.json();
if (json.errors) {
console.error("Hardcover GraphQL errors:", JSON.stringify(json.errors, null, 2));
throw new Error(`Hardcover GraphQL error: ${json.errors[0]?.message || "Unknown error"}`);
}
return json;
}
export const searchBook = async (query: string) => {
const id = `query:booksearch:${query}`;
if (cache.has(id)) return cache.get(id) as HardcoverBookResult[];
const graphqlQuery = `
query SearchBooks($query: String!, $perPage: Int, $page: Int) {
search(
query: $query
query_type: "Book"
per_page: $perPage
page: $page
) {
results
}
}
`;
const result = await hardcoverFetch(graphqlQuery, {
query,
perPage: 10,
page: 1,
});
const typesenseResponse = result.data?.search?.results;
const hits = typesenseResponse?.hits;
const books = (hits || []).map((hit: { document: HardcoverBookResult }) => hit.document);
cache.set(id, books);
return books as HardcoverBookResult[];
};
export const getBookDetails = async (id: string) => {
const cacheId = `query:bookdetails:${id}`;
if (cache.has(cacheId)) return cache.get(cacheId);
const graphqlQuery = `
query GetBook($id: Int!) {
books(where: { id: { _eq: $id } }) {
id
slug
title
subtitle
description
release_date
release_year
rating
ratings_count
pages
cached_image
cached_contributors
featured_book_series {
series {
id
name
slug
}
}
editions(limit: 1) {
isbn_10
isbn_13
}
}
}
`;
const result = await hardcoverFetch(graphqlQuery, { id: parseInt(id) });
const bookData = result.data?.books?.[0];
if (bookData) {
const isbn13 = bookData.editions?.[0]?.isbn_13;
const isbn10 = bookData.editions?.[0]?.isbn_10;
const cachedContributors = bookData.cached_contributors as Array<{ author: { name: string }; contribution: string }> | undefined;
const authorName = cachedContributors?.[0]?.author?.name;
const book = {
...bookData,
isbn: isbn13 || isbn10 || "",
isbn13,
isbn10,
author_names: authorName ? [authorName] : [],
image: bookData.cached_image?.url,
};
cache.set(cacheId, book);
return book;
}
return null;
};