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
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
import { json } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { listBooks, allProgress, getSavedSlugs } from '$lib/server/pocketbase';
|
|
import { log } from '$lib/server/logger';
|
|
|
|
/**
|
|
* GET /api/library
|
|
* Returns the user's library: books they have started reading or explicitly saved.
|
|
* Each item includes the book record, the last chapter read, and saved_at timestamp.
|
|
*
|
|
* Response shape mirrors LibraryItem in the iOS APIClient.
|
|
*/
|
|
export const GET: RequestHandler = async ({ locals }) => {
|
|
let allBooks: Awaited<ReturnType<typeof listBooks>>;
|
|
let progressList: Awaited<ReturnType<typeof allProgress>>;
|
|
let savedSlugs: Set<string>;
|
|
|
|
try {
|
|
[allBooks, progressList, savedSlugs] = await Promise.all([
|
|
listBooks(),
|
|
allProgress(locals.sessionId, locals.user?.id),
|
|
getSavedSlugs(locals.sessionId, locals.user?.id)
|
|
]);
|
|
} catch (e) {
|
|
log.error('api/library', 'failed to load library data', { err: String(e) });
|
|
allBooks = [];
|
|
progressList = [];
|
|
savedSlugs = new Set();
|
|
}
|
|
|
|
const progressMap: Record<string, number> = {};
|
|
const progressUpdatedMap: Record<string, string> = {};
|
|
for (const p of progressList) {
|
|
progressMap[p.slug] = p.chapter;
|
|
progressUpdatedMap[p.slug] = p.updated;
|
|
}
|
|
|
|
const progressSlugs = new Set(progressList.map((p) => p.slug));
|
|
const books = allBooks.filter((b) => progressSlugs.has(b.slug) || savedSlugs.has(b.slug));
|
|
|
|
const withProgress = books.filter((b) => progressSlugs.has(b.slug));
|
|
const savedOnly = books
|
|
.filter((b) => !progressSlugs.has(b.slug))
|
|
.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? ''));
|
|
|
|
withProgress.sort((a, b) => {
|
|
const ta = progressUpdatedMap[a.slug] ?? '';
|
|
const tb = progressUpdatedMap[b.slug] ?? '';
|
|
return tb.localeCompare(ta);
|
|
});
|
|
|
|
const ordered = [...withProgress, ...savedOnly];
|
|
|
|
const items = ordered.map((book) => ({
|
|
book,
|
|
last_chapter: progressMap[book.slug] ?? null,
|
|
saved_at: progressUpdatedMap[book.slug] ?? new Date().toISOString()
|
|
}));
|
|
|
|
return json(items);
|
|
};
|