268 lines
7.7 KiB
TypeScript

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<string, unknown>;
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 },
);
}
}