summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-22 02:41:18 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-22 02:41:18 +0200
commitf5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3 (patch)
tree779132620064669a21c5fd9a787ba28187c0384e /apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
parenta709af0059dae4702b52197f34af85a817c59a6a (diff)
downloadopencut-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.ts313
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,