import { and, desc, eq, ilike, inArray, ne, sql } from "drizzle-orm"; import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { mediaAsset } from "@/lib/db/schema"; import { createLogger } from "@/lib/observability/logger"; import { VIDEO_CANVAS_SUBJECT_UPLOAD_SOURCE_TYPE, VIDEO_CANVAS_SUBJECT_VOICE_SOURCE_TYPE, } from "@/lib/video-canvas/subjects"; export const runtime = "nodejs"; const logger = createLogger("video-canvas-assets"); const SUPPORTED_MEDIA_TYPES = new Set(["image", "video", "audio", "model"]); const ASSET_CATEGORIES = new Set(["其它", "人物", "场景", "物品", "风格", "音效"]); function normalizeAssetIds(value: unknown) { if (!Array.isArray(value)) return []; return Array.from( new Set( value .map((item) => String(item || "").trim()) .filter(Boolean) .slice(0, 50), ), ); } export async function GET(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json( { success: false, error: "未登录" }, { status: 401 }, ); } const url = new URL(request.url); const query = url.searchParams.get("q")?.trim().slice(0, 120) || ""; const requestedType = url.searchParams.get("mediaType")?.trim() || ""; const mediaType = SUPPORTED_MEDIA_TYPES.has(requestedType) ? requestedType : ""; const filters = [ eq(mediaAsset.userId, session.user.id), eq(mediaAsset.status, "ready"), ne(mediaAsset.mediaType, "subject"), ne(mediaAsset.sourceType, VIDEO_CANVAS_SUBJECT_UPLOAD_SOURCE_TYPE), ne(mediaAsset.sourceType, VIDEO_CANVAS_SUBJECT_VOICE_SOURCE_TYPE), ]; if (mediaType) filters.push(eq(mediaAsset.mediaType, mediaType)); if (query) filters.push(ilike(mediaAsset.displayName, `%${query}%`)); const assets = await db .select({ createdAt: mediaAsset.createdAt, displayName: mediaAsset.displayName, durationMs: mediaAsset.durationMs, id: mediaAsset.id, mediaType: mediaAsset.mediaType, metadata: mediaAsset.metadata, mimeType: mediaAsset.mimeType, originalUrl: mediaAsset.originalUrl, previewUrl: mediaAsset.previewUrl, thumbnailUrl: mediaAsset.thumbnailUrl, }) .from(mediaAsset) .where(and(...filters)) .orderBy(desc(mediaAsset.updatedAt)) .limit(100); return NextResponse.json({ success: true, assets }); } catch (error) { logger.error("asset list failed", { error }); return NextResponse.json( { success: false, error: "资产加载失败" }, { status: 500 }, ); } } export async function POST(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json( { success: false, error: "未登录" }, { status: 401 }, ); } const body = (await request.json().catch(() => null)) as { action?: string; name?: string; } | null; if (body?.action !== "create-folder") { return NextResponse.json( { success: false, error: "不支持的操作" }, { status: 400 }, ); } const name = String(body.name || "").trim().slice(0, 20); if (!name) { return NextResponse.json( { success: false, error: "请输入文件夹名称" }, { status: 400 }, ); } const folderUrl = `asset-folder://${crypto.randomUUID()}`; const [folder] = await db .insert(mediaAsset) .values({ displayName: name, mediaType: "folder", metadata: { isFolder: true }, normalizedUrl: folderUrl, originalUrl: folderUrl, sourceTable: "video_canvas_asset_library", sourceType: "video_canvas_asset_folder", status: "ready", userId: session.user.id, }) .returning(); return NextResponse.json({ success: true, asset: folder }); } catch (error) { logger.error("asset folder creation failed", { error }); return NextResponse.json( { success: false, error: "文件夹创建失败" }, { status: 500 }, ); } } export async function PATCH(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json( { success: false, error: "未登录" }, { status: 401 }, ); } const body = (await request.json().catch(() => null)) as { action?: string; assetIds?: unknown; category?: string; folderId?: string | null; } | null; const assetIds = normalizeAssetIds(body?.assetIds); if (!assetIds.length) { return NextResponse.json( { success: false, error: "请选择资产" }, { status: 400 }, ); } let metadataPatch: Record; if (body?.action === "update-category") { const category = String(body.category || "").trim(); if (!ASSET_CATEGORIES.has(category)) { return NextResponse.json( { success: false, error: "资产标签无效" }, { status: 400 }, ); } metadataPatch = { category }; } else if (body?.action === "move-to-folder") { const folderId = String(body.folderId || "").trim() || null; if (folderId) { const [folder] = await db .select({ id: mediaAsset.id }) .from(mediaAsset) .where( and( eq(mediaAsset.id, folderId), eq(mediaAsset.userId, session.user.id), eq(mediaAsset.mediaType, "folder"), ), ) .limit(1); if (!folder) { return NextResponse.json( { success: false, error: "文件夹不存在" }, { status: 404 }, ); } } metadataPatch = { folderId }; } else { return NextResponse.json( { success: false, error: "不支持的操作" }, { status: 400 }, ); } const updated = await db .update(mediaAsset) .set({ metadata: sql`coalesce(${mediaAsset.metadata}, '{}'::jsonb) || ${JSON.stringify(metadataPatch)}::jsonb`, updatedAt: new Date(), }) .where( and( eq(mediaAsset.userId, session.user.id), inArray(mediaAsset.id, assetIds), ne(mediaAsset.mediaType, "folder"), ), ) .returning({ id: mediaAsset.id }); return NextResponse.json({ success: true, updated: updated.length }); } catch (error) { logger.error("asset batch update failed", { error }); return NextResponse.json( { success: false, error: "批量更新失败" }, { status: 500 }, ); } } export async function DELETE(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json( { success: false, error: "未登录" }, { status: 401 }, ); } const body = (await request.json().catch(() => null)) as { assetIds?: unknown; } | null; const assetIds = normalizeAssetIds(body?.assetIds); if (!assetIds.length) { return NextResponse.json( { success: false, error: "请选择资产" }, { status: 400 }, ); } const removed = await db .delete(mediaAsset) .where( and( eq(mediaAsset.userId, session.user.id), inArray(mediaAsset.id, assetIds), ne(mediaAsset.mediaType, "folder"), ), ) .returning({ id: mediaAsset.id }); return NextResponse.json({ success: true, removed: removed.length }); } catch (error) { logger.error("asset batch delete failed", { error }); return NextResponse.json( { success: false, error: "批量删除失败" }, { status: 500 }, ); } }