@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/dist/VERSION.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.10.0-beta.
|
|
1
|
+
export declare const VERSION = "0.10.0-beta.7";
|
package/dist/VERSION.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const buildAssetId: (
|
|
1
|
+
export declare const buildAssetId: (srcDir: string, src: string, basename: string) => Promise<string>;
|
package/dist/commands/render.js
CHANGED
|
@@ -1,55 +1,50 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
2
|
+
import path, { join, basename } from "node:path";
|
|
4
3
|
import { PassThrough } from "node:stream";
|
|
5
|
-
import
|
|
4
|
+
import { inspect } from "node:util";
|
|
6
5
|
import { program, Option } from "commander";
|
|
7
6
|
import { parse } from "node-html-parser";
|
|
7
|
+
import * as tar from "tar";
|
|
8
8
|
import { md5Directory, md5FilePath } from "@editframe/assets";
|
|
9
|
-
import {
|
|
10
|
-
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
11
10
|
import { createRender, uploadRender } from "@editframe/api";
|
|
12
|
-
import
|
|
13
|
-
import { PreviewServer } from "../utils/startPreviewServer.js";
|
|
14
|
-
import { getClient } from "../utils/index.js";
|
|
11
|
+
import debug from "debug";
|
|
15
12
|
import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.js";
|
|
16
13
|
import { processRenderInfo } from "../operations/processRenderInfo.js";
|
|
17
|
-
import {
|
|
14
|
+
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
|
|
15
|
+
import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.js";
|
|
18
16
|
import { getFolderSize } from "../utils/getFolderSize.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
import { getClient } from "../utils/index.js";
|
|
18
|
+
import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.js";
|
|
19
|
+
import { PreviewServer } from "../utils/startPreviewServer.js";
|
|
20
|
+
import { validateVideoResolution } from "../utils/validateVideoResolution.js";
|
|
21
|
+
import { withSpinner } from "../utils/withSpinner.js";
|
|
22
|
+
const log = debug("ef:cli:render");
|
|
23
|
+
const buildAssetId = async (srcDir, src, basename2) => {
|
|
24
|
+
log(`Building image asset id for ${src}
|
|
25
|
+
`);
|
|
26
|
+
const assetPath = path.join(srcDir, src);
|
|
27
|
+
const assetMd5 = await md5FilePath(assetPath);
|
|
28
|
+
const syncStatus = new SyncStatus(
|
|
29
|
+
join(srcDir, "assets", ".cache", assetMd5, basename2)
|
|
30
|
+
);
|
|
31
|
+
const info = await syncStatus.readInfo();
|
|
32
|
+
if (!info) {
|
|
33
|
+
throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
const strategyBuilders = {
|
|
36
|
-
v1: new V1Builder(),
|
|
37
|
-
v2: new V2Builder()
|
|
35
|
+
return info.asset_id;
|
|
38
36
|
};
|
|
39
37
|
program.command("render [directory]").description(
|
|
40
38
|
"Render a directory's index.html file as a video in the editframe cloud"
|
|
41
39
|
).addOption(
|
|
42
|
-
new Option("-s, --strategy <strategy>", "Render strategy").choices(["v1"
|
|
40
|
+
new Option("-s, --strategy <strategy>", "Render strategy").choices(["v1"]).default("v1")
|
|
43
41
|
).action(async (directory, options) => {
|
|
44
42
|
directory ??= ".";
|
|
45
|
-
await syncAssetDirectory(
|
|
43
|
+
await syncAssetDirectory(
|
|
44
|
+
join(process.cwd(), directory, "src", "assets", ".cache")
|
|
45
|
+
);
|
|
46
46
|
const srcDir = path.join(directory, "src");
|
|
47
47
|
const distDir = path.join(directory, "dist");
|
|
48
|
-
const builder = strategyBuilders[options.strategy];
|
|
49
|
-
if (!builder) {
|
|
50
|
-
console.error("Invalid strategy", options.strategy);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
48
|
await withSpinner("Building\n", async () => {
|
|
54
49
|
try {
|
|
55
50
|
await withSpinner("Building\n", async () => {
|
|
@@ -95,22 +90,39 @@ program.command("render [directory]").description(
|
|
|
95
90
|
const doc = parse(
|
|
96
91
|
await readFile(path.join(distDir, "index.html"), "utf-8")
|
|
97
92
|
);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
log("Building asset IDs");
|
|
94
|
+
for (const element of doc.querySelectorAll(
|
|
95
|
+
"ef-image, ef-audio, ef-video"
|
|
96
|
+
)) {
|
|
97
|
+
log(`Processing ${element.tagName}`);
|
|
102
98
|
if (element.hasAttribute("asset-id")) {
|
|
99
|
+
log(
|
|
100
|
+
`Asset ID for ${element.tagName} ${element.getAttribute("src")} is ${element.getAttribute("asset-id")}`
|
|
101
|
+
);
|
|
103
102
|
continue;
|
|
104
103
|
}
|
|
105
104
|
const src = element.getAttribute("src");
|
|
106
105
|
if (!src) {
|
|
106
|
+
log(`No src attribute for ${element.tagName}`);
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
switch (element.tagName) {
|
|
110
|
+
case "EF-IMAGE":
|
|
111
|
+
element.setAttribute(
|
|
112
|
+
"asset-id",
|
|
113
|
+
await buildAssetId(srcDir, src, basename(src))
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
case "EF-AUDIO":
|
|
117
|
+
case "EF-VIDEO":
|
|
118
|
+
element.setAttribute(
|
|
119
|
+
"asset-id",
|
|
120
|
+
await buildAssetId(srcDir, src, "isobmff")
|
|
121
|
+
);
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
log(`Unknown element type: ${element.tagName}`);
|
|
125
|
+
}
|
|
114
126
|
}
|
|
115
127
|
await writeFile(path.join(distDir, "index.html"), doc.toString());
|
|
116
128
|
const md5 = await md5Directory(distDir);
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { program } from "commander";
|
|
2
3
|
import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
|
|
3
|
-
program.command("sync").description("Sync assets to Editframe servers for rendering").argument("[directory]", "Path to project directory to sync.").action(async (
|
|
4
|
-
await syncAssetDirectory(
|
|
4
|
+
program.command("sync").description("Sync assets to Editframe servers for rendering").argument("[directory]", "Path to project directory to sync.").action(async (directory = ".") => {
|
|
5
|
+
await syncAssetDirectory(
|
|
6
|
+
join(process.cwd(), directory, "src", "assets", ".cache")
|
|
7
|
+
);
|
|
5
8
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SyncStatus } from './SyncStatus.ts';
|
|
2
|
+
import { SyncCaption } from './SyncCaption.ts';
|
|
3
|
+
import { SyncFragmentIndex } from './SyncFragmentIndex.ts';
|
|
4
|
+
import { SyncImage } from './SyncImage.ts';
|
|
5
|
+
import { SyncTrack } from './SyncTrack.ts';
|
|
6
|
+
export interface SubAssetSync<CreationType> {
|
|
7
|
+
icon: string;
|
|
8
|
+
label: string;
|
|
9
|
+
path: string;
|
|
10
|
+
md5: string;
|
|
11
|
+
prepare: () => Promise<void>;
|
|
12
|
+
validate: () => Promise<void>;
|
|
13
|
+
create: () => Promise<void>;
|
|
14
|
+
upload: () => Promise<void>;
|
|
15
|
+
syncStatus: SyncStatus;
|
|
16
|
+
isComplete: () => boolean;
|
|
17
|
+
markSynced: () => Promise<void>;
|
|
18
|
+
created: CreationType | null;
|
|
19
|
+
}
|
|
20
|
+
export declare const getAssetSync: (subAssetPath: string, md5: string) => SyncCaption | SyncFragmentIndex | SyncImage | SyncTrack;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SyncCaption } from "./SyncCaption.js";
|
|
2
|
+
import { SyncFragmentIndex } from "./SyncFragmentIndex.js";
|
|
3
|
+
import { SyncImage } from "./SyncImage.js";
|
|
4
|
+
import { SyncTrack } from "./SyncTrack.js";
|
|
5
|
+
const trackMatch = /\.track-[\d]+.mp4$/i;
|
|
6
|
+
const fragmentIndexMatch = /\.tracks.json$/i;
|
|
7
|
+
const captionsMatch = /\.captions.json$/i;
|
|
8
|
+
const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
|
|
9
|
+
const getAssetSync = (subAssetPath, md5) => {
|
|
10
|
+
if (imageMatch.test(subAssetPath)) {
|
|
11
|
+
return new SyncImage(subAssetPath, md5);
|
|
12
|
+
}
|
|
13
|
+
if (trackMatch.test(subAssetPath)) {
|
|
14
|
+
return new SyncTrack(subAssetPath, md5);
|
|
15
|
+
}
|
|
16
|
+
if (fragmentIndexMatch.test(subAssetPath)) {
|
|
17
|
+
return new SyncFragmentIndex(subAssetPath, md5);
|
|
18
|
+
}
|
|
19
|
+
if (captionsMatch.test(subAssetPath)) {
|
|
20
|
+
return new SyncCaption(subAssetPath, md5);
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Unrecognized sub-asset type: ${subAssetPath}`);
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
getAssetSync
|
|
26
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CreateCaptionFileResult } from '../../../../api/src';
|
|
2
|
+
import { SubAssetSync } from './SubAssetSync.ts';
|
|
3
|
+
import { SyncStatus } from './SyncStatus.ts';
|
|
4
|
+
export declare class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
|
|
5
|
+
path: string;
|
|
6
|
+
md5: string;
|
|
7
|
+
icon: string;
|
|
8
|
+
label: string;
|
|
9
|
+
syncStatus: SyncStatus;
|
|
10
|
+
created: CreateCaptionFileResult | null;
|
|
11
|
+
constructor(path: string, md5: string);
|
|
12
|
+
byteSize(): Promise<number>;
|
|
13
|
+
prepare(): Promise<void>;
|
|
14
|
+
validate(): Promise<void>;
|
|
15
|
+
create(): Promise<void>;
|
|
16
|
+
isComplete(): boolean;
|
|
17
|
+
upload(): Promise<void>;
|
|
18
|
+
markSynced(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createCaptionFile, uploadCaptionFile } from "@editframe/api";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { getClient } from "../../utils/index.js";
|
|
5
|
+
import { SyncStatus } from "./SyncStatus.js";
|
|
6
|
+
class SyncCaption {
|
|
7
|
+
constructor(path, md5) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
this.md5 = md5;
|
|
10
|
+
this.icon = "📝";
|
|
11
|
+
this.label = "captions";
|
|
12
|
+
this.syncStatus = new SyncStatus(this.path);
|
|
13
|
+
this.created = null;
|
|
14
|
+
}
|
|
15
|
+
async byteSize() {
|
|
16
|
+
return (await fs.stat(this.path)).size;
|
|
17
|
+
}
|
|
18
|
+
async prepare() {
|
|
19
|
+
}
|
|
20
|
+
async validate() {
|
|
21
|
+
}
|
|
22
|
+
async create() {
|
|
23
|
+
this.created = await createCaptionFile(getClient(), {
|
|
24
|
+
md5: this.md5,
|
|
25
|
+
filename: this.path.replace(/\.captions.json$/, ""),
|
|
26
|
+
byte_size: await this.byteSize()
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
isComplete() {
|
|
30
|
+
return !!this.created?.complete;
|
|
31
|
+
}
|
|
32
|
+
async upload() {
|
|
33
|
+
if (!this.created) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Caption not created. Should have been prevented by .isComplete()"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
await uploadCaptionFile(
|
|
39
|
+
getClient(),
|
|
40
|
+
this.created.id,
|
|
41
|
+
// It's not clear why we need to use Readable.from here, but it seems
|
|
42
|
+
// to fix an issue where the request is closed early in tests
|
|
43
|
+
Readable.from(await fs.readFile(this.path)),
|
|
44
|
+
await this.byteSize()
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
async markSynced() {
|
|
48
|
+
if (!this.created) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Caption not created. Should have been prevented by .isComplete()"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const byteSize = await this.byteSize();
|
|
54
|
+
await this.syncStatus.markSynced({
|
|
55
|
+
version: "1",
|
|
56
|
+
complete: true,
|
|
57
|
+
id: this.created.id,
|
|
58
|
+
md5: this.md5,
|
|
59
|
+
asset_id: this.created.asset_id,
|
|
60
|
+
byte_size: byteSize
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
SyncCaption
|
|
66
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CreateISOBMFFFileResult } from '../../../../api/src';
|
|
2
|
+
import { SubAssetSync } from './SubAssetSync.ts';
|
|
3
|
+
import { SyncStatus } from './SyncStatus.ts';
|
|
4
|
+
export declare class SyncFragmentIndex implements SubAssetSync<CreateISOBMFFFileResult> {
|
|
5
|
+
path: string;
|
|
6
|
+
md5: string;
|
|
7
|
+
icon: string;
|
|
8
|
+
label: string;
|
|
9
|
+
syncStatus: SyncStatus;
|
|
10
|
+
fileSyncStatus: SyncStatus;
|
|
11
|
+
created: CreateISOBMFFFileResult | null;
|
|
12
|
+
constructor(path: string, md5: string);
|
|
13
|
+
byteSize(): Promise<number>;
|
|
14
|
+
prepare(): Promise<void>;
|
|
15
|
+
validate(): Promise<void>;
|
|
16
|
+
create(): Promise<void>;
|
|
17
|
+
isComplete(): boolean;
|
|
18
|
+
upload(): Promise<void>;
|
|
19
|
+
markSynced(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { createISOBMFFFile, uploadFragmentIndex } from "@editframe/api";
|
|
5
|
+
import { getClient } from "../../utils/index.js";
|
|
6
|
+
import { SyncStatus } from "./SyncStatus.js";
|
|
7
|
+
class SyncFragmentIndex {
|
|
8
|
+
constructor(path, md5) {
|
|
9
|
+
this.path = path;
|
|
10
|
+
this.md5 = md5;
|
|
11
|
+
this.icon = "📋";
|
|
12
|
+
this.label = "fragment index";
|
|
13
|
+
this.syncStatus = new SyncStatus(this.path);
|
|
14
|
+
this.fileSyncStatus = new SyncStatus(
|
|
15
|
+
join(dirname(this.path), "isobmff")
|
|
16
|
+
);
|
|
17
|
+
this.created = null;
|
|
18
|
+
}
|
|
19
|
+
async byteSize() {
|
|
20
|
+
return (await fs.stat(this.path)).size;
|
|
21
|
+
}
|
|
22
|
+
async prepare() {
|
|
23
|
+
}
|
|
24
|
+
async validate() {
|
|
25
|
+
}
|
|
26
|
+
async create() {
|
|
27
|
+
this.created = await createISOBMFFFile(getClient(), {
|
|
28
|
+
md5: this.md5,
|
|
29
|
+
filename: this.path.replace(/\.tracks.json$/, "")
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
isComplete() {
|
|
33
|
+
return !!this.created?.fragment_index_complete;
|
|
34
|
+
}
|
|
35
|
+
async upload() {
|
|
36
|
+
if (!this.created) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Fragment index not created. Should have been prevented by .isComplete()"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
await uploadFragmentIndex(
|
|
42
|
+
getClient(),
|
|
43
|
+
this.created.id,
|
|
44
|
+
// It is unclear why we need to use Readable.from here
|
|
45
|
+
// Tests fail when using createReadStream
|
|
46
|
+
Readable.from(await fs.readFile(this.path)),
|
|
47
|
+
await this.byteSize()
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
async markSynced() {
|
|
51
|
+
if (!this.created) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Fragment index not created. Should have been prevented by .isComplete()"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const byteSize = await this.byteSize();
|
|
57
|
+
await Promise.all([
|
|
58
|
+
this.syncStatus.markSynced({
|
|
59
|
+
version: "1",
|
|
60
|
+
complete: true,
|
|
61
|
+
id: this.created.id,
|
|
62
|
+
md5: this.md5,
|
|
63
|
+
asset_id: this.created.asset_id,
|
|
64
|
+
byte_size: byteSize
|
|
65
|
+
}),
|
|
66
|
+
this.fileSyncStatus.markSynced({
|
|
67
|
+
version: "1",
|
|
68
|
+
complete: true,
|
|
69
|
+
id: this.created.id,
|
|
70
|
+
md5: this.md5,
|
|
71
|
+
asset_id: this.created.asset_id,
|
|
72
|
+
byte_size: byteSize
|
|
73
|
+
})
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
SyncFragmentIndex
|
|
79
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CreateImageFileResult } from '../../../../api/src';
|
|
2
|
+
import { Probe } from '../../../../assets/src';
|
|
3
|
+
import { SubAssetSync } from './SubAssetSync.ts';
|
|
4
|
+
import { SyncStatus } from './SyncStatus.ts';
|
|
5
|
+
export declare class SyncImage implements SubAssetSync<CreateImageFileResult> {
|
|
6
|
+
path: string;
|
|
7
|
+
md5: string;
|
|
8
|
+
icon: string;
|
|
9
|
+
label: string;
|
|
10
|
+
syncStatus: SyncStatus;
|
|
11
|
+
created: CreateImageFileResult | null;
|
|
12
|
+
constructor(path: string, md5: string);
|
|
13
|
+
private _probeResult;
|
|
14
|
+
prepare(): Promise<void>;
|
|
15
|
+
get probeResult(): Probe;
|
|
16
|
+
get extension(): string;
|
|
17
|
+
byteSize(): Promise<number>;
|
|
18
|
+
validate(): Promise<void>;
|
|
19
|
+
create(): Promise<void>;
|
|
20
|
+
isComplete(): boolean;
|
|
21
|
+
upload(): Promise<void>;
|
|
22
|
+
markSynced(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path, { basename } from "node:path";
|
|
4
|
+
import { createImageFile, uploadImageFile } from "@editframe/api";
|
|
5
|
+
import { Probe } from "@editframe/assets";
|
|
6
|
+
import { getClient } from "../../utils/index.js";
|
|
7
|
+
import { SyncStatus } from "./SyncStatus.js";
|
|
8
|
+
class SyncImage {
|
|
9
|
+
constructor(path2, md5) {
|
|
10
|
+
this.path = path2;
|
|
11
|
+
this.md5 = md5;
|
|
12
|
+
this.icon = "🖼️";
|
|
13
|
+
this.label = "image";
|
|
14
|
+
this.syncStatus = new SyncStatus(this.path);
|
|
15
|
+
this.created = null;
|
|
16
|
+
this._probeResult = null;
|
|
17
|
+
}
|
|
18
|
+
async prepare() {
|
|
19
|
+
this._probeResult = await Probe.probePath(this.path);
|
|
20
|
+
}
|
|
21
|
+
get probeResult() {
|
|
22
|
+
if (!this._probeResult) {
|
|
23
|
+
throw new Error("Probe result not found. Call prepare() first.");
|
|
24
|
+
}
|
|
25
|
+
return this._probeResult;
|
|
26
|
+
}
|
|
27
|
+
get extension() {
|
|
28
|
+
return path.extname(this.path).slice(1);
|
|
29
|
+
}
|
|
30
|
+
async byteSize() {
|
|
31
|
+
return (await fs.stat(this.path)).size;
|
|
32
|
+
}
|
|
33
|
+
async validate() {
|
|
34
|
+
const [videoProbe] = this.probeResult.videoStreams;
|
|
35
|
+
if (!videoProbe) {
|
|
36
|
+
throw new Error(`No media info found in image: ${this.path}`);
|
|
37
|
+
}
|
|
38
|
+
const ext = this.extension;
|
|
39
|
+
if (!(ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "webp")) {
|
|
40
|
+
throw new Error(`Invalid image format: ${this.path}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async create() {
|
|
44
|
+
const byteSize = (await fs.stat(this.path)).size;
|
|
45
|
+
const [videoProbe] = this.probeResult.videoStreams;
|
|
46
|
+
if (!videoProbe) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"No video stream found in image. Should have been prevented by .validate()"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this.created = await createImageFile(getClient(), {
|
|
52
|
+
md5: this.md5,
|
|
53
|
+
filename: basename(this.path),
|
|
54
|
+
width: videoProbe.width,
|
|
55
|
+
height: videoProbe.height,
|
|
56
|
+
mime_type: `image/${this.extension}`,
|
|
57
|
+
byte_size: byteSize
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
isComplete() {
|
|
61
|
+
return !!this.created?.complete;
|
|
62
|
+
}
|
|
63
|
+
upload() {
|
|
64
|
+
if (!this.created) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Image not created. Should have been prevented by .isComplete()"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return uploadImageFile(
|
|
70
|
+
getClient(),
|
|
71
|
+
this.created.id,
|
|
72
|
+
createReadStream(this.path),
|
|
73
|
+
Number.parseInt(this.probeResult.format.size || "0")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
async markSynced() {
|
|
77
|
+
if (!this.created) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Image not created. Should have been prevented by .isComplete()"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const byteSize = await this.byteSize();
|
|
83
|
+
return this.syncStatus.markSynced({
|
|
84
|
+
version: "1",
|
|
85
|
+
complete: true,
|
|
86
|
+
id: this.created.id,
|
|
87
|
+
md5: this.md5,
|
|
88
|
+
asset_id: this.created.asset_id,
|
|
89
|
+
byte_size: byteSize
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
SyncImage
|
|
95
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const SyncStatusSchema: z.ZodObject<{
|
|
3
|
+
version: z.ZodString;
|
|
4
|
+
complete: z.ZodBoolean;
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
md5: z.ZodString;
|
|
7
|
+
asset_id: z.ZodString;
|
|
8
|
+
byte_size: z.ZodNumber;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
md5: string;
|
|
11
|
+
byte_size: number;
|
|
12
|
+
complete: boolean;
|
|
13
|
+
version: string;
|
|
14
|
+
id: string;
|
|
15
|
+
asset_id: string;
|
|
16
|
+
}, {
|
|
17
|
+
md5: string;
|
|
18
|
+
byte_size: number;
|
|
19
|
+
complete: boolean;
|
|
20
|
+
version: string;
|
|
21
|
+
id: string;
|
|
22
|
+
asset_id: string;
|
|
23
|
+
}>;
|
|
24
|
+
export interface SyncStatusInfo extends z.infer<typeof SyncStatusSchema> {
|
|
25
|
+
}
|
|
26
|
+
export declare class SyncStatus {
|
|
27
|
+
private basePath;
|
|
28
|
+
constructor(basePath: string);
|
|
29
|
+
infoPath: string;
|
|
30
|
+
isSynced(): Promise<boolean>;
|
|
31
|
+
readInfo(): Promise<{
|
|
32
|
+
md5: string;
|
|
33
|
+
byte_size: number;
|
|
34
|
+
complete: boolean;
|
|
35
|
+
version: string;
|
|
36
|
+
id: string;
|
|
37
|
+
asset_id: string;
|
|
38
|
+
} | null>;
|
|
39
|
+
markSynced(info: SyncStatusInfo): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const SYNC_VERSION = "1";
|
|
4
|
+
const SyncStatusSchema = z.object({
|
|
5
|
+
version: z.string(),
|
|
6
|
+
complete: z.boolean(),
|
|
7
|
+
id: z.string(),
|
|
8
|
+
md5: z.string(),
|
|
9
|
+
asset_id: z.string(),
|
|
10
|
+
byte_size: z.number()
|
|
11
|
+
});
|
|
12
|
+
class SyncStatus {
|
|
13
|
+
constructor(basePath) {
|
|
14
|
+
this.basePath = basePath;
|
|
15
|
+
this.infoPath = `${this.basePath}.info`;
|
|
16
|
+
}
|
|
17
|
+
async isSynced() {
|
|
18
|
+
const syncInfo = await this.readInfo();
|
|
19
|
+
if (!syncInfo) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return syncInfo.version === SYNC_VERSION && syncInfo.complete;
|
|
23
|
+
}
|
|
24
|
+
async readInfo() {
|
|
25
|
+
try {
|
|
26
|
+
const info = await fs.readFile(this.infoPath, "utf-8");
|
|
27
|
+
return SyncStatusSchema.parse(JSON.parse(info));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async markSynced(info) {
|
|
36
|
+
process.stderr.write(`✏️ Marking asset as synced: ${this.basePath}
|
|
37
|
+
`);
|
|
38
|
+
await fs.writeFile(this.infoPath, JSON.stringify(info, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
SyncStatus
|
|
43
|
+
};
|