import { NextRequest, NextResponse } from "next/server"; import { assertLocalRequest } from "@/lib/api/localOnly"; import { listImages, countImages } from "@/lib/db/queries"; import { resolveSort } from "@/lib/sortServer"; import { parseFilterCriteria, statusToFlags } from "@/lib/filters"; import { getAppSetting } from "@/lib/db/appSettings"; import type { LibraryView } from "@/components/grid/ViewToggle"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; /** * Paginated covers feed for client-side infinite-scroll appends. * Mirrors the SSR filter shape in app/page.tsx — every filter the * grid supports (letter, search, sort, marks, multi-select tabs) * resolves through the same listImages/countImages path. */ export async function GET(req: NextRequest) { const blocked = assertLocalRequest(req); if (blocked) return blocked; const sp = req.nextUrl.searchParams; // Ape Object.fromEntries for plain access matching page params. const params: Record = {}; for (const [k, v] of sp.entries()) { const cur = params[k]; if (cur == null) params[k] = v; else if (Array.isArray(cur)) cur.push(v); else params[k] = [cur, v]; } const criteria = parseFilterCriteria(params); const sort = await resolveSort(typeof params.sort === "string" ? params.sort : undefined); const rawLetter = (typeof params.letter === "string" ? params.letter : "").toUpperCase(); const letter = rawLetter === "#" ? "#" : (/^[A-Z]$/.test(rawLetter) ? rawLetter : null); const search = (typeof params.q === "string" ? params.q.trim() : "") || undefined; // view is purely a presentational hint; included for symmetry but // doesn't affect query. void (params.view === "portrait" ? "portrait" : "landscape" as LibraryView); const rawPage = typeof params.page === "string" ? Number(params.page) : NaN; const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : 1; const filterOpts = { sort, letter: letter ?? undefined, search, ...statusToFlags(criteria.status), marks: criteria.marks, actressIds: criteria.ids.actresses, actressMode: criteria.mode.actresses, studioIds: criteria.ids.studios, seriesIds: criteria.ids.series, genreIds: criteria.ids.genres, genreMode: criteria.mode.genres, collectionIds: criteria.ids.collections, collectionMode: criteria.mode.collections, tagIds: criteria.ids.tags, tagMode: criteria.mode.tags, categoryIds: criteria.ids.categories, categoryMode: criteria.mode.categories, }; const PAGE_SIZE = Math.max(25, Math.min(500, getAppSetting("coverPageSize") ?? 100)); const totalCount = countImages(filterOpts); const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)); const effectivePage = Math.min(page, totalPages); const offset = (effectivePage - 1) * PAGE_SIZE; const items = listImages({ ...filterOpts, limit: PAGE_SIZE, offset }); return NextResponse.json( { items, page: effectivePage, totalPages, totalCount, hasMore: effectivePage < totalPages }, { headers: { "Cache-Control": "no-store" } }, ); }