feat(media): add Convex media archive with backfill and mixed-media library

This commit is contained in:
2026-04-10 15:15:44 +02:00
parent ddb2412349
commit a1df097f9c
26 changed files with 2664 additions and 122 deletions

115
lib/media-archive.ts Normal file
View File

@@ -0,0 +1,115 @@
export type MediaArchiveKind = "image" | "video" | "asset";
export type MediaArchiveSource =
| "upload"
| "ai-image"
| "ai-video"
| "freepik-asset"
| "pexels-video";
type MediaArchiveCommonFields = {
kind: MediaArchiveKind;
source: MediaArchiveSource;
dedupeKey: string;
title?: string;
filename?: string;
mimeType?: string;
width?: number;
height?: number;
durationSeconds?: number;
metadata?: Record<string, unknown>;
firstSourceCanvasId?: string;
firstSourceNodeId?: string;
};
type MediaArchiveStoredFields = {
storageId?: string;
previewStorageId?: string;
};
type MediaArchiveExternalFields = {
originalUrl?: string;
previewUrl?: string;
sourceUrl?: string;
providerAssetId?: string;
};
export type MediaArchiveInput = MediaArchiveCommonFields &
MediaArchiveStoredFields &
MediaArchiveExternalFields & {
[key: string]: unknown;
};
export type NormalizedMediaArchiveInput =
| (MediaArchiveCommonFields & MediaArchiveStoredFields)
| (MediaArchiveCommonFields & MediaArchiveExternalFields);
export type MediaArchiveRow = (MediaArchiveCommonFields &
MediaArchiveStoredFields &
MediaArchiveExternalFields & {
_id: string;
createdAt: number;
updatedAt: number;
lastUsedAt: number;
});
export type MediaArchiveListItem = Omit<MediaArchiveRow, "_id"> & {
id: string;
};
export function buildStoredMediaDedupeKey(storageId: string): string {
return `storage:${storageId}`;
}
export function buildFreepikAssetDedupeKey(assetType: string, assetId: number | string): string {
return `freepik:${assetType}:${assetId}`;
}
export function buildPexelsVideoDedupeKey(videoId: number | string): string {
return `pexels:video:${videoId}`;
}
function isExternalSource(source: MediaArchiveSource): boolean {
return source === "freepik-asset" || source === "pexels-video";
}
export function normalizeMediaArchiveInput(input: MediaArchiveInput): NormalizedMediaArchiveInput {
const base: MediaArchiveCommonFields = {
kind: input.kind,
source: input.source,
dedupeKey: input.dedupeKey,
title: input.title,
filename: input.filename,
mimeType: input.mimeType,
width: input.width,
height: input.height,
durationSeconds: input.durationSeconds,
metadata: input.metadata,
firstSourceCanvasId: input.firstSourceCanvasId,
firstSourceNodeId: input.firstSourceNodeId,
};
if (isExternalSource(input.source)) {
return {
...base,
originalUrl: input.originalUrl,
previewUrl: input.previewUrl,
sourceUrl: input.sourceUrl,
providerAssetId: input.providerAssetId,
};
}
return {
...base,
storageId: input.storageId,
previewStorageId: input.previewStorageId,
};
}
export function mapMediaArchiveRowToListItem(row: MediaArchiveRow): MediaArchiveListItem {
const { _id, ...rest } = row;
return {
id: _id,
...rest,
};
}