@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.
Files changed (59) hide show
  1. package/dist/VERSION.d.ts +1 -1
  2. package/dist/VERSION.js +1 -1
  3. package/dist/commands/render.d.ts +1 -1
  4. package/dist/commands/render.js +77 -51
  5. package/dist/commands/sync.js +5 -2
  6. package/dist/operations/getRenderInfo.d.ts +40 -4
  7. package/dist/operations/getRenderInfo.js +13 -0
  8. package/dist/operations/processRenderInfo.js +3 -0
  9. package/dist/operations/syncAssetsDirectory/SubAssetSync.d.ts +20 -0
  10. package/dist/operations/syncAssetsDirectory/SubAssetSync.js +26 -0
  11. package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +19 -0
  12. package/dist/operations/syncAssetsDirectory/SyncCaption.js +66 -0
  13. package/dist/operations/syncAssetsDirectory/SyncCaption.test.d.ts +1 -0
  14. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +20 -0
  15. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +79 -0
  16. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.test.d.ts +1 -0
  17. package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +23 -0
  18. package/dist/operations/syncAssetsDirectory/SyncImage.js +95 -0
  19. package/dist/operations/syncAssetsDirectory/SyncImage.test.d.ts +1 -0
  20. package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +41 -0
  21. package/dist/operations/syncAssetsDirectory/SyncStatus.js +43 -0
  22. package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +70 -0
  23. package/dist/operations/syncAssetsDirectory/SyncTrack.js +138 -0
  24. package/dist/operations/syncAssetsDirectory/SyncTrack.test.d.ts +1 -0
  25. package/dist/operations/syncAssetsDirectory/doAssetSync.d.ts +5 -0
  26. package/dist/operations/syncAssetsDirectory/doAssetSync.js +48 -0
  27. package/dist/operations/syncAssetsDirectory/doAssetSync.test.d.ts +1 -0
  28. package/dist/operations/syncAssetsDirectory.d.ts +1 -1
  29. package/dist/operations/syncAssetsDirectory.js +20 -240
  30. package/dist/test-fixtures/fixture.d.ts +26 -0
  31. package/dist/utils/index.js +4 -1
  32. package/package.json +5 -5
  33. package/src/commands/process.ts +0 -1
  34. package/src/commands/render.ts +79 -58
  35. package/src/commands/sync.ts +5 -2
  36. package/src/operations/getRenderInfo.ts +14 -0
  37. package/src/operations/processRenderInfo.ts +3 -0
  38. package/src/operations/syncAssetsDirectory/SubAssetSync.ts +42 -0
  39. package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +145 -0
  40. package/src/operations/syncAssetsDirectory/SyncCaption.ts +76 -0
  41. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +151 -0
  42. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +92 -0
  43. package/src/operations/syncAssetsDirectory/SyncImage.test.ts +131 -0
  44. package/src/operations/syncAssetsDirectory/SyncImage.ts +112 -0
  45. package/src/operations/syncAssetsDirectory/SyncStatus.ts +51 -0
  46. package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +222 -0
  47. package/src/operations/syncAssetsDirectory/SyncTrack.ts +164 -0
  48. package/src/operations/syncAssetsDirectory/doAssetSync.test.ts +134 -0
  49. package/src/operations/syncAssetsDirectory/doAssetSync.ts +62 -0
  50. package/src/operations/syncAssetsDirectory.test.ts +482 -0
  51. package/src/operations/syncAssetsDirectory.ts +22 -283
  52. package/src/utils/index.ts +4 -1
  53. package/test-fixtures/fixture.ts +141 -0
  54. package/test-fixtures/network.ts +181 -0
  55. package/test-fixtures/test-captions.json +9 -0
  56. package/test-fixtures/test.mp4 +0 -0
  57. package/test-fixtures/test.png +0 -0
  58. package/src/commands/render.test.ts +0 -34
  59. /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.4";
1
+ export declare const VERSION = "0.10.0-beta.6";
package/dist/VERSION.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "0.10.0-beta.4";
1
+ const VERSION = "0.10.0-beta.6";
2
2
  export {
3
3
  VERSION
4
4
  };
