import { error } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; import { env } from '$env/dynamic/private'; import { log } from '$lib/server/logger'; const SCRAPER_URL = env.SCRAPER_API_URL ?? 'http://localhost:8080'; export interface NovelListing { slug: string; title: string; cover: string; rank: string; rating: string; chapters: string; url: string; // enriched fields (only set when sort=rank) author?: string; status?: string; genres?: string[]; source_url?: string; } export const load: PageServerLoad = async ({ url, locals }) => { const page = url.searchParams.get('page') ?? '1'; const genre = url.searchParams.get('genre') ?? 'all'; const sort = url.searchParams.get('sort') ?? 'popular'; const status = url.searchParams.get('status') ?? 'all'; const q = url.searchParams.get('q') ?? ''; let novels: NovelListing[] = []; let pageNum = parseInt(page, 10) || 1; let hasNext = false; let searchQuery = ''; let searchLocalCount = 0; let searchRemoteCount = 0; // ── Search mode: ?q= overrides browse/ranking ───────────────────────── if (q.trim().length >= 2) { searchQuery = q.trim(); const apiURL = `${SCRAPER_URL}/api/search?q=${encodeURIComponent(searchQuery)}`; try { const res = await fetch(apiURL); if (!res.ok) { log.error('browse', 'search returned error', { status: res.status }); throw error(502, `Search failed: ${res.status}`); } const data: { results: NovelListing[]; local_count: number; remote_count: number; } = await res.json(); novels = data.results ?? []; searchLocalCount = data.local_count ?? 0; searchRemoteCount = data.remote_count ?? 0; } catch (e) { if (e instanceof Error && 'status' in e) throw e; log.error('browse', 'search network error', { q: searchQuery, err: String(e) }); throw error(502, 'Could not reach search service'); } return { novels, page: 1, hasNext: false, genre, sort, status, isAdmin: locals.user?.role === 'admin', searchQuery, searchLocalCount, searchRemoteCount }; } if (sort === 'rank') { // Ranking view: fetch from /api/ranking which returns richer metadata. // Pagination and filters (genre/status) don't apply here — the ranking // is a single pre-computed list from the last catalogue scrape. const apiURL = `${SCRAPER_URL}/api/ranking`; try { const res = await fetch(apiURL); if (!res.ok) { log.error('browse', 'scraper ranking returned error', { status: res.status }); throw error(502, `Ranking fetch failed: ${res.status}`); } const items: Array<{ rank: number; slug: string; title: string; author: string; cover: string; status: string; genres: string[]; source_url: string; }> = await res.json(); novels = (items ?? []).map((item) => ({ slug: item.slug, title: item.title, cover: item.cover, rank: item.rank != null ? `#${item.rank}` : '', rating: '', chapters: '', url: item.source_url ?? '', author: item.author, status: item.status, genres: item.genres ?? [], source_url: item.source_url })); pageNum = 1; hasNext = false; } catch (e) { if (e instanceof Error && 'status' in e) throw e; log.error('browse', 'scraper ranking network error', { err: String(e) }); throw error(502, 'Could not load ranking'); } } else { // Browse view: paginated catalogue from /api/browse. const params = new URLSearchParams({ page, genre, sort, status }); const apiURL = `${SCRAPER_URL}/api/browse?${params.toString()}`; try { const res = await fetch(apiURL); if (!res.ok) { log.error('browse', 'scraper browse returned error', { status: res.status, url: apiURL }); throw error(502, `Browse fetch failed: ${res.status}`); } const data: { novels: NovelListing[]; page: number; hasNext: boolean } = await res.json(); novels = data.novels ?? []; pageNum = data.page ?? 1; hasNext = data.hasNext ?? false; } catch (e) { if (e instanceof Error && 'status' in e) throw e; log.error('browse', 'scraper browse network error', { url: apiURL, err: String(e) }); throw error(502, 'Could not load browse page'); } } return { novels, page: pageNum, hasNext, genre, sort, status, isAdmin: locals.user?.role === 'admin', searchQuery: '', searchLocalCount: 0, searchRemoteCount: 0 }; }; // Admin action: trigger a full catalogue scrape (refreshes ranking + library). export const actions: Actions = { refresh: async ({ locals, fetch }) => { if (!locals.user || locals.user.role !== 'admin') { throw error(403, 'Forbidden'); } try { const res = await fetch('/api/scrape', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (res.status === 409) return { status: 'busy' }; if (!res.ok) return { status: 'error' }; return { status: 'queued' }; } catch { return { status: 'error' }; } } };