diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:24:33 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:24:33 +0200 |
| commit | 608dc1a62fdb133e218f45796cf936bdfb27c7ae (patch) | |
| tree | 605132265bab1f345b0edfee8bdadfcaf8e973db /apps/web/src/services/storage/migrations | |
| parent | cc150c7cfb859dfc38372210ec5bcc8bd196bbc4 (diff) | |
| parent | f1d5932c54178c4f8eac61d3d3e0e4a30101780f (diff) | |
| download | opencut-608dc1a62fdb133e218f45796cf936bdfb27c7ae.tar.gz | |
Merge branch 'feature/separate-audio'
Diffstat (limited to 'apps/web/src/services/storage/migrations')
4 files changed, 247 insertions, 1 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts new file mode 100644 index 00000000..0e0cfbbe --- /dev/null +++ b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, test } from "bun:test"; +import { transformProjectV19ToV20 } from "../transformers/v19-to-v20"; + +describe("V19 to V20 Migration", () => { + test("backfills source audio enabled state on video elements", () => { + const result = transformProjectV19ToV20({ + project: { + id: "project-v19-source-audio", + version: 19, + metadata: { + id: "project-v19-source-audio", + name: "Project", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [ + { + id: "scene-main", + name: "Main", + isMain: true, + bookmarks: [], + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + tracks: [ + { + id: "track-video", + type: "video", + name: "Video", + isMain: true, + muted: false, + hidden: false, + elements: [ + { + id: "video-1", + type: "video", + name: "Clip", + mediaId: "media-1", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + position: { x: 0, y: 0 }, + scale: { x: 1, y: 1 }, + rotation: 0, + }, + opacity: 1, + }, + ], + }, + { + id: "track-audio", + type: "audio", + name: "Audio", + muted: false, + elements: [ + { + id: "audio-1", + type: "audio", + sourceType: "upload", + mediaId: "media-audio-1", + name: "Audio", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + volume: 0, + }, + ], + }, + ], + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(20); + expect( + ((result.project.scenes as Array<Record<string, unknown>>)[0] + .tracks as Array<Record<string, unknown>>)[0].elements, + ).toEqual([ + expect.objectContaining({ + id: "video-1", + isSourceAudioEnabled: true, + }), + ]); + expect( + (((result.project.scenes as Array<Record<string, unknown>>)[0] + .tracks as Array<Record<string, unknown>>)[1] + .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled, + ).toBeUndefined(); + }); + + test("preserves existing explicit source audio state", () => { + const result = transformProjectV19ToV20({ + project: { + id: "project-v19-existing-state", + version: 19, + scenes: [ + { + tracks: [ + { + elements: [ + { + type: "video", + isSourceAudioEnabled: false, + }, + ], + }, + ], + }, + ], + }, + }); + + expect( + (((result.project.scenes as Array<Record<string, unknown>>)[0] + .tracks as Array<Record<string, unknown>>)[0] + .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled, + ).toBe(false); + }); + + test("skips projects already on v20", () => { + const result = transformProjectV19ToV20({ + project: { + id: "project-v20", + version: 20, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v20"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index e87d1cb9..d278263b 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -18,10 +18,11 @@ import { V15toV16Migration } from "./v15-to-v16"; import { V16toV17Migration } from "./v16-to-v17";
import { V17toV18Migration } from "./v17-to-v18";
import { V18toV19Migration } from "./v18-to-v19";
+import { V19toV20Migration } from "./v19-to-v20";
export { runStorageMigrations } from "./runner";
export type { MigrationProgress } from "./runner";
-export const CURRENT_PROJECT_VERSION = 19;
+export const CURRENT_PROJECT_VERSION = 20;
export const migrations = [
new V0toV1Migration(),
@@ -43,4 +44,5 @@ export const migrations = [ new V16toV17Migration(),
new V17toV18Migration(),
new V18toV19Migration(),
+ new V19toV20Migration(),
];
diff --git a/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts b/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts new file mode 100644 index 00000000..f72812a4 --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts @@ -0,0 +1,87 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV19ToV20({ + project, +}: { + project: ProjectRecord; +}): MigrationResult<ProjectRecord> { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 20) { + return { project, skipped: true, reason: "already v20" }; + } + + return { + project: { + ...migrateVideoSourceAudioState({ project }), + version: 20, + }, + skipped: false, + }; +} + +function migrateVideoSourceAudioState({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + if (!Array.isArray(project.scenes)) { + return project; + } + + return { + ...project, + scenes: project.scenes.map((scene) => migrateScene({ scene })), + }; +} + +function migrateScene({ + scene, +}: { + scene: unknown; +}): unknown { + if (!isRecord(scene) || !Array.isArray(scene.tracks)) { + return scene; + } + + return { + ...scene, + tracks: scene.tracks.map((track) => migrateTrack({ track })), + }; +} + +function migrateTrack({ + track, +}: { + track: unknown; +}): unknown { + if (!isRecord(track) || !Array.isArray(track.elements)) { + return track; + } + + return { + ...track, + elements: track.elements.map((element) => migrateElement({ element })), + }; +} + +function migrateElement({ + element, +}: { + element: unknown; +}): unknown { + if (!isRecord(element) || element.type !== "video") { + return element; + } + + return { + ...element, + isSourceAudioEnabled: + typeof element.isSourceAudioEnabled === "boolean" + ? element.isSourceAudioEnabled + : true, + }; +} diff --git a/apps/web/src/services/storage/migrations/v19-to-v20.ts b/apps/web/src/services/storage/migrations/v19-to-v20.ts new file mode 100644 index 00000000..c6809067 --- /dev/null +++ b/apps/web/src/services/storage/migrations/v19-to-v20.ts @@ -0,0 +1,16 @@ +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV19ToV20 } from "./transformers/v19-to-v20"; + +export class V19toV20Migration extends StorageMigration { + from = 19; + to = 20; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV19ToV20({ project }); + } +} |
