76 lines
3.1 KiB
TypeScript
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" } },
|
|
);
|
|
}
|