All checks were successful
Release / Scraper / Test (push) Successful in 10s
Release / UI / Build (push) Successful in 26s
Release / v2 / Build ui-v2 (push) Successful in 17s
Release / Scraper / Docker (push) Successful in 47s
Release / UI / Docker (push) Successful in 56s
CI / Scraper / Lint (pull_request) Successful in 7s
CI / Scraper / Test (pull_request) Successful in 8s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
Release / v2 / Docker / ui-v2 (push) Successful in 56s
Release / v2 / Test backend (push) Successful in 4m35s
iOS CI / Build (pull_request) Successful in 4m28s
Release / v2 / Docker / backend (push) Successful in 1m29s
Release / v2 / Docker / runner (push) Successful in 1m39s
iOS CI / Test (pull_request) Successful in 9m51s
- backend/: Go API server and runner binaries with PocketBase + MinIO storage - ui-v2/: SvelteKit frontend rewrite - docker-compose-new.yml: compose file for the v2 stack - .gitea/workflows/release-v2.yaml: CI/CD for backend, runner, and ui-v2 Docker Hub images - scripts/pb-init.sh: migrate from wget to curl, add superuser bootstrap for fresh installs - .env.example: document DOCKER_BUILDKIT=1 for Colima users
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { getBook, listChapterIdx, getProgress, isBookSaved } from '$lib/server/pocketbase';
|
|
import { log } from '$lib/server/logger';
|
|
import { env } from '$env/dynamic/private';
|
|
|
|
const SCRAPER_URL = env.SCRAPER_API_URL ?? 'http://localhost:8080';
|
|
|
|
/**
|
|
* GET /api/book/[slug]
|
|
* Returns book metadata, chapter list, progress, and library status.
|
|
*
|
|
* If the book is not yet in PocketBase, asks the backend to enqueue a scrape
|
|
* task and returns 202 with { scraping: true, task_id }.
|
|
* The client should poll and retry once the task completes.
|
|
*/
|
|
export const GET: RequestHandler = async ({ params, locals }) => {
|
|
const { slug } = params;
|
|
|
|
// Try PocketBase first
|
|
let book = await getBook(slug).catch((e) => {
|
|
log.error('api/book', 'getBook failed', { slug, err: String(e) });
|
|
return null;
|
|
});
|
|
|
|
if (book) {
|
|
let chapters, progress, saved;
|
|
try {
|
|
[chapters, progress, saved] = await Promise.all([
|
|
listChapterIdx(slug),
|
|
getProgress(locals.sessionId, slug, locals.user?.id),
|
|
isBookSaved(locals.sessionId, slug, locals.user?.id)
|
|
]);
|
|
} catch (e) {
|
|
log.error('api/book', 'failed to load book detail data', { slug, err: String(e) });
|
|
error(500, 'Failed to load book');
|
|
}
|
|
|
|
return json({
|
|
book,
|
|
chapters,
|
|
in_lib: true,
|
|
saved,
|
|
last_chapter: progress?.chapter ?? null,
|
|
scraping: false,
|
|
task_id: null
|
|
});
|
|
}
|
|
|
|
// Fall back to backend: enqueue scrape task if not in library.
|
|
try {
|
|
const res = await fetch(`${SCRAPER_URL}/api/book-preview/${encodeURIComponent(slug)}`);
|
|
|
|
if (res.status === 202) {
|
|
const body: { task_id: string; message: string } = await res.json();
|
|
log.info('api/book', 'scrape task enqueued', { slug, task_id: body.task_id });
|
|
return json({ scraping: true, task_id: body.task_id, in_lib: false }, { status: 202 });
|
|
}
|
|
|
|
if (!res.ok) {
|
|
log.warn('api/book', 'book-preview returned error', { slug, status: res.status });
|
|
error(404, `Book "${slug}" not found`);
|
|
}
|
|
|
|
// 200 — book was already in library
|
|
const preview: {
|
|
in_lib: boolean;
|
|
meta: {
|
|
slug: string;
|
|
title: string;
|
|
author: string;
|
|
cover: string;
|
|
status: string;
|
|
genres: string[];
|
|
summary: string;
|
|
total_chapters: number;
|
|
source_url: string;
|
|
};
|
|
chapters: { number: number; title: string; date?: string }[];
|
|
} = await res.json();
|
|
|
|
const previewBook = {
|
|
id: '',
|
|
slug: preview.meta.slug || slug,
|
|
title: preview.meta.title,
|
|
author: preview.meta.author,
|
|
cover: preview.meta.cover,
|
|
status: preview.meta.status,
|
|
genres: preview.meta.genres ?? [],
|
|
summary: preview.meta.summary,
|
|
total_chapters: preview.meta.total_chapters,
|
|
source_url: preview.meta.source_url,
|
|
ranking: 0,
|
|
meta_updated: ''
|
|
};
|
|
|
|
return json({
|
|
book: previewBook,
|
|
chapters: preview.chapters,
|
|
in_lib: true,
|
|
saved: false,
|
|
last_chapter: null,
|
|
scraping: false,
|
|
task_id: null
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Error && 'status' in e) throw e;
|
|
log.error('api/book', 'book-preview fetch failed', { slug, err: String(e) });
|
|
error(404, `Book "${slug}" not found`);
|
|
}
|
|
};
|