import type { PageServerLoad } from './$types'; import { getBooksBySlugs, recentlyUpdatedBooks, allProgress, getHomeStats, getSubscriptionFeed, getTrendingBooks, getRecommendedBooks, getBooksWithAudioCount } from '$lib/server/pocketbase'; import { log } from '$lib/server/logger'; import type { Book, Progress } from '$lib/server/pocketbase'; function parseGenresLocal(genres: string[] | string | null | undefined): string[] { if (!genres) return []; if (Array.isArray(genres)) return genres; try { return JSON.parse(genres) as string[]; } catch { return []; } } function computeStreak(progressList: Progress[]): number { const days = new Set( progressList.filter((p) => p.updated).map((p) => p.updated.slice(0, 10)) ); let streak = 0; const today = new Date(); for (let i = 0; i < 365; i++) { const d = new Date(today); d.setUTCDate(d.getUTCDate() - i); if (days.has(d.toISOString().slice(0, 10))) streak++; else if (i > 0) break; } return streak; } export const load: PageServerLoad = async ({ locals }) => { let recentBooks: Book[] = []; let progressList: Progress[] = []; let stats = { totalBooks: 0, totalChapters: 0 }; try { [recentBooks, progressList, stats] = await Promise.all([ recentlyUpdatedBooks(8).catch(() => [] as Book[]), allProgress(locals.sessionId, locals.user?.id), getHomeStats() ]); } catch (e) { log.error('home', 'failed to load home data', { err: String(e) }); } const streak = computeStreak(progressList); // Fetch only the books we need for continue-reading (avoid loading all books) const progressSlugs = progressList.map((p) => p.slug); const progressBooks = progressSlugs.length > 0 ? await getBooksBySlugs(progressSlugs).catch(() => [] as Book[]) : []; const bookMap = new Map(progressBooks.map((b) => [b.slug, b])); // All continue-reading entries joined with book data, most recent first const continueReading = progressList .filter((p) => bookMap.has(p.slug)) .slice(0, 8) .map((p) => ({ book: bookMap.get(p.slug)!, chapter: p.chapter })); // Split into in-progress vs completed const continueInProgress = continueReading.filter( ({ book, chapter }) => book.total_chapters === 0 || chapter < book.total_chapters ); const continueCompleted = continueReading.filter( ({ book, chapter }) => book.total_chapters > 0 && chapter >= book.total_chapters ); // Top genres from books the user has been reading const genreFreq = new Map(); for (const { book } of continueReading) { for (const g of parseGenresLocal(book.genres)) { genreFreq.set(g, (genreFreq.get(g) ?? 0) + 1); } } const topGenres = [...genreFreq.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([g]) => g); // Deduplicate recently-updated against in-progress slugs const inProgressSlugs = new Set(continueReading.map((c) => c.book.slug)); const recentlyUpdated = recentBooks.filter((b) => !inProgressSlugs.has(b.slug)).slice(0, 6); // Fetch trending, recommendations, subscription feed, and audio books in parallel const [trendingBooks, recommendedBooks, subscriptionFeed, audioBooks] = await Promise.all([ getTrendingBooks(8).catch(() => [] as Book[]), topGenres.length > 0 ? getRecommendedBooks(topGenres, inProgressSlugs, 8).catch(() => [] as Book[]) : Promise.resolve([] as Book[]), locals.user ? getSubscriptionFeed(locals.user.id, 12).catch((e) => { log.error('home', 'failed to load subscription feed', { err: String(e) }); return [] as Awaited>; }) : Promise.resolve([]), getBooksWithAudioCount(20).catch(() => []) ]); // Strip books the user is already reading from trending (redundant) const trendingFiltered = trendingBooks.filter((b) => !inProgressSlugs.has(b.slug)); // Strip already-reading books from audio shelf; cap at 8 const readyToListen = audioBooks .filter((e) => !inProgressSlugs.has(e.book.slug)) .slice(0, 8); return { continueInProgress, continueCompleted, recentlyUpdated, subscriptionFeed, trendingBooks: trendingFiltered, recommendedBooks, readyToListen, topGenre: topGenres[0] ?? null, stats: { ...stats, booksInProgress: continueInProgress.length, streak } }; };