268 lines
7.7 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|