diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-22 02:41:18 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-22 02:41:18 +0200 |
| commit | f5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3 (patch) | |
| tree | 779132620064669a21c5fd9a787ba28187c0384e /apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts | |
| parent | a709af0059dae4702b52197f34af85a817c59a6a (diff) | |
| download | opencut-f5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3.tar.gz | |
refactor: migration run() and pure v1-to-v2 transform
Diffstat (limited to 'apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts')
| -rw-r--r-- | apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts | 313 |
1 files changed, 122 insertions, 191 deletions
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts index c4b64e50..065a6b34 100644 --- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts @@ -1,17 +1,24 @@ -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; -const DEFAULT_FPS = 30; -import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; -import type { MediaAssetData } from "@/services/storage/types"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -interface LegacyTimelineData { - tracks: unknown[]; - lastModified: string; +// Frozen snapshots of v2-era defaults. See ./README.md. +const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10; +const DEFAULT_BACKGROUND_COLOR = "#000000"; +const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 }; +const DEFAULT_FPS = 30; + +type LegacyMediaType = "image" | "video" | "audio"; + +export interface V1ToV2Context { + legacyTracksBySceneId: Record<string, unknown[]>; + mediaTypesById: Record<string, LegacyMediaType>; } +const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = { + legacyTracksBySceneId: {}, + mediaTypesById: {}, +}; + interface LegacyMediaElement { type: "media"; mediaId: string; @@ -146,21 +153,13 @@ interface V2AudioTrack { type V2TimelineTrack = V2VideoTrack | V2TextTrack | V2AudioTrack; -export interface TransformV1ToV2Options { - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -} - -export async function transformProjectV1ToV2({ +export function transformProjectV1ToV2({ project, - options = {}, + context = EMPTY_V1_TO_V2_CONTEXT, }: { project: ProjectRecord; - options?: TransformV1ToV2Options; -}): Promise<MigrationResult<ProjectRecord>> { + context?: V1ToV2Context; +}): MigrationResult<ProjectRecord> { const projectId = getProjectId({ project }); if (!projectId) { return { project, skipped: true, reason: "no project id" }; @@ -170,27 +169,23 @@ export async function transformProjectV1ToV2({ return { project, skipped: true, reason: "already v2" }; } - const migratedProject = await migrateProject({ + const migratedProject = migrateProject({ project, projectId, - loadMediaAsset: options.loadMediaAsset, + context, }); return { project: migratedProject, skipped: false }; } -async function migrateProject({ +function migrateProject({ project, projectId, - loadMediaAsset, + context, }: { project: ProjectRecord; projectId: string; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -}): Promise<ProjectRecord> { + context: V1ToV2Context; +}): ProjectRecord { const createdAt = normalizeDateString({ value: project.createdAt }); const updatedAt = normalizeDateString({ value: project.updatedAt }); const metadataValue = project.metadata; @@ -217,42 +212,35 @@ async function migrateProject({ ? project.bookmarks : null; - const migratedScenes = await Promise.all( - scenes.map(async (scene) => { - if (!isRecord(scene)) { - return scene; - } - - const sceneId = getStringValue({ value: scene.id }); - if (!sceneId) { - return scene; - } + const migratedScenes = scenes.map((scene) => { + if (!isRecord(scene)) { + return scene; + } - const existingTracks = scene.tracks; - const shouldLoadTracks = - !Array.isArray(existingTracks) || existingTracks.length === 0; + const sceneId = getStringValue({ value: scene.id }); + if (!sceneId) { + return scene; + } - if (!shouldLoadTracks) { - return scene; - } + const existingTracks = scene.tracks; + const shouldLoadTracks = + !Array.isArray(existingTracks) || existingTracks.length === 0; - const tracks = await loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain: scene.isMain === true, - }); + if (!shouldLoadTracks) { + return scene; + } - const transformedTracks = await transformTracks({ - tracks, - loadMediaAsset, - }); + const tracks = context.legacyTracksBySceneId[sceneId] ?? []; + const transformedTracks = transformTracks({ + tracks, + context, + }); - return { - ...scene, - tracks: transformedTracks, - }; - }), - ); + return { + ...scene, + tracks: transformedTracks, + }; + }); const normalizedScenes = applyLegacyBookmarks({ scenes: migratedScenes, @@ -305,183 +293,126 @@ async function migrateProject({ }; } -async function loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain, -}: { - projectId: string; - sceneId: string; - isMain: boolean; -}): Promise<unknown[]> { - if (typeof indexedDB === "undefined") { - return []; - } - - const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`; - const projectDbName = `video-editor-timelines-${projectId}`; - - const adapter = new IndexedDBAdapter<LegacyTimelineData>( - sceneDbName, - "timeline", - 1, - ); - - let data = await adapter.get("timeline"); - - if (!data && isMain) { - const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>( - projectDbName, - "timeline", - 1, - ); - data = await projectAdapter.get("timeline"); - } - - if (!data || !Array.isArray(data.tracks)) { - return []; - } - - return data.tracks; -} - -async function transformTracks({ +function transformTracks({ tracks, - loadMediaAsset, + context, }: { tracks: unknown[]; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -}): Promise<V2TimelineTrack[]> { + context: V1ToV2Context; +}): V2TimelineTrack[] { if (!Array.isArray(tracks)) { return []; } let isFirstVideoTrackFound = false; - const transformedTracks: (V2TimelineTrack | null)[] = []; - - for (const track of tracks) { + const transformedTracks = tracks.map((track): V2TimelineTrack | null => { if (!isRecord(track)) { - transformedTracks.push(null); - continue; + return null; } const trackType = track.type; if (trackType === "media") { - const videoTrack = await transformMediaTrack({ + const isMain = !isFirstVideoTrackFound; + isFirstVideoTrackFound = true; + const videoTrack = transformMediaTrack({ track: track as LegacyMediaTrack, - loadMediaAsset, - isMain: !isFirstVideoTrackFound, + context, + isMain, }); - isFirstVideoTrackFound = true; - transformedTracks.push(videoTrack); - continue; + return videoTrack; } if (trackType === "text") { - transformedTracks.push(transformTextTrack({ track })); - continue; + return transformTextTrack({ track }); } if (trackType === "audio") { - transformedTracks.push(transformAudioTrack({ track })); - continue; + return transformAudioTrack({ track }); } - transformedTracks.push(null); - } + return null; + }); return transformedTracks.filter( (track): track is V2TimelineTrack => track !== null, ); } -async function transformMediaTrack({ +function transformMediaTrack({ track, - loadMediaAsset, + context, isMain, }: { track: LegacyMediaTrack; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; + context: V1ToV2Context; isMain: boolean; -}): Promise<V2VideoTrack> { +}): V2VideoTrack { const elements = Array.isArray(track.elements) ? track.elements : []; - const transformedElements = await Promise.all( - elements.map(async (element) => { - if (!isRecord(element) || element.type !== "media") { - return null; - } - - const mediaElement = element as LegacyMediaElement; - const mediaId = getStringValue({ value: mediaElement.mediaId }); - if (!mediaId) { - return null; - } + const transformedElements = elements.map((element) => { + if (!isRecord(element) || element.type !== "media") { + return null; + } - let mediaType: "video" | "image" = "video"; - if (loadMediaAsset) { - const mediaAsset = await loadMediaAsset({ mediaId }); - if (mediaAsset) { - mediaType = mediaAsset.type === "image" ? "image" : "video"; - } - } + const mediaElement = element as LegacyMediaElement; + const mediaId = getStringValue({ value: mediaElement.mediaId }); + if (!mediaId) { + return null; + } - const defaultTransform: V2Transform = { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }; + let mediaType: "video" | "image" = "video"; + const storedMediaType = context.mediaTypesById[mediaId]; + if (storedMediaType) { + mediaType = storedMediaType === "image" ? "image" : "video"; + } - const muted = mediaElement.muted === true; + const defaultTransform: V2Transform = { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }; - if (mediaType === "image") { - const imageElement: V2ImageElement = { - id: getStringValue({ value: element.id, fallback: "" }), - name: getStringValue({ value: element.name, fallback: "" }), - type: "image", - mediaId, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ - value: element.startTime, - fallback: 0, - }), - trimStart: getNumberValue({ - value: element.trimStart, - fallback: 0, - }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), - hidden: false, - transform: defaultTransform, - opacity: 1, - }; - return imageElement; - } + const muted = mediaElement.muted === true; - const videoElement: V2VideoElement = { + if (mediaType === "image") { + const imageElement: V2ImageElement = { id: getStringValue({ value: element.id, fallback: "" }), name: getStringValue({ value: element.name, fallback: "" }), - type: "video", + type: "image", mediaId, - muted, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ + value: element.startTime, + fallback: 0, + }), + trimStart: getNumberValue({ + value: element.trimStart, + fallback: 0, + }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), hidden: false, transform: defaultTransform, opacity: 1, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ value: element.startTime, fallback: 0 }), - trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), }; - return videoElement; - }), - ); + return imageElement; + } + + const videoElement: V2VideoElement = { + id: getStringValue({ value: element.id, fallback: "" }), + name: getStringValue({ value: element.name, fallback: "" }), + type: "video", + mediaId, + muted, + hidden: false, + transform: defaultTransform, + opacity: 1, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ value: element.startTime, fallback: 0 }), + trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), + }; + return videoElement; + }); const validElements = transformedElements.filter( (element): element is V2VideoElement | V2ImageElement => element !== null, |
