import { type NextRequest, NextResponse } from "next/server"; import type { FeedItem, FeedResponse } from "@/app/types/feed"; import { isVideoContentTag } from "@/lib/video-tags"; import { getFileUrl } from "@/lib/minio"; import { prisma } from "@/lib/prisma"; // Contract // Inputs: search params { before?: ISOString, limit?: number } // Output: { items: FeedItem[], nextCursor: ISOString | null } export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const limitParam = searchParams.get("limit"); const beforeParam = searchParams.get("before"); const rawTag = (searchParams.get("tag") || "").trim(); const selectedTag = rawTag && isVideoContentTag(rawTag) ? rawTag : null; if (rawTag && !selectedTag) { return NextResponse.json({ error: "Invalid tag" }, { status: 400 }); } const limit = Math.min(Math.max(Number(limitParam ?? "24"), 1), 60); // 1..60 const before = beforeParam ? new Date(beforeParam) : null; const videoWhere = { ...(before ? { created_at: { lt: before } } : {}), ...(selectedTag ? { manual_content_tags: { has: selectedTag }, } : {}), }; // fetch chunk from both tables const [videos, posts] = await Promise.all([ prisma.video.findMany({ where: videoWhere, orderBy: { created_at: "desc" }, take: limit, include: { author: true, }, }), selectedTag ? Promise.resolve([]) : prisma.imagePost.findMany({ where: before ? { created_at: { lt: before } } : undefined, orderBy: { created_at: "desc" }, take: limit, include: { author: true, images: { orderBy: { order: "asc" }, take: 1 }, }, }), ]); const merged: FeedItem[] = [ ...videos.map((v) => ({ type: "video" as const, aweme_id: v.aweme_id, created_at: v.created_at, desc: v.desc, video_url: getFileUrl(v.video_url), cover_url: getFileUrl(v.cover_url ?? "default_cover.png"), width: v.width ?? null, height: v.height ?? null, author: { nickname: v.author.nickname, avatar_url: getFileUrl(v.author.avatar_url ?? ""), sec_uid: v.author.sec_uid, }, likes: Number(v.digg_count), content_tags: v.manual_content_tags, })), ...posts.map((p) => { const coverPath = p.images?.[0]?.url; return { type: "image" as const, aweme_id: p.aweme_id, created_at: p.created_at, desc: p.desc, cover_url: coverPath ? getFileUrl(coverPath) : null, width: p.images?.[0]?.width ?? null, height: p.images?.[0]?.height ?? null, author: { nickname: p.author.nickname, avatar_url: getFileUrl(p.author.avatar_url ?? ""), sec_uid: p.author.sec_uid, }, likes: Number(p.digg_count), }; }), ] .sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at)) .slice(0, limit); const nextCursor = merged.length > 0 ? new Date(merged[merged.length - 1].created_at).toISOString() : null; const payload: FeedResponse = { items: merged, nextCursor }; return NextResponse.json(payload); }