Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 0s
CI / Scraper / Test (pull_request) Successful in 11s
CI / UI / Build (pull_request) Failing after 14s
CI / Scraper / Lint (pull_request) Successful in 23s
CI / Scraper / Build (pull_request) Successful in 24s
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
- iOS SwiftUI app (ios/LibNovel/) targeting iOS 17+, generated via xcodegen - Full feature set: auth, home, library, book detail, chapter reader, browse, audio player, profile - Kingfisher for image loading, swift-markdown-ui for chapter rendering - Base URL: https://v2.libnovel.kalekber.cc - SvelteKit JSON API routes (ui/src/routes/api/) for iOS consumption: auth/login, auth/register, auth/me, auth/logout, auth/change-password, home, library, book/[slug], chapter/[slug]/[n], search, ranking, progress/[slug], presign/audio (updated) - Gitea Actions CI: .gitea/workflows/ios.yaml (build + test on macos-latest) - justfile: ios-gen, ios-build, ios-test recipes
106 lines
2.7 KiB
TypeScript
106 lines
2.7 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';
|
|
|
|
interface PreviewChapter {
|
|
number: number;
|
|
title: string;
|
|
url: string;
|
|
}
|
|
|
|
/**
|
|
* GET /api/book/[slug]
|
|
* Returns book metadata, chapter list, progress, and library status.
|
|
* Falls back to a live scraper preview if the book is not in PocketBase.
|
|
*
|
|
* Response shape mirrors BookDetailResponse in the iOS APIClient.
|
|
*/
|
|
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,
|
|
preview_chapters: null,
|
|
in_lib: true,
|
|
saved,
|
|
last_chapter: progress?.chapter ?? null
|
|
});
|
|
}
|
|
|
|
// Fall back to live scraper preview
|
|
try {
|
|
const res = await fetch(`${SCRAPER_URL}/api/book-preview/${encodeURIComponent(slug)}`);
|
|
if (!res.ok) {
|
|
log.warn('api/book', 'book-preview returned error', { slug, status: res.status });
|
|
error(404, `Book "${slug}" not found`);
|
|
}
|
|
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: PreviewChapter[];
|
|
} = 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: preview.chapters,
|
|
in_lib: preview.in_lib,
|
|
saved: false,
|
|
last_chapter: 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`);
|
|
}
|
|
};
|