@editframe/cli 0.10.0-beta.4 → 0.10.0-beta.6
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 +77 -51
- package/dist/commands/sync.js +5 -2
- package/dist/operations/getRenderInfo.d.ts +40 -4
- package/dist/operations/getRenderInfo.js +13 -0
- package/dist/operations/processRenderInfo.js +3 -0
- 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/process.ts +0 -1
- package/src/commands/render.ts +79 -58
- package/src/commands/sync.ts +5 -2
- package/src/operations/getRenderInfo.ts +14 -0
- package/src/operations/processRenderInfo.ts +3 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/cli",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.6",
|
|
4
4
|
"description": "Command line interface for EditFrame",
|
|
5
5
|
"bin": {
|
|
6
6
|
"editframe": "./dist/index.js"
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"vite-tsconfig-paths": "^4.3.2"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@editframe/api": "0.10.0-beta.
|
|
27
|
-
"@editframe/assets": "0.10.0-beta.
|
|
28
|
-
"@editframe/elements": "0.10.0-beta.
|
|
29
|
-
"@editframe/vite-plugin": "0.10.0-beta.
|
|
26
|
+
"@editframe/api": "0.10.0-beta.6",
|
|
27
|
+
"@editframe/assets": "0.10.0-beta.6",
|
|
28
|
+
"@editframe/elements": "0.10.0-beta.6",
|
|
29
|
+
"@editframe/vite-plugin": "0.10.0-beta.6",
|
|
30
30
|
"@inquirer/prompts": "^5.3.8",
|
|
31
31
|
"axios": "^1.6.8",
|
|
32
32
|
"chalk": "^5.3.0",
|
package/src/commands/process.ts
CHANGED
|
@@ -17,7 +17,6 @@ program
|
|
|
17
17
|
.action(async (directory) => {
|
|
18
18
|
directory ??= ".";
|
|
19
19
|
|
|
20
|
-
// const srcDir = path.join(directory, "src");
|
|
21
20
|
const distDir = path.join(directory, "dist");
|
|
22
21
|
await withSpinner("Building\n", async () => {
|
|
23
22
|
spawnSync("npx", ["vite", "build", directory], {
|
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";
|
|
9
|
-
import
|
|
8
|
+
import * as tar from "tar";
|
|
10
9
|
|
|
11
10
|
import { md5Directory, md5FilePath } from "@editframe/assets";
|
|
12
11
|
|
|
13
|
-
import {
|
|
14
|
-
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
15
13
|
import { createRender, uploadRender } from "@editframe/api";
|
|
14
|
+
import debug from "debug";
|
|
15
|
+
import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.ts";
|
|
16
|
+
import { processRenderInfo } from "../operations/processRenderInfo.ts";
|
|
17
|
+
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
|
|
18
|
+
import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.ts";
|
|
19
|
+
import { getFolderSize } from "../utils/getFolderSize.ts";
|
|
20
|
+
import { getClient } from "../utils/index.ts";
|
|
16
21
|
import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.ts";
|
|
17
22
|
import { PreviewServer } from "../utils/startPreviewServer.ts";
|
|
18
|
-
import { getClient } from "../utils/index.ts";
|
|
19
|
-
import { getRenderInfo } from "../operations/getRenderInfo.ts";
|
|
20
|
-
import { processRenderInfo } from "../operations/processRenderInfo.ts";
|
|
21
23
|
import { validateVideoResolution } from "../utils/validateVideoResolution.ts";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
interface StrategyBuilder {
|
|
25
|
-
buildAssetId: (assetPath: string) => Promise<string>;
|
|
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
|
-
}
|
|
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,27 +51,33 @@ 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
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
await withSpinner("Building\n", async () => {
|
|
69
|
+
spawnSync(
|
|
70
|
+
"npx",
|
|
71
|
+
// biome-ignore format: Grouping CLI arguments
|
|
72
|
+
[
|
|
73
|
+
"vite", "build", directory,
|
|
74
|
+
"--clearScreen", "false",
|
|
75
|
+
"--logLevel", "debug",
|
|
76
|
+
],
|
|
77
|
+
{
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
81
|
});
|
|
82
82
|
} catch (error) {
|
|
83
83
|
console.error("Build failed:", error);
|
|
@@ -96,7 +96,7 @@ program
|
|
|
96
96
|
headless: true,
|
|
97
97
|
},
|
|
98
98
|
async (page) => {
|
|
99
|
-
const renderInfo = await page.evaluate(getRenderInfo);
|
|
99
|
+
const renderInfo = RenderInfo.parse(await page.evaluate(getRenderInfo));
|
|
100
100
|
|
|
101
101
|
validateVideoResolution({
|
|
102
102
|
width: renderInfo.width,
|
|
@@ -108,20 +108,41 @@ program
|
|
|
108
108
|
const doc = parseHTML(
|
|
109
109
|
await readFile(path.join(distDir, "index.html"), "utf-8"),
|
|
110
110
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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}`);
|
|
117
|
+
if (element.hasAttribute("asset-id")) {
|
|
118
|
+
log(
|
|
119
|
+
`Asset ID for ${element.tagName} ${element.getAttribute("src")} is ${element.getAttribute("asset-id")}`,
|
|
120
|
+
);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
115
123
|
const src = element.getAttribute("src");
|
|
116
124
|
if (!src) {
|
|
125
|
+
log(`No src attribute for ${element.tagName}`);
|
|
117
126
|
continue;
|
|
118
127
|
}
|
|
119
|
-
const assetPath = path.join(srcDir, src);
|
|
120
128
|
|
|
121
|
-
element.
|
|
122
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
await writeFile(path.join(distDir, "index.html"), doc.toString());
|
|
@@ -159,7 +180,7 @@ program
|
|
|
159
180
|
const readable = new PassThrough();
|
|
160
181
|
tarStream.pipe(readable);
|
|
161
182
|
const folderSize = await getFolderSize(distDir);
|
|
162
|
-
await uploadRender(getClient(),
|
|
183
|
+
await uploadRender(getClient(), render.id, readable, folderSize);
|
|
163
184
|
process.stderr.write("Render assets uploaded\n");
|
|
164
185
|
process.stderr.write(inspect(render));
|
|
165
186
|
process.stderr.write("\n");
|
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
|
});
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* RUN IN A WEB BROWSER. LOGS ARE CAPTURED AND RE-LOGGED.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
6
8
|
import type {
|
|
7
9
|
EFCaptions,
|
|
8
10
|
EFImage,
|
|
@@ -10,6 +12,18 @@ import type {
|
|
|
10
12
|
EFTimegroup,
|
|
11
13
|
} from "@editframe/elements";
|
|
12
14
|
|
|
15
|
+
export const RenderInfo = z.object({
|
|
16
|
+
width: z.number().positive(),
|
|
17
|
+
height: z.number().positive(),
|
|
18
|
+
fps: z.number().positive(),
|
|
19
|
+
durationMs: z.number().positive(),
|
|
20
|
+
assets: z.object({
|
|
21
|
+
efMedia: z.record(z.any()),
|
|
22
|
+
efCaptions: z.array(z.string()),
|
|
23
|
+
efImage: z.array(z.string()),
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
13
27
|
export const getRenderInfo = async () => {
|
|
14
28
|
const rootTimeGroup = document.querySelector("ef-timegroup") as
|
|
15
29
|
| EFTimegroup
|
|
@@ -13,6 +13,9 @@ export const processRenderInfo = async (
|
|
|
13
13
|
process.stderr.write(src);
|
|
14
14
|
process.stderr.write("\n");
|
|
15
15
|
for (const trackId in tracks) {
|
|
16
|
+
process.stderr.write("Generating track: ");
|
|
17
|
+
process.stderr.write(trackId);
|
|
18
|
+
process.stderr.write("\n");
|
|
16
19
|
await generateTrack(
|
|
17
20
|
"./src/assets",
|
|
18
21
|
`./src${src}`,
|
|
@@ -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
|
+
}
|