website/src/pages/api/reading.json.ts
2026-09-09 20:09:07 -04:00

134 lines
3.5 KiB
TypeScript

import type { APIRoute } from "astro";
import { env as cfEnv } from "cloudflare:workers";
export const prerender = false;
type Env = Record<string, string | undefined>;
interface Book {
title: string;
author: string;
cover: string | null;
url: string;
progress: number | null;
}
const GQL = "https://api.hardcover.app/v1/graphql";
const SITE = "https://hardcover.app";
const QUERY = `
query SiteReading {
me {
current: user_books(where: {status_id: {_eq: 2}}, limit: 5) {
id
user_book_reads(order_by: {id: desc}, limit: 1) { progress_pages }
book {
title
slug
pages
image { url }
contributions { author { name } }
}
}
want: user_books(where: {status_id: {_eq: 1}}, order_by: {id: desc}, limit: 12) {
book {
title
slug
image { url }
contributions { author { name } }
}
}
recent: user_books(where: {status_id: {_eq: 3}}, order_by: {id: desc}, limit: 8) {
book {
title
slug
image { url }
contributions { author { name } }
}
}
}
}`;
function getEnv(): Env {
const nodeEnv = typeof process !== "undefined" ? process.env : {};
let workerEnv: Env = {};
try {
workerEnv = cfEnv as unknown as Env;
} catch {
}
return { ...nodeEnv, ...workerEnv };
}
function mapBook(ub: any, withProgress: boolean): Book {
const b = ub?.book ?? {};
const author = (b?.contributions?.[0]?.author?.name ?? "").replace(/\s+/g, " ").trim();
const cover = b?.image?.url ?? null;
let progress: number | null = null;
if (withProgress) {
const read = ub?.user_book_reads?.[0];
const done = read?.progress_pages;
const total = b?.pages;
if (typeof done === "number" && typeof total === "number" && total > 0) {
progress = Math.max(0, Math.min(100, Math.round((done / total) * 100)));
}
}
return {
title: b?.title ?? "",
author,
cover,
url: b?.slug ? `${SITE}/books/${b.slug}` : SITE,
progress,
};
}
export const GET: APIRoute = async () => {
const token = getEnv().HARDCOVER_TOKEN;
const empty = { configured: false, current: [], want: [], recent: [] };
const json = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), {
status,
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=300, s-maxage=1800, stale-while-revalidate=86400",
},
});
if (!token) return json(empty);
let payload: any;
try {
const res = await fetch(GQL, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`,
"user-agent": "zachy.cc reading widget",
},
body: JSON.stringify({ query: QUERY }),
});
payload = await res.json();
} catch {
return json({ ...empty, configured: true });
}
const me = Array.isArray(payload?.data?.me) ? payload.data.me[0] : payload?.data?.me;
if (!me || payload.errors) {
return json({ ...empty, configured: true });
}
const current: Book[] = (me.current ?? [])
.map((ub: any) => mapBook(ub, true))
.filter((b: Book) => b.title);
const want: Book[] = (me.want ?? [])
.map((ub: any) => mapBook(ub, false))
.filter((b: Book) => b.title);
const recent: Book[] = (me.recent ?? [])
.map((ub: any) => mapBook(ub, false))
.filter((b: Book) => b.title);
return json({ configured: true, current, want, recent });
};