@@ -1 +1 @@
1
- export declare const buildAssetId: (assetPath: string) => Promise<string>;
1
+ export declare const buildAssetId: (srcDir: string, src: string, basename: string) => Promise<string>;
@@ -1,63 +1,69 @@
1
- import path from "node:path";
2
1
  import { readFile, writeFile } from "node:fs/promises";
3
- import { inspect } from "node:util";
2
+ import path, { join, basename } from "node:path";
4
3
  import { PassThrough } from "node:stream";
5
- import * as tar from "tar";
4
+ import { inspect } from "node:util";
6
5
  import { program, Option } from "commander";
7
6
  import { parse } from "node-html-parser";
8
- import { build } from "vite";
7
+ import * as tar from "tar";
9
8
  import { md5Directory, md5FilePath } from "@editframe/assets";
10
- import { withSpinner } from "../utils/withSpinner.js";
11
- import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
9
+ import { spawnSync } from "node:child_process";
12
10
  import { createRender, uploadRender } from "@editframe/api";
11
+ import debug from "debug";
12
+ import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.js";
13
+ import { processRenderInfo } from "../operations/processRenderInfo.js";
14
+ import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
15
+ import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.js";
16
+ import { getFolderSize } from "../utils/getFolderSize.js";
17
+ import { getClient } from "../utils/index.js";
13
18
  import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.js";
14
19
  import { PreviewServer } from "../utils/startPreviewServer.js";
15
- import { getClient } from "../utils/index.js";
16
- import { getRenderInfo } from "../operations/getRenderInfo.js";
17
- import { processRenderInfo } from "../operations/processRenderInfo.js";
18
20
  import { validateVideoResolution } from "../utils/validateVideoResolution.js";
