Files
2026-05-26 22:46:00 +02:00

76 lines
3.1 KiB
TypeScript

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<string, string | string[] | undefined> = {};
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" } },
);
}