@editframe/cli 0.10.0-beta.5 → 0.10.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/VERSION.d.ts +1 -1
- package/dist/VERSION.js +1 -1
- package/dist/commands/render.d.ts +1 -1
- package/dist/commands/render.js +55 -43
- package/dist/commands/sync.js +5 -2
- package/dist/operations/syncAssetsDirectory/SubAssetSync.d.ts +20 -0
- package/dist/operations/syncAssetsDirectory/SubAssetSync.js +26 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +19 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.js +66 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +20 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +79 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +23 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.js +95 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +41 -0
- package/dist/operations/syncAssetsDirectory/SyncStatus.js +43 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +70 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.js +138 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.d.ts +5 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.js +48 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory.d.ts +1 -1
- package/dist/operations/syncAssetsDirectory.js +20 -240
- package/dist/test-fixtures/fixture.d.ts +26 -0
- package/dist/utils/index.js +4 -1
- package/package.json +5 -5
- package/src/commands/render.ts +61 -52
- package/src/commands/sync.ts +5 -2
- package/src/operations/syncAssetsDirectory/SubAssetSync.ts +42 -0
- package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +145 -0
- package/src/operations/syncAssetsDirectory/SyncCaption.ts +76 -0
- package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +151 -0
- package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +92 -0
- package/src/operations/syncAssetsDirectory/SyncImage.test.ts +131 -0
- package/src/operations/syncAssetsDirectory/SyncImage.ts +112 -0
- package/src/operations/syncAssetsDirectory/SyncStatus.ts +51 -0
- package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +222 -0
- package/src/operations/syncAssetsDirectory/SyncTrack.ts +164 -0
- package/src/operations/syncAssetsDirectory/doAssetSync.test.ts +134 -0
- package/src/operations/syncAssetsDirectory/doAssetSync.ts +62 -0
- package/src/operations/syncAssetsDirectory.test.ts +482 -0
- package/src/operations/syncAssetsDirectory.ts +22 -283
- package/src/utils/index.ts +4 -1
- package/test-fixtures/fixture.ts +141 -0
- package/test-fixtures/network.ts +181 -0
- package/test-fixtures/test-captions.json +9 -0
- package/test-fixtures/test.mp4 +0 -0
- package/test-fixtures/test.png +0 -0
- package/src/commands/render.test.ts +0 -34
- /package/dist/{commands/render.test.d.ts → operations/syncAssetsDirectory.test.d.ts} +0 -0
package/src/commands/render.ts
CHANGED
|
@@ -1,53 +1,47 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
2
|
+
import path, { basename, join } from "node:path";
|
|
4
3
|
import { PassThrough } from "node:stream";
|
|
4
|
+
import { inspect } from "node:util";
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import { program, Option } from "commander";
|
|
6
|
+
import { Option, program } from "commander";
|
|
8
7
|
import { parse as parseHTML } from "node-html-parser";
|
|
8
|
+
import * as tar from "tar";
|
|
9
9
|
|
|
10
10
|
import { md5Directory, md5FilePath } from "@editframe/assets";
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
14
13
|
import { createRender, uploadRender } from "@editframe/api";
|
|
15
|
-
import
|
|
16
|
-
import { PreviewServer } from "../utils/startPreviewServer.ts";
|
|
17
|
-
import { getClient } from "../utils/index.ts";
|
|
14
|
+
import debug from "debug";
|
|
18
15
|
import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.ts";
|
|
19
16
|
import { processRenderInfo } from "../operations/processRenderInfo.ts";
|
|
20
|
-
import {
|
|
17
|
+
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
|
|
18
|
+
import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.ts";
|
|
21
19
|
import { getFolderSize } from "../utils/getFolderSize.ts";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const buildAssetId = async (assetPath: string) => {
|
|
29
|
-
const md5Sum = await md5FilePath(assetPath);
|
|
30
|
-
|
|
31
|
-
const basename = path.basename(assetPath);
|
|
32
|
-
|
|
33
|
-
return `${md5Sum}:${basename}`;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
class V1Builder implements StrategyBuilder {
|
|
37
|
-
async buildAssetId(assetPath: string) {
|
|
38
|
-
return buildAssetId(assetPath);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
20
|
+
import { getClient } from "../utils/index.ts";
|
|
21
|
+
import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.ts";
|
|
22
|
+
import { PreviewServer } from "../utils/startPreviewServer.ts";
|
|
23
|
+
import { validateVideoResolution } from "../utils/validateVideoResolution.ts";
|
|
24
|
+
import { withSpinner } from "../utils/withSpinner.ts";
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
const log = debug("ef:cli:render");
|
|
27
|
+
|
|
28
|
+
export const buildAssetId = async (
|
|
29
|
+
srcDir: string,
|
|
30
|
+
src: string,
|
|
31
|
+
basename: string,
|
|
32
|
+
) => {
|
|
33
|
+
log(`Building image asset id for ${src}\n`);
|
|
34
|
+
const assetPath = path.join(srcDir, src);
|
|
35
|
+
const assetMd5 = await md5FilePath(assetPath);
|
|
36
|
+
const syncStatus = new SyncStatus(
|
|
37
|
+
join(srcDir, "assets", ".cache", assetMd5, basename),
|
|
38
|
+
);
|
|
39
|
+
const info = await syncStatus.readInfo();
|
|
40
|
+
if (!info) {
|
|
41
|
+
throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);
|
|
45
42
|
}
|
|
46
|
-
}
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
v1: new V1Builder(),
|
|
50
|
-
v2: new V2Builder(),
|
|
44
|
+
return info.asset_id;
|
|
51
45
|
};
|
|
52
46
|
|
|
53
47
|
program
|
|
@@ -57,21 +51,18 @@ program
|
|
|
57
51
|
)
|
|
58
52
|
.addOption(
|
|
59
53
|
new Option("-s, --strategy <strategy>", "Render strategy")
|
|
60
|
-
.choices(["v1"
|
|
54
|
+
.choices(["v1"])
|
|
61
55
|
.default("v1"),
|
|
62
56
|
)
|
|
63
57
|
.action(async (directory, options) => {
|
|
64
58
|
directory ??= ".";
|
|
65
59
|
|
|
66
|
-
await syncAssetDirectory(
|
|
60
|
+
await syncAssetDirectory(
|
|
61
|
+
join(process.cwd(), directory, "src", "assets", ".cache"),
|
|
62
|
+
);
|
|
67
63
|
|
|
68
64
|
const srcDir = path.join(directory, "src");
|
|
69
65
|
const distDir = path.join(directory, "dist");
|
|
70
|
-
const builder = strategyBuilders[options.strategy];
|
|
71
|
-
if (!builder) {
|
|
72
|
-
console.error("Invalid strategy", options.strategy);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
66
|
await withSpinner("Building\n", async () => {
|
|
76
67
|
try {
|
|
77
68
|
await withSpinner("Building\n", async () => {
|
|
@@ -117,23 +108,41 @@ program
|
|
|
117
108
|
const doc = parseHTML(
|
|
118
109
|
await readFile(path.join(distDir, "index.html"), "utf-8"),
|
|
119
110
|
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
|
|
112
|
+
log("Building asset IDs");
|
|
113
|
+
for (const element of doc.querySelectorAll(
|
|
114
|
+
"ef-image, ef-audio, ef-video",
|
|
115
|
+
)) {
|
|
116
|
+
log(`Processing ${element.tagName}`);
|
|
124
117
|
if (element.hasAttribute("asset-id")) {
|
|
118
|
+
log(
|
|
119
|
+
`Asset ID for ${element.tagName} ${element.getAttribute("src")} is ${element.getAttribute("asset-id")}`,
|
|
120
|
+
);
|
|
125
121
|
continue;
|
|
126
122
|
}
|
|
127
123
|
const src = element.getAttribute("src");
|
|
128
124
|
if (!src) {
|
|
125
|
+
log(`No src attribute for ${element.tagName}`);
|
|
129
126
|
continue;
|
|
130
127
|
}
|
|
131
|
-
const assetPath = path.join(srcDir, src);
|
|
132
128
|
|
|
133
|
-
element.
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
switch (element.tagName) {
|
|
130
|
+
case "EF-IMAGE":
|
|
131
|
+
element.setAttribute(
|
|
132
|
+
"asset-id",
|
|
133
|
+
await buildAssetId(srcDir, src, basename(src)),
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
case "EF-AUDIO":
|
|
137
|
+
case "EF-VIDEO":
|
|
138
|
+
element.setAttribute(
|
|
139
|
+
"asset-id",
|
|
140
|
+
await buildAssetId(srcDir, src, "isobmff"),
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
log(`Unknown element type: ${element.tagName}`);
|
|
145
|
+
}
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
await writeFile(path.join(distDir, "index.html"), doc.toString());
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { program } from "commander";
|
|
2
3
|
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
|
|
3
4
|
|
|
@@ -5,6 +6,8 @@ program
|
|
|
5
6
|
.command("sync")
|
|
6
7
|
.description("Sync assets to Editframe servers for rendering")
|
|
7
8
|
.argument("[directory]", "Path to project directory to sync.")
|
|
8
|
-
.action(async (
|
|
9
|
-
await syncAssetDirectory(
|
|
9
|
+
.action(async (directory = ".") => {
|
|
10
|
+
await syncAssetDirectory(
|
|
11
|
+
join(process.cwd(), directory, "src", "assets", ".cache"),
|
|
12
|
+
);
|
|
10
13
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SyncStatus } from "./SyncStatus.ts";
|
|
2
|
+
|
|
3
|
+
import { SyncCaption } from "./SyncCaption.ts";
|
|
4
|
+
import { SyncFragmentIndex } from "./SyncFragmentIndex.ts";
|
|
5
|
+
import { SyncImage } from "./SyncImage.ts";
|
|
6
|
+
import { SyncTrack } from "./SyncTrack.ts";
|
|
7
|
+
|
|
8
|
+
export interface SubAssetSync<CreationType> {
|
|
9
|
+
icon: string;
|
|
10
|
+
label: string;
|
|
11
|
+
path: string;
|
|
12
|
+
md5: string;
|
|
13
|
+
prepare: () => Promise<void>;
|
|
14
|
+
validate: () => Promise<void>;
|
|
15
|
+
create: () => Promise<void>;
|
|
16
|
+
upload: () => Promise<void>;
|
|
17
|
+
syncStatus: SyncStatus;
|
|
18
|
+
isComplete: () => boolean;
|
|
19
|
+
markSynced: () => Promise<void>;
|
|
20
|
+
created: CreationType | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const trackMatch = /\.track-[\d]+.mp4$/i;
|
|
24
|
+
const fragmentIndexMatch = /\.tracks.json$/i;
|
|
25
|
+
const captionsMatch = /\.captions.json$/i;
|
|
26
|
+
const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
|
|
27
|
+
|
|
28
|
+
export const getAssetSync = (subAssetPath: string, md5: string) => {
|
|
29
|
+
if (imageMatch.test(subAssetPath)) {
|
|
30
|
+
return new SyncImage(subAssetPath, md5);
|
|
31
|
+
}
|
|
32
|
+
if (trackMatch.test(subAssetPath)) {
|
|
33
|
+
return new SyncTrack(subAssetPath, md5);
|
|
34
|
+
}
|
|
35
|
+
if (fragmentIndexMatch.test(subAssetPath)) {
|
|
36
|
+
return new SyncFragmentIndex(subAssetPath, md5);
|
|
37
|
+
}
|
|
38
|
+
if (captionsMatch.test(subAssetPath)) {
|
|
39
|
+
return new SyncCaption(subAssetPath, md5);
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unrecognized sub-asset type: ${subAssetPath}`);
|
|
42
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
|
|
3
|
+
import {
|
|
4
|
+
mockCreateCaptionFile,
|
|
5
|
+
mockUploadCaptionFile,
|
|
6
|
+
useMSW,
|
|
7
|
+
} from "../../../test-fixtures/network.ts";
|
|
8
|
+
import { SyncCaption } from "./SyncCaption.ts";
|
|
9
|
+
|
|
10
|
+
describe("SyncCaption", async () => {
|
|
11
|
+
const server = useMSW();
|
|
12
|
+
await withFixtures(
|
|
13
|
+
[fixture("test.mp4", "test.mp4")],
|
|
14
|
+
async ({ files: [video], generateCaptions }) => {
|
|
15
|
+
test("Reads byte size", async () => {
|
|
16
|
+
const syncCaption = new SyncCaption(
|
|
17
|
+
await generateCaptions(video!),
|
|
18
|
+
video!.md5,
|
|
19
|
+
);
|
|
20
|
+
await expect(syncCaption.byteSize()).resolves.toEqual(35);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("prepare() is noop", async () => {
|
|
24
|
+
const syncCaption = new SyncCaption(
|
|
25
|
+
await generateCaptions(video!),
|
|
26
|
+
video!.md5,
|
|
27
|
+
);
|
|
28
|
+
await expect(syncCaption.prepare()).resolves.toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("validate() is noop", async () => {
|
|
32
|
+
const syncCaption = new SyncCaption(
|
|
33
|
+
await generateCaptions(video!),
|
|
34
|
+
video!.md5,
|
|
35
|
+
);
|
|
36
|
+
await expect(syncCaption.validate()).resolves.toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe(".create()", () => {
|
|
40
|
+
test("isComplete() returns false when not created", async () => {
|
|
41
|
+
server.use(
|
|
42
|
+
mockCreateCaptionFile({
|
|
43
|
+
complete: false,
|
|
44
|
+
id: "123",
|
|
45
|
+
filename: "test.mp4",
|
|
46
|
+
fixture: video!,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const syncCaption = new SyncCaption(
|
|
50
|
+
await generateCaptions(video!),
|
|
51
|
+
video!.md5,
|
|
52
|
+
);
|
|
53
|
+
await syncCaption.create();
|
|
54
|
+
expect(syncCaption.isComplete()).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("isComplete() returns true when created", async () => {
|
|
58
|
+
server.use(
|
|
59
|
+
mockCreateCaptionFile({
|
|
60
|
+
complete: true,
|
|
61
|
+
id: "123",
|
|
62
|
+
filename: "test.mp4",
|
|
63
|
+
fixture: video!,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
const syncCaption = new SyncCaption(
|
|
67
|
+
await generateCaptions(video!),
|
|
68
|
+
video!.md5,
|
|
69
|
+
);
|
|
70
|
+
await syncCaption.create();
|
|
71
|
+
expect(syncCaption.isComplete()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe(".upload()", () => {
|
|
76
|
+
test("throws when not created", async () => {
|
|
77
|
+
const syncCaption = new SyncCaption(
|
|
78
|
+
await generateCaptions(video!),
|
|
79
|
+
video!.md5,
|
|
80
|
+
);
|
|
81
|
+
await expect(syncCaption.upload()).rejects.toThrow();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("uploads caption", async () => {
|
|
85
|
+
server.use(
|
|
86
|
+
mockCreateCaptionFile({
|
|
87
|
+
complete: true,
|
|
88
|
+
id: "123",
|
|
89
|
+
filename: "test.mp4",
|
|
90
|
+
fixture: video!,
|
|
91
|
+
}),
|
|
92
|
+
mockUploadCaptionFile({
|
|
93
|
+
id: "123",
|
|
94
|
+
filename: "test.mp4",
|
|
95
|
+
fixture: video!,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
const syncCaption = new SyncCaption(
|
|
99
|
+
await generateCaptions(video!),
|
|
100
|
+
video!.md5,
|
|
101
|
+
);
|
|
102
|
+
await syncCaption.create();
|
|
103
|
+
await expect(syncCaption.upload()).resolves.toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe(".markSynced()", () => {
|
|
108
|
+
test("throws when not created", async () => {
|
|
109
|
+
const syncCaption = new SyncCaption(
|
|
110
|
+
await generateCaptions(video!),
|
|
111
|
+
video!.md5,
|
|
112
|
+
);
|
|
113
|
+
await expect(syncCaption.markSynced()).rejects.toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("marks synced", async () => {
|
|
117
|
+
server.use(
|
|
118
|
+
mockCreateCaptionFile({
|
|
119
|
+
complete: true,
|
|
120
|
+
id: "123",
|
|
121
|
+
filename: "test.mp4",
|
|
122
|
+
fixture: video!,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
const syncCaption = new SyncCaption(
|
|
126
|
+
await generateCaptions(video!),
|
|
127
|
+
video!.md5,
|
|
128
|
+
);
|
|
129
|
+
await syncCaption.create();
|
|
130
|
+
await syncCaption.markSynced();
|
|
131
|
+
|
|
132
|
+
await expect(syncCaption.syncStatus.isSynced()).resolves.toBe(true);
|
|
133
|
+
await expect(syncCaption.syncStatus.readInfo()).resolves.toEqual({
|
|
134
|
+
version: "1",
|
|
135
|
+
complete: true,
|
|
136
|
+
id: "123",
|
|
137
|
+
md5: video!.md5,
|
|
138
|
+
asset_id: `${video!.md5}:test.mp4`,
|
|
139
|
+
byte_size: 35,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type CreateCaptionFileResult,
|
|
5
|
+
createCaptionFile,
|
|
6
|
+
uploadCaptionFile,
|
|
7
|
+
} from "@editframe/api";
|
|
8
|
+
|
|
9
|
+
import { Readable } from "node:stream";
|
|
10
|
+
import { getClient } from "../../utils/index.ts";
|
|
11
|
+
import type { SubAssetSync } from "./SubAssetSync.ts";
|
|
12
|
+
import { SyncStatus } from "./SyncStatus.ts";
|
|
13
|
+
|
|
14
|
+
export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
|
|
15
|
+
icon = "📝";
|
|
16
|
+
label = "captions";
|
|
17
|
+
syncStatus: SyncStatus = new SyncStatus(this.path);
|
|
18
|
+
created: CreateCaptionFileResult | null = null;
|
|
19
|
+
constructor(
|
|
20
|
+
public path: string,
|
|
21
|
+
public md5: string,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async byteSize() {
|
|
25
|
+
return (await fs.stat(this.path)).size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async prepare() {}
|
|
29
|
+
|
|
30
|
+
async validate() {}
|
|
31
|
+
|
|
32
|
+
async create() {
|
|
33
|
+
this.created = await createCaptionFile(getClient(), {
|
|
34
|
+
md5: this.md5,
|
|
35
|
+
filename: this.path.replace(/\.captions.json$/, ""),
|
|
36
|
+
byte_size: await this.byteSize(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isComplete() {
|
|
41
|
+
return !!this.created?.complete;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async upload() {
|
|
45
|
+
if (!this.created) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Caption not created. Should have been prevented by .isComplete()",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
await uploadCaptionFile(
|
|
51
|
+
getClient(),
|
|
52
|
+
this.created.id,
|
|
53
|
+
// It's not clear why we need to use Readable.from here, but it seems
|
|
54
|
+
// to fix an issue where the request is closed early in tests
|
|
55
|
+
Readable.from(await fs.readFile(this.path)),
|
|
56
|
+
await this.byteSize(),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async markSynced() {
|
|
61
|
+
if (!this.created) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Caption not created. Should have been prevented by .isComplete()",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const byteSize = await this.byteSize();
|
|
67
|
+
await this.syncStatus.markSynced({
|
|
68
|
+
version: "1",
|
|
69
|
+
complete: true,
|
|
70
|
+
id: this.created.id,
|
|
71
|
+
md5: this.md5,
|
|
72
|
+
asset_id: this.created.asset_id,
|
|
73
|
+
byte_size: byteSize,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
|
|
3
|
+
import {
|
|
4
|
+
mockCreateIsobmffFile,
|
|
5
|
+
mockUploadIsobmffFileIndex,
|
|
6
|
+
useMSW,
|
|
7
|
+
} from "../../../test-fixtures/network.ts";
|
|
8
|
+
import { SyncFragmentIndex } from "./SyncFragmentIndex.ts";
|
|
9
|
+
|
|
10
|
+
describe("SyncFragmentIndex", async () => {
|
|
11
|
+
const server = useMSW();
|
|
12
|
+
await withFixtures(
|
|
13
|
+
[fixture("test.mp4", "test.mp4")],
|
|
14
|
+
async ({ files: [video], generateTrackFragmentIndex }) => {
|
|
15
|
+
test("Reads byte size", async () => {
|
|
16
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
17
|
+
await generateTrackFragmentIndex(video!),
|
|
18
|
+
video!.md5,
|
|
19
|
+
);
|
|
20
|
+
await expect(syncFragmentIndex.byteSize()).resolves.toEqual(31);
|
|
21
|
+
});
|
|
22
|
+
test("prepare() is noop", async () => {
|
|
23
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
24
|
+
await generateTrackFragmentIndex(video!),
|
|
25
|
+
video!.md5,
|
|
26
|
+
);
|
|
27
|
+
await expect(syncFragmentIndex.prepare()).resolves.toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
test("validate() is noop", async () => {
|
|
30
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
31
|
+
await generateTrackFragmentIndex(video!),
|
|
32
|
+
video!.md5,
|
|
33
|
+
);
|
|
34
|
+
await expect(syncFragmentIndex.validate()).resolves.toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
describe(".create()", () => {
|
|
37
|
+
test("isComplete() returns false when not created", async () => {
|
|
38
|
+
server.use(
|
|
39
|
+
mockCreateIsobmffFile({
|
|
40
|
+
complete: false,
|
|
41
|
+
id: "123",
|
|
42
|
+
filename: "test.mp4",
|
|
43
|
+
fixture: video!,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
47
|
+
await generateTrackFragmentIndex(video!),
|
|
48
|
+
video!.md5,
|
|
49
|
+
);
|
|
50
|
+
await syncFragmentIndex.create();
|
|
51
|
+
expect(syncFragmentIndex.isComplete()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
test("isComplete() returns true when created", async () => {
|
|
54
|
+
server.use(
|
|
55
|
+
mockCreateIsobmffFile({
|
|
56
|
+
complete: true,
|
|
57
|
+
id: "123",
|
|
58
|
+
filename: "test.mp4",
|
|
59
|
+
fixture: video!,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
63
|
+
await generateTrackFragmentIndex(video!),
|
|
64
|
+
video!.md5,
|
|
65
|
+
);
|
|
66
|
+
await syncFragmentIndex.create();
|
|
67
|
+
expect(syncFragmentIndex.isComplete()).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe(".upload()", () => {
|
|
71
|
+
test("throws when not created", async () => {
|
|
72
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
73
|
+
await generateTrackFragmentIndex(video!),
|
|
74
|
+
video!.md5,
|
|
75
|
+
);
|
|
76
|
+
await expect(syncFragmentIndex.upload()).rejects.toThrow();
|
|
77
|
+
});
|
|
78
|
+
test("uploads caption", async () => {
|
|
79
|
+
server.use(
|
|
80
|
+
mockCreateIsobmffFile({
|
|
81
|
+
complete: true,
|
|
82
|
+
id: "123",
|
|
83
|
+
filename: "test.mp4",
|
|
84
|
+
fixture: video!,
|
|
85
|
+
}),
|
|
86
|
+
mockUploadIsobmffFileIndex({
|
|
87
|
+
id: "123",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
91
|
+
await generateTrackFragmentIndex(video!),
|
|
92
|
+
video!.md5,
|
|
93
|
+
);
|
|
94
|
+
await syncFragmentIndex.create();
|
|
95
|
+
await expect(syncFragmentIndex.upload()).resolves.toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe(".markSynced()", () => {
|
|
99
|
+
test("throws when not created", async () => {
|
|
100
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
101
|
+
await generateTrackFragmentIndex(video!),
|
|
102
|
+
video!.md5,
|
|
103
|
+
);
|
|
104
|
+
await expect(syncFragmentIndex.markSynced()).rejects.toThrow();
|
|
105
|
+
});
|
|
106
|
+
test("marks synced", async () => {
|
|
107
|
+
server.use(
|
|
108
|
+
mockCreateIsobmffFile({
|
|
109
|
+
complete: true,
|
|
110
|
+
id: "123",
|
|
111
|
+
fixture: video!,
|
|
112
|
+
}),
|
|
113
|
+
mockUploadIsobmffFileIndex({
|
|
114
|
+
id: "123",
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
118
|
+
await generateTrackFragmentIndex(video!),
|
|
119
|
+
video!.md5,
|
|
120
|
+
);
|
|
121
|
+
await syncFragmentIndex.create();
|
|
122
|
+
await syncFragmentIndex.markSynced();
|
|
123
|
+
await expect(syncFragmentIndex.syncStatus.isSynced()).resolves.toBe(
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
await expect(
|
|
127
|
+
syncFragmentIndex.syncStatus.readInfo(),
|
|
128
|
+
).resolves.toEqual({
|
|
129
|
+
version: "1",
|
|
130
|
+
complete: true,
|
|
131
|
+
id: "123",
|
|
132
|
+
md5: video!.md5,
|
|
133
|
+
asset_id: `${video!.md5}:test.mp4`,
|
|
134
|
+
byte_size: 31,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
syncFragmentIndex.fileSyncStatus.readInfo(),
|
|
139
|
+
).resolves.toEqual({
|
|
140
|
+
version: "1",
|
|
141
|
+
complete: true,
|
|
142
|
+
id: "123",
|
|
143
|
+
md5: video!.md5,
|
|
144
|
+
asset_id: `${video!.md5}:test.mp4`,
|
|
145
|
+
byte_size: 31,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CreateISOBMFFFileResult,
|
|
7
|
+
createISOBMFFFile,
|
|
8
|
+
uploadFragmentIndex,
|
|
9
|
+
} from "@editframe/api";
|
|
10
|
+
|
|
11
|
+
import { getClient } from "../../utils/index.ts";
|
|
12
|
+
import type { SubAssetSync } from "./SubAssetSync.ts";
|
|
13
|
+
import { SyncStatus } from "./SyncStatus.ts";
|
|
14
|
+
|
|
15
|
+
export class SyncFragmentIndex
|
|
16
|
+
implements SubAssetSync<CreateISOBMFFFileResult>
|
|
17
|
+
{
|
|
18
|
+
icon = "📋";
|
|
19
|
+
label = "fragment index";
|
|
20
|
+
syncStatus: SyncStatus = new SyncStatus(this.path);
|
|
21
|
+
fileSyncStatus: SyncStatus = new SyncStatus(
|
|
22
|
+
join(dirname(this.path), "isobmff"),
|
|
23
|
+
);
|
|
24
|
+
created: CreateISOBMFFFileResult | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
public path: string,
|
|
28
|
+
public md5: string,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async byteSize() {
|
|
32
|
+
return (await fs.stat(this.path)).size;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async prepare() {}
|
|
36
|
+
|
|
37
|
+
async validate() {}
|
|
38
|
+
|
|
39
|
+
async create() {
|
|
40
|
+
this.created = await createISOBMFFFile(getClient(), {
|
|
41
|
+
md5: this.md5,
|
|
42
|
+
filename: this.path.replace(/\.tracks.json$/, ""),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isComplete() {
|
|
47
|
+
return !!this.created?.fragment_index_complete;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async upload() {
|
|
51
|
+
if (!this.created) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Fragment index not created. Should have been prevented by .isComplete()",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
await uploadFragmentIndex(
|
|
57
|
+
getClient(),
|
|
58
|
+
this.created.id,
|
|
59
|
+
// It is unclear why we need to use Readable.from here
|
|
60
|
+
// Tests fail when using createReadStream
|
|
61
|
+
Readable.from(await fs.readFile(this.path)),
|
|
62
|
+
await this.byteSize(),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async markSynced() {
|
|
67
|
+
if (!this.created) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"Fragment index not created. Should have been prevented by .isComplete()",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const byteSize = await this.byteSize();
|
|
73
|
+
await Promise.all([
|
|
74
|
+
this.syncStatus.markSynced({
|
|
75
|
+
version: "1",
|
|
76
|
+
complete: true,
|
|
77
|
+
id: this.created.id,
|
|
78
|
+
md5: this.md5,
|
|
79
|
+
asset_id: this.created.asset_id,
|
|
80
|
+
byte_size: byteSize,
|
|
81
|
+
}),
|
|
82
|
+
this.fileSyncStatus.markSynced({
|
|
83
|
+
version: "1",
|
|
84
|
+
complete: true,
|
|
85
|
+
id: this.created.id,
|
|
86
|
+
md5: this.md5,
|
|
87
|
+
asset_id: this.created.asset_id,
|
|
88
|
+
byte_size: byteSize,
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
}
|