19
- import { getFolderSize } from "../utils/getFolderSize.js";
20
- const buildAssetId = async (assetPath) => {
21
- const md5Sum = await md5FilePath(assetPath);
22
- const basename = path.basename(assetPath);
23
- return `${md5Sum}:${basename}`;
24
- };
25
- class V1Builder {
26
- async buildAssetId(assetPath) {
27
- return buildAssetId(assetPath);
28
- }
29
- }
30
- class V2Builder {
31
- async buildAssetId(assetPath) {
32
- return buildAssetId(assetPath);
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", "v2"]).default("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(directory);
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
- await build({
56
- root: directory,
57
- logLevel: "info",
58
- // Optional: adjust log level as needed
59
- clearScreen: false
60
- // Optional: keep console output clean
50
+ await withSpinner("Building\n", async () => {
51
+ spawnSync(
52
+ "npx",
53
+ // biome-ignore format: Grouping CLI arguments
54
+ [
55
+ "vite",
56
+ "build",
57
+ directory,
58
+ "--clearScreen",
59
+ "false",
60
+ "--logLevel",
61
+ "debug"
62
+ ],
63
+ {
64
+ stdio: "inherit"
65
+ }
66
+ );
61
67
  });
62
68
  } catch (error) {
63
69
  console.error("Build failed:", error);
@@ -75,7 +81,7 @@ program.command("render [directory]").description(
75
81
  headless: true
76
82
  },
77
83
  async (page) => {
78
- const renderInfo = await page.evaluate(getRenderInfo);
84
+ const renderInfo = RenderInfo.parse(await page.evaluate(getRenderInfo));
79
85
  validateVideoResolution({
80
86
  width: renderInfo.width,
81
87
  height: renderInfo.height
@@ -84,19 +90,39 @@ program.command("render [directory]").description(
84
90
  const doc = parse(
85
91
  await readFile(path.join(distDir, "index.html"), "utf-8")
86
92
  );
87
- const elements = doc.querySelectorAll(
88
- "ef-audio, ef-video, ef-image, ef-captions"
89
- );
90
- for (const element of elements) {
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}`);
98
+ if (element.hasAttribute("asset-id")) {
99
+ log(
100
+ `Asset ID for ${element.tagName} ${element.getAttribute("src")} is ${element.getAttribute("asset-id")}`
101
+ );
102
+ continue;
103
+ }
91
104
  const src = element.getAttribute("src");
92
105
  if (!src) {
106
+ log(`No src attribute for ${element.tagName}`);
93
107
  continue;
94
108
  }
95
- const assetPath = path.join(srcDir, src);
96
- element.setAttribute(
97
- "asset-id",
98
- await builder.buildAssetId(assetPath)
99
- );
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
+ }
100
126
  }
101
127
  await writeFile(path.join(distDir, "index.html"), doc.toString());
102
128
  const md5 = await md5Directory(distDir);
@@ -126,7 +152,7 @@ program.command("render [directory]").description(
126
152
  const readable = new PassThrough();
127
153
  tarStream.pipe(readable);
128
154
  const folderSize = await getFolderSize(distDir);
129
- await uploadRender(getClient(), md5, readable, folderSize);
155
+ await uploadRender(getClient(), render.id, readable, folderSize);
130
156
  process.stderr.write("Render assets uploaded\n");
131
157
  process.stderr.write(inspect(render));
132
158
  process.stderr.write("\n");
@@ -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 (projectDirectory = ".") => {
4
- await syncAssetDirectory(projectDirectory);
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
  });
@@ -1,7 +1,43 @@
1
- /**
2
- * THIS MODULE DOESNT USE THE DEBUG LOGGER BECAUSE IT IS
3
- * RUN IN A WEB BROWSER. LOGS ARE CAPTURED AND RE-LOGGED.
4
- */
1
+ import { z } from 'zod';
2
+ export declare const RenderInfo: z.ZodObject<{
3
+ width: z.ZodNumber;
4
+ height: z.ZodNumber;
5
+ fps: z.ZodNumber;
6
+ durationMs: z.ZodNumber;
7
+ assets: z.ZodObject<{
8
+ efMedia: z.ZodRecord<z.ZodString, z.ZodAny>;
9
+ efCaptions: z.ZodArray<z.ZodString, "many">;
10
+ efImage: z.ZodArray<z.ZodString, "many">;
11
+ }, "strip", z.ZodTypeAny, {
12
+ efMedia: Record<string, any>;
13
+ efCaptions: string[];
14
+ efImage: string[];
15
+ }, {
16
+ efMedia: Record<string, any>;
17
+ efCaptions: string[];
18
+ efImage: string[];
19
+ }>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ height: number;
22
+ width: number;
23
+ fps: number;
24
+ assets: {
25
+ efMedia: Record<string, any>;
26
+ efCaptions: string[];
27
+ efImage: string[];
28
+ };
29
+ durationMs: number;
30
+ }, {
31
+ height: number;
32
+ width: number;
33
+ fps: number;
34
+ assets: {
35
+ efMedia: Record<string, any>;
36
+ efCaptions: string[];
37
+ efImage: string[];
38
+ };
39
+ durationMs: number;
40
+ }>;
5
41
  export declare const getRenderInfo: () => Promise<{
6
42
  width: number;
7
43
  height: number;
@@ -1,3 +1,15 @@
1
+ import { z } from "zod";
2
+ const RenderInfo = z.object({
3
+ width: z.number().positive(),
4
+ height: z.number().positive(),
5
+ fps: z.number().positive(),
6
+ durationMs: z.number().positive(),
7
+ assets: z.object({
8
+ efMedia: z.record(z.any()),
9
+ efCaptions: z.array(z.string()),
10
+ efImage: z.array(z.string())
11
+ })
12
+ });
1
13
  const getRenderInfo = async () => {
2
14
  const rootTimeGroup = document.querySelector("ef-timegroup");
3
15
  if (!rootTimeGroup) {
@@ -55,5 +67,6 @@ const getRenderInfo = async () => {
55
67
  return renderInfo;
56
68
  };
57
69
  export {
70
+ RenderInfo,
58
71
  getRenderInfo
59
72
  };
@@ -5,6 +5,9 @@ const processRenderInfo = async (renderInfo) => {
5
5
  process.stderr.write(src);
6
6
  process.stderr.write("\n");
7
7
  for (const trackId in tracks) {
8
+ process.stderr.write("Generating track: ");
9
+ process.stderr.write(trackId);
10
+ process.stderr.write("\n");
8
11
  await generateTrack(
9
12
  "./src/assets",
10
13
  `./src${src}`,
@@ -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,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,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
+ };