@editframe/cli 0.8.0-beta.1 → 0.8.0-beta.10

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 CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.0-beta.1";
1
+ export declare const VERSION = "0.8.0-beta.10";
package/dist/VERSION.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "0.8.0-beta.1";
1
+ const VERSION = "0.8.0-beta.10";
2
2
  export {
3
3
  VERSION
4
4
  };
@@ -1,23 +1,26 @@
1
1
  import { basename } from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { createReadStream } from "node:fs";
2
4
  import { program } from "commander";
3
5
  import { withSpinner } from "../utils/withSpinner.js";
4
6
  import { getClient } from "../utils/index.js";
5
- import { createReadStream } from "node:fs";
6
7
  import { createUnprocessedFile, uploadUnprocessedFile, updateUnprocessedFile } from "@editframe/api";
7
8
  import { md5FilePath } from "@editframe/assets";
8
9
  program.command("process-file <file>").description("Upload a audio/video to Editframe for processing.").action(async (path) => {
9
10
  const client = getClient();
10
11
  const fileId = await md5FilePath(path);
12
+ const byte_size = (await stat(path)).size;
11
13
  await withSpinner("Creating unprocessed file record", async () => {
12
14
  await createUnprocessedFile(client, {
13
15
  id: fileId,
14
16
  processes: [],
15
- filename: basename(path)
17
+ filename: basename(path),
18
+ byte_size
16
19
  });
17
20
  });
18
21
  const readStream = createReadStream(path);
19
22
  await withSpinner("Uploading file", async () => {
20
- await uploadUnprocessedFile(client, fileId, readStream);
23
+ await uploadUnprocessedFile(client, fileId, readStream, byte_size);
21
24
  });
22
25
  const unprocessedFile = await withSpinner(
23
26
  "Marking for processing",
@@ -16,6 +16,7 @@ import { getClient } from "../utils/index.js";
16
16
  import { getRenderInfo } from "../operations/getRenderInfo.js";
17
17
  import { processRenderInfo } from "../operations/processRenderInfo.js";
18
18
  import { validateVideoResolution } from "../utils/validateVideoResolution.js";
19
+ import { getFolderSize } from "../utils/getFolderSize.js";
19
20
  const buildProductionUrl = async (origin, tagName, assetPath) => {
20
21
  const md5Sum = await md5FilePath(assetPath);
21
22
  const basename = path.basename(assetPath);
@@ -142,7 +143,8 @@ program.command("render [directory]").description(
142
143
  );
143
144
  const readable = new PassThrough();
144
145
  tarStream.pipe(readable);
145
- await uploadRender(getClient(), md5, readable);
146
+ const folderSize = await getFolderSize(distDir);
147
+ await uploadRender(getClient(), md5, readable, folderSize);
146
148
  process.stderr.write("Render assets uploaded\n");
147
149
  process.stderr.write(inspect(render));
148
150
  process.stderr.write("\n");
@@ -0,0 +1,7 @@
1
+ export interface APITestWebhhokResult {
2
+ message: string;
3
+ }
4
+ export declare const testWebhookURL: ({ webhookURL, topic, }: {
5
+ webhookURL: string;
6
+ topic: string;
7
+ }) => Promise<APITestWebhhokResult>;
@@ -0,0 +1,61 @@
1
+ import { program, Option } from "commander";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+ import debug from "debug";
5
+ import { input, select } from "@inquirer/prompts";
6
+ import { getClient } from "../utils/index.js";
7
+ const log = debug("ef:cli:auth");
8
+ const topics = [
9
+ "render.created",
10
+ "render.rendering",
11
+ "render.pending",
12
+ "render.failed",
13
+ "render.completed"
14
+ ];
15
+ const testWebhookURL = async ({
16
+ webhookURL,
17
+ topic
18
+ }) => {
19
+ const response = await getClient().authenticatedFetch(
20
+ "/api/v1/test_webhook",
21
+ {
22
+ method: "POST",
23
+ body: JSON.stringify({
24
+ webhookURL,
25
+ topic
26
+ })
27
+ }
28
+ );
29
+ return response.json();
30
+ };
31
+ const webhookCommand = program.command("webhook").description("Test webhook URL with a topic").option("-u, --webhookURL <webhookURL>", "Webhook URL").addOption(new Option("-t, --topic <topic>", "Topic").choices(topics)).action(async () => {
32
+ const options = webhookCommand.opts();
33
+ log("Options:", options);
34
+ let { webhookURL, topic } = options;
35
+ if (!webhookURL) {
36
+ const answer = await input({ message: "Enter a webhook URL:" });
37
+ webhookURL = answer;
38
+ }
39
+ if (!topic) {
40
+ const answer = await select({
41
+ message: "Select a topic:",
42
+ choices: [...topics.map((topic2) => ({ title: topic2, value: topic2 }))]
43
+ });
44
+ topic = answer;
45
+ }
46
+ const spinner = ora("Testing...").start();
47
+ try {
48
+ const apiData = await testWebhookURL({ webhookURL, topic });
49
+ spinner.succeed("Webhook URL is working! šŸŽ‰");
50
+ process.stderr.write(chalk.green(`${apiData.message}
51
+ `));
52
+ } catch (error) {
53
+ spinner.fail("Webhook URL is not working!");
54
+ process.stderr.write(error?.message);
55
+ process.stderr.write("\n");
56
+ log("Error:", error);
57
+ }
58
+ });
59
+ export {
60
+ testWebhookURL
61
+ };
package/dist/index.d.ts CHANGED
@@ -1 +0,0 @@
1
-
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import "./commands/preview.js";
9
9
  import "./commands/process.js";
10
10
  import "./commands/process-file.js";
11
11
  import "./commands/check.js";
12
+ import "./commands/webhook.js";
12
13
  program.name("editframe").addOption(new Option("-t, --token <token>", "API Token").env("EF_TOKEN")).addOption(
13
14
  new Option("--ef-host <host>", "Editframe Host").env("EF_HOST").default("https://editframe.dev")
14
15
  ).addOption(
@@ -1,3 +1,2 @@
1
1
  import { getRenderInfo } from './getRenderInfo.ts';
2
-
3
2
  export declare const processRenderInfo: (renderInfo: Awaited<ReturnType<typeof getRenderInfo>>) => Promise<void>;
@@ -2,7 +2,7 @@ import { Probe } from "@editframe/assets";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createCaptionFile, uploadCaptionFile, createISOBMFFFile, uploadFragmentIndex, createISOBMFFTrack, uploadISOBMFFTrack, createImageFile, uploadImageFile } from "@editframe/api";
5
- import { createReadStream } from "node:fs";
5
+ import { createReadStream, statSync } from "node:fs";
6
6
  import { getClient } from "../utils/index.js";
7
7
  const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
8
8
  const trackMatch = /\.track-[\d]+.mp4$/i;
@@ -49,13 +49,27 @@ const syncAssetDirectory = async (projectDirectory) => {
49
49
  const assets = await fs.readdir(fullPath);
50
50
  process.stderr.write(`Syncing asset dir: ${fullPath}
51
51
  `);
52
- for (const asset of assets) {
53
- process.stderr.write(`Syncing asset: ${asset}
52
+ const errors = {};
53
+ const reportError = (path2, message) => {
54
+ errors[path2] ||= [];
55
+ errors[path2].push(message);
56
+ process.stderr.write(` 🚫 ${message}
57
+ `);
58
+ };
59
+ const reportSuccess = (_path, message) => {
60
+ process.stderr.write(` āœ… ${message}
54
61
  `);
62
+ };
63
+ const reportInfo = (_path, message) => {
64
+ process.stderr.write(` ${message}
65
+ `);
66
+ };
67
+ for (const asset of assets) {
68
+ reportInfo(asset, `Syncing asset: ${asset}`);
55
69
  const assetDir = path.join(fullPath, asset);
56
70
  const stat2 = await fs.stat(assetDir);
57
71
  if (!stat2.isDirectory()) {
58
- process.stderr.write("Invalid asset. Did not find asset directory.\n");
72
+ reportError(asset, "Invalid asset. Did not find asset directory.");
59
73
  return;
60
74
  }
61
75
  const subAssets = await fs.readdir(assetDir);
@@ -66,9 +80,9 @@ const syncAssetDirectory = async (projectDirectory) => {
66
80
  const subAssetPath = path.join(assetDir, subAsset);
67
81
  const syncStatus = new SyncStatus(subAssetPath);
68
82
  if (await syncStatus.isSynced()) {
69
- process.stderr.write(
70
- ` āœ” Sub-asset has already been synced: ${subAsset}
71
- `
83
+ reportInfo(
84
+ subAsset,
85
+ `āœ” Sub-asset has already been synced: ${subAsset}`
72
86
  );
73
87
  continue;
74
88
  }
@@ -76,46 +90,50 @@ const syncAssetDirectory = async (projectDirectory) => {
76
90
  case imageMatch.test(subAsset): {
77
91
  const probeResult = await Probe.probePath(subAssetPath);
78
92
  const [videoProbe] = probeResult.videoStreams;
93
+ const { format } = probeResult;
79
94
  if (!videoProbe) {
80
- process.stderr.write(
81
- `🚫 No video stream found in image: ${subAsset}
82
- `
83
- );
95
+ reportError(subAsset, `No media info found in image: ${subAsset}`);
84
96
  break;
85
97
  }
86
98
  const ext = path.extname(subAsset).slice(1);
87
99
  if (!(ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "webp")) {
88
- process.stderr.write(`🚫 Invalid image format: ${subAsset}
89
- `);
100
+ reportError(subAsset, `Invalid image format: ${subAsset}`);
90
101
  break;
91
102
  }
92
- process.stderr.write(`šŸ–¼ļø Syncing image: ${subAsset}
93
- `);
103
+ reportInfo(subAsset, `šŸ–¼ļø Syncing image: ${subAsset}`);
94
104
  const created = await createImageFile(getClient(), {
95
105
  id: asset,
96
106
  filename: subAsset,
97
107
  width: videoProbe.width,
98
108
  height: videoProbe.height,
99
- mime_type: `image/${ext}`
109
+ mime_type: `image/${ext}`,
110
+ byte_size: (await fs.stat(subAssetPath)).size
100
111
  });
101
112
  if (created) {
102
113
  if (created.complete) {
103
- process.stderr.write(" āœ” Image has already been synced.\n");
114
+ reportInfo(subAsset, " āœ” Image has already been synced");
115
+ await syncStatus.markSynced();
104
116
  } else {
105
117
  await uploadImageFile(
106
118
  getClient(),
107
119
  created.id,
108
- createReadStream(subAssetPath)
109
- );
110
- process.stderr.write(" āœ… Image has been synced.\n");
120
+ createReadStream(subAssetPath),
121
+ Number.parseInt(format.size || "0")
122
+ ).then(() => {
123
+ reportSuccess(subAsset, "Image has been synced.");
124
+ return syncStatus.markSynced();
125
+ }).catch((error) => {
126
+ reportError(
127
+ subAsset,
128
+ `Error uploading image: ${error.message}`
129
+ );
130
+ });
111
131
  }
112
- await syncStatus.markSynced();
113
132
  }
114
133
  break;
115
134
  }
116
135
  case trackMatch.test(subAsset): {
117
- process.stderr.write(`šŸ“¼ Syncing a/v track: ${subAsset}
118
- `);
136
+ reportInfo(subAsset, `šŸ“¼ Syncing a/v track: ${subAsset}`);
119
137
  const createdFile = await createISOBMFFFile(getClient(), {
120
138
  id: asset,
121
139
  filename: subAsset.replace(/\.track-[\d]+.mp4$/, "")
@@ -124,25 +142,19 @@ const syncAssetDirectory = async (projectDirectory) => {
124
142
  const probe = await Probe.probePath(subAssetPath);
125
143
  const trackId = subAsset.match(/track-([\d]+).mp4/)?.[1];
126
144
  if (!trackId) {
127
- process.stderr.write(
128
- `🚫 No track ID found for track: ${subAsset}
129
- `
130
- );
145
+ reportError(subAsset, `No track ID found for track: ${subAsset}`);
131
146
  break;
132
147
  }
133
148
  const [track] = probe.streams;
134
149
  if (!track) {
135
- process.stderr.write(
136
- `🚫 No track stream found in track: ${subAsset}
137
- `
150
+ reportError(
151
+ subAsset,
152
+ `No track stream found in track: ${subAsset}`
138
153
  );
139
154
  break;
140
155
  }
141
156
  if (track.duration === void 0) {
142
- process.stderr.write(
143
- `🚫 No duration found in track: ${subAsset}
144
- `
145
- );
157
+ reportError(subAsset, `No duration found in track: ${subAsset}`);
146
158
  break;
147
159
  }
148
160
  const stat3 = await fs.stat(subAssetPath);
@@ -168,82 +180,122 @@ const syncAssetDirectory = async (projectDirectory) => {
168
180
  createPayload
169
181
  );
170
182
  if (createdTrack) {
171
- if (createdTrack.last_received_byte === createdTrack.byte_size - 1) {
172
- process.stderr.write(" āœ” Track has already been synced.\n");
183
+ if (createdTrack.next_byte === createdTrack.byte_size) {
184
+ reportInfo(subAsset, "āœ” Track has already been synced.");
185
+ await syncStatus.markSynced();
173
186
  } else {
174
187
  await uploadISOBMFFTrack(
175
188
  getClient(),
176
189
  createdFile.id,
177
190
  Number(trackId),
178
- createReadStream(subAssetPath)
179
- );
180
- process.stderr.write(" āœ… Track has been synced.\n");
191
+ createReadStream(subAssetPath),
192
+ createdTrack.byte_size
193
+ ).then(() => {
194
+ reportSuccess(subAsset, "Track has been synced.");
195
+ return syncStatus.markSynced();
196
+ }).catch((error) => {
197
+ reportError(
198
+ subAsset,
199
+ `Error uploading track: ${error.message}`
200
+ );
201
+ });
181
202
  }
182
- await syncStatus.markSynced();
183
203
  }
184
204
  }
185
205
  break;
186
206
  }
187
207
  case fragmentIndexMatch.test(subAsset): {
188
- process.stderr.write(`šŸ“‹ Syncing fragment index: ${subAsset}
189
- `);
208
+ reportInfo(subAsset, `šŸ“‹ Syncing fragment index: ${subAsset}`);
190
209
  const createdFile = await createISOBMFFFile(getClient(), {
191
210
  id: asset,
192
211
  filename: subAsset.replace(/\.tracks.json$/, "")
193
212
  });
194
213
  if (createdFile) {
195
214
  if (createdFile.fragment_index_complete) {
196
- process.stderr.write(
197
- " āœ” Fragment index has already been synced.\n"
198
- );
215
+ reportInfo(subAsset, "āœ” Fragment index has already been synced.");
216
+ await syncStatus.markSynced();
199
217
  } else {
218
+ const stats = statSync(subAssetPath);
200
219
  const readStream = createReadStream(subAssetPath);
201
- await uploadFragmentIndex(getClient(), asset, readStream);
202
- process.stderr.write(" āœ… Fragment index has been synced.\n");
220
+ await uploadFragmentIndex(
221
+ getClient(),
222
+ asset,
223
+ readStream,
224
+ stats.size
225
+ ).then(() => {
226
+ reportSuccess(subAsset, "Fragment index has been synced.");
227
+ return syncStatus.markSynced();
228
+ }).catch((error) => {
229
+ reportError(
230
+ subAsset,
231
+ `Error uploading fragment index: ${error.message}`
232
+ );
233
+ });
203
234
  }
204
- await syncStatus.markSynced();
205
235
  } else {
206
- process.stderr.write(
207
- `🚫 No file found for fragment index: ${subAsset}
208
- `
236
+ reportError(
237
+ subAsset,
238
+ `No file found for fragment index: ${subAsset}`
209
239
  );
210
240
  break;
211
241
  }
212
242
  break;
213
243
  }
214
244
  case captionsMatch.test(subAsset): {
215
- process.stderr.write(`šŸ“ Syncing captions: ${subAsset}
216
- `);
245
+ reportInfo(subAsset, `šŸ“ Syncing captions: ${subAsset}`);
217
246
  const createdFile = await createCaptionFile(getClient(), {
218
247
  id: asset,
219
- filename: subAsset.replace(/\.captions.json$/, "")
248
+ filename: subAsset.replace(/\.captions.json$/, ""),
249
+ byte_size: (await fs.stat(subAsset)).size
220
250
  });
221
251
  if (createdFile) {
222
252
  if (createdFile.complete) {
223
- process.stderr.write(" āœ” Captions have already been synced.\n");
253
+ reportInfo(subAsset, "āœ” Captions have already been synced.");
254
+ await syncStatus.markSynced();
224
255
  } else {
225
256
  const readStream = createReadStream(subAssetPath);
226
- await uploadCaptionFile(getClient(), asset, readStream);
227
- process.stderr.write(" āœ… Captions have been synced.\n");
257
+ const stats = statSync(subAssetPath);
258
+ await uploadCaptionFile(
259
+ getClient(),
260
+ asset,
261
+ readStream,
262
+ stats.size
263
+ ).then(() => {
264
+ reportSuccess(subAsset, "Captions have been synced.");
265
+ return syncStatus.markSynced();
266
+ }).catch((error) => {
267
+ reportError(
268
+ subAsset,
269
+ `Error uploading captions: ${error.message}`
270
+ );
271
+ });
228
272
  }
229
- await syncStatus.markSynced();
230
273
  } else {
231
- process.stderr.write(
232
- `🚫 No file found for captions: ${subAsset}
233
- `
234
- );
274
+ reportError(subAsset, `No file found for captions: ${subAsset}`);
235
275
  break;
236
276
  }
237
277
  break;
238
278
  }
239
279
  default: {
240
- process.stderr.write(`🚫 Unknown sub-asset: ${subAsset}
241
- `);
280
+ reportError(subAsset, `Unknown sub-asset: ${subAsset}`);
242
281
  break;
243
282
  }
244
283
  }
245
284
  }
246
285
  }
286
+ if (Object.keys(errors).length) {
287
+ process.stderr.write("\n\nāŒ Encountered errors while syncing assets:\n");
288
+ for (const [asset, messages] of Object.entries(errors)) {
289
+ process.stderr.write(`
290
+ 🚫 ${asset}
291
+ `);
292
+ for (const message of messages) {
293
+ process.stderr.write(` - ${message}
294
+ `);
295
+ }
296
+ }
297
+ throw new Error("Failed to sync assets");
298
+ }
247
299
  };
248
300
  export {
249
301
  syncAssetDirectory
@@ -1,3 +1,2 @@
1
1
  import { Page } from 'playwright';
2
-
3
2
  export declare const attachWorkbench: (page: Page) => void;
@@ -0,0 +1 @@
1
+ export declare const getFolderSize: (folderPath: string) => Promise<number>;
@@ -0,0 +1,22 @@
1
+ import { promises } from "node:fs";
2
+ import path from "node:path";
3
+ const getFolderSize = async (folderPath) => {
4
+ let totalSize = 0;
5
+ async function calculateSize(dir) {
6
+ const files = await promises.readdir(dir);
7
+ for (const file of files) {
8
+ const filePath = path.join(dir, file);
9
+ const stats = await promises.stat(filePath);
10
+ if (stats.isDirectory()) {
11
+ await calculateSize(filePath);
12
+ } else {
13
+ totalSize += stats.size;
14
+ }
15
+ }
16
+ }
17
+ await calculateSize(folderPath);
18
+ return totalSize;
19
+ };
20
+ export {
21
+ getFolderSize
22
+ };
@@ -1,3 +1,2 @@
1
1
  import { Client } from '../../../api/src';
2
-
3
2
  export declare const getClient: () => Client;
@@ -1,5 +1,4 @@
1
1
  import { Page } from 'playwright';
2
-
3
2
  interface LaunchOptions {
4
3
  url: string;
5
4
  headless?: boolean;
@@ -1,5 +1,4 @@
1
1
  import { ViteDevServer } from 'vite';
2
-
3
2
  export declare class DevServer {
4
3
  private devServer;
5
4
  static start(directory: string): Promise<DevServer>;
@@ -1,5 +1,4 @@
1
1
  import { ViteDevServer } from 'vite';
2
-
3
2
  export declare class PreviewServer {
4
3
  private previewServer;
5
4
  static start(directory: string): Promise<PreviewServer>;
@@ -3,7 +3,7 @@ type VideoPayload = {
3
3
  height: number;
4
4
  };
5
5
  export declare const validateVideoResolution: (rawPayload: VideoPayload) => Promise<{
6
- width: number;
7
6
  height: number;
7
+ width: number;
8
8
  }>;
9
9
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/cli",
3
- "version": "0.8.0-beta.1",
3
+ "version": "0.8.0-beta.10",
4
4
  "description": "Command line interface for EditFrame",
5
5
  "bin": {
6
6
  "editframe": "./dist/index.js"
@@ -19,14 +19,15 @@
19
19
  "@types/promptly": "^3.0.5",
20
20
  "@types/tar": "^6.1.13",
21
21
  "typescript": "^5.5.4",
22
- "vite-plugin-dts": "^3.9.1",
22
+ "vite-plugin-dts": "^4.0.3",
23
23
  "vite-tsconfig-paths": "^4.3.2"
24
24
  },
25
25
  "dependencies": {
26
- "@editframe/api": "0.8.0-beta.1",
27
- "@editframe/assets": "0.8.0-beta.1",
28
- "@editframe/elements": "0.8.0-beta.1",
29
- "@editframe/vite-plugin": "0.8.0-beta.1",
26
+ "@editframe/api": "0.8.0-beta.10",
27
+ "@editframe/assets": "0.8.0-beta.10",
28
+ "@editframe/elements": "0.8.0-beta.10",
29
+ "@editframe/vite-plugin": "0.8.0-beta.10",
30
+ "@inquirer/prompts": "^5.3.8",
30
31
  "axios": "^1.6.8",
31
32
  "chalk": "^5.3.0",
32
33
  "commander": "^12.0.0",
@@ -1,11 +1,12 @@
1
1
  import { basename } from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { createReadStream } from "node:fs";
2
4
 
3
5
  import { program } from "commander";
4
6
 
5
7
  import { withSpinner } from "../utils/withSpinner.ts";
6
8
 
7
9
  import { getClient } from "../utils/index.ts";
8
- import { createReadStream } from "node:fs";
9
10
  import {
10
11
  createUnprocessedFile,
11
12
  updateUnprocessedFile,
@@ -21,18 +22,21 @@ program
21
22
 
22
23
  const fileId = await md5FilePath(path);
23
24
 
25
+ const byte_size = (await stat(path)).size;
26
+
24
27
  await withSpinner("Creating unprocessed file record", async () => {
25
28
  await createUnprocessedFile(client, {
26
29
  id: fileId,
27
30
  processes: [],
28
31
  filename: basename(path),
32
+ byte_size,
29
33
  });
30
34
  });
31
35
 
32
36
  const readStream = createReadStream(path);
33
37
 
34
38
  await withSpinner("Uploading file", async () => {
35
- await uploadUnprocessedFile(client, fileId, readStream);
39
+ await uploadUnprocessedFile(client, fileId, readStream, byte_size);
36
40
  });
37
41
 
38
42
  const unprocessedFile = await withSpinner(
@@ -19,6 +19,7 @@ import { getClient } from "../utils/index.ts";
19
19
  import { getRenderInfo } from "../operations/getRenderInfo.ts";
20
20
  import { processRenderInfo } from "../operations/processRenderInfo.ts";
21
21
  import { validateVideoResolution } from "../utils/validateVideoResolution.ts";
22
+ import { getFolderSize } from "../utils/getFolderSize.ts";
22
23
 
23
24
  interface StrategyBuilder {
24
25
  buildProductionUrl: (tagName: string, assetPath: string) => Promise<string>;
@@ -179,7 +180,8 @@ program
179
180
  );
180
181
  const readable = new PassThrough();
181
182
  tarStream.pipe(readable);
182
- await uploadRender(getClient(), md5, readable);
183
+ const folderSize = await getFolderSize(distDir);
184
+ await uploadRender(getClient(), md5, readable, folderSize);
183
185
  process.stderr.write("Render assets uploaded\n");
184
186
  process.stderr.write(inspect(render));
185
187
  process.stderr.write("\n");
@@ -0,0 +1,76 @@
1
+ import { program, Option } from "commander";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+ import debug from "debug";
5
+ import { input, select } from "@inquirer/prompts";
6
+
7
+ import { getClient } from "../utils/index.ts";
8
+
9
+ const log = debug("ef:cli:auth");
10
+
11
+ export interface APITestWebhhokResult {
12
+ message: string;
13
+ }
14
+ const topics = [
15
+ "render.created",
16
+ "render.rendering",
17
+ "render.pending",
18
+ "render.failed",
19
+ "render.completed",
20
+ ];
21
+
22
+ export const testWebhookURL = async ({
23
+ webhookURL,
24
+ topic,
25
+ }: {
26
+ webhookURL: string;
27
+ topic: string;
28
+ }) => {
29
+ const response = await getClient().authenticatedFetch(
30
+ "/api/v1/test_webhook",
31
+ {
32
+ method: "POST",
33
+ body: JSON.stringify({
34
+ webhookURL,
35
+ topic,
36
+ }),
37
+ },
38
+ );
39
+ return response.json() as Promise<APITestWebhhokResult>;
40
+ };
41
+
42
+ const webhookCommand = program
43
+ .command("webhook")
44
+ .description("Test webhook URL with a topic")
45
+ .option("-u, --webhookURL <webhookURL>", "Webhook URL")
46
+ .addOption(new Option("-t, --topic <topic>", "Topic").choices(topics))
47
+ .action(async () => {
48
+ const options = webhookCommand.opts();
49
+ log("Options:", options);
50
+ let { webhookURL, topic } = options;
51
+
52
+ if (!webhookURL) {
53
+ const answer = await input({ message: "Enter a webhook URL:" });
54
+ webhookURL = answer;
55
+ }
56
+
57
+ if (!topic) {
58
+ const answer = await select({
59
+ message: "Select a topic:",
60
+ choices: [...topics.map((topic) => ({ title: topic, value: topic }))],
61
+ });
62
+ topic = answer;
63
+ }
64
+
65
+ const spinner = ora("Testing...").start();
66
+ try {
67
+ const apiData = await testWebhookURL({ webhookURL, topic });
68
+ spinner.succeed("Webhook URL is working! šŸŽ‰");
69
+ process.stderr.write(chalk.green(`${apiData.message}\n`));
70
+ } catch (error: any) {
71
+ spinner.fail("Webhook URL is not working!");
72
+ process.stderr.write(error?.message);
73
+ process.stderr.write("\n");
74
+ log("Error:", error);
75
+ }
76
+ });
@@ -12,6 +12,7 @@ import {
12
12
  uploadISOBMFFTrack,
13
13
  type CreateISOBMFFTrackPayload,
14
14
  } from "@editframe/api";
15
+ import { statSync } from "node:fs";
15
16
 
16
17
  import { createReadStream } from "node:fs";
17
18
  import type { z } from "zod";
@@ -62,6 +63,7 @@ export const syncAssetDirectory = async (
62
63
  }
63
64
  throw error;
64
65
  });
66
+
65
67
  if (!stat?.isDirectory()) {
66
68
  console.error(`No assets cache directory found at ${fullPath}`);
67
69
  return;
@@ -70,12 +72,28 @@ export const syncAssetDirectory = async (
70
72
 
71
73
  process.stderr.write(`Syncing asset dir: ${fullPath}\n`);
72
74
 
75
+ const errors: Record<string, string[]> = {};
76
+
77
+ const reportError = (path: string, message: string) => {
78
+ errors[path] ||= [];
79
+ errors[path].push(message);
80
+ process.stderr.write(` 🚫 ${message}\n`);
81
+ };
82
+
83
+ const reportSuccess = (_path: string, message: string) => {
84
+ process.stderr.write(` āœ… ${message}\n`);
85
+ };
86
+
87
+ const reportInfo = (_path: string, message: string) => {
88
+ process.stderr.write(` ${message}\n`);
89
+ };
90
+
73
91
  for (const asset of assets) {
74
- process.stderr.write(`Syncing asset: ${asset}\n`);
92
+ reportInfo(asset, `Syncing asset: ${asset}`);
75
93
  const assetDir = path.join(fullPath, asset);
76
94
  const stat = await fs.stat(assetDir);
77
95
  if (!stat.isDirectory()) {
78
- process.stderr.write("Invalid asset. Did not find asset directory.\n");
96
+ reportError(asset, "Invalid asset. Did not find asset directory.");
79
97
  return;
80
98
  }
81
99
  const subAssets = await fs.readdir(assetDir);
@@ -88,8 +106,9 @@ export const syncAssetDirectory = async (
88
106
  const subAssetPath = path.join(assetDir, subAsset);
89
107
  const syncStatus = new SyncStatus(subAssetPath);
90
108
  if (await syncStatus.isSynced()) {
91
- process.stderr.write(
92
- ` āœ” Sub-asset has already been synced: ${subAsset}\n`,
109
+ reportInfo(
110
+ subAsset,
111
+ `āœ” Sub-asset has already been synced: ${subAsset}`,
93
112
  );
94
113
  continue;
95
114
  }
@@ -97,10 +116,9 @@ export const syncAssetDirectory = async (
97
116
  case imageMatch.test(subAsset): {
98
117
  const probeResult = await Probe.probePath(subAssetPath);
99
118
  const [videoProbe] = probeResult.videoStreams;
119
+ const { format } = probeResult;
100
120
  if (!videoProbe) {
101
- process.stderr.write(
102
- `🚫 No video stream found in image: ${subAsset}\n`,
103
- );
121
+ reportError(subAsset, `No media info found in image: ${subAsset}`);
104
122
  break;
105
123
  }
106
124
  const ext = path.extname(subAsset).slice(1);
@@ -112,34 +130,45 @@ export const syncAssetDirectory = async (
112
130
  ext === "webp"
113
131
  )
114
132
  ) {
115
- process.stderr.write(`🚫 Invalid image format: ${subAsset}\n`);
133
+ reportError(subAsset, `Invalid image format: ${subAsset}`);
116
134
  break;
117
135
  }
118
- process.stderr.write(`šŸ–¼ļø Syncing image: ${subAsset}\n`);
136
+ reportInfo(subAsset, `šŸ–¼ļø Syncing image: ${subAsset}`);
119
137
  const created = await createImageFile(getClient(), {
120
138
  id: asset,
121
139
  filename: subAsset,
122
140
  width: videoProbe.width,
123
141
  height: videoProbe.height,
124
142
  mime_type: `image/${ext}`,
143
+ byte_size: (await fs.stat(subAssetPath)).size,
125
144
  });
126
145
  if (created) {
127
146
  if (created.complete) {
128
- process.stderr.write(" āœ” Image has already been synced.\n");
147
+ reportInfo(subAsset, " āœ” Image has already been synced");
148
+ await syncStatus.markSynced();
129
149
  } else {
130
150
  await uploadImageFile(
131
151
  getClient(),
132
152
  created.id,
133
153
  createReadStream(subAssetPath),
134
- );
135
- process.stderr.write(" āœ… Image has been synced.\n");
154
+ Number.parseInt(format.size || "0"),
155
+ )
156
+ .then(() => {
157
+ reportSuccess(subAsset, "Image has been synced.");
158
+ return syncStatus.markSynced();
159
+ })
160
+ .catch((error) => {
161
+ reportError(
162
+ subAsset,
163
+ `Error uploading image: ${error.message}`,
164
+ );
165
+ });
136
166
  }
137
- await syncStatus.markSynced();
138
167
  }
139
168
  break;
140
169
  }
141
170
  case trackMatch.test(subAsset): {
142
- process.stderr.write(`šŸ“¼ Syncing a/v track: ${subAsset}\n`);
171
+ reportInfo(subAsset, `šŸ“¼ Syncing a/v track: ${subAsset}`);
143
172
  const createdFile = await createISOBMFFFile(getClient(), {
144
173
  id: asset,
145
174
  filename: subAsset.replace(/\.track-[\d]+.mp4$/, ""),
@@ -148,29 +177,26 @@ export const syncAssetDirectory = async (
148
177
  const probe = await Probe.probePath(subAssetPath);
149
178
  const trackId = subAsset.match(/track-([\d]+).mp4/)?.[1];
150
179
  if (!trackId) {
151
- process.stderr.write(
152
- `🚫 No track ID found for track: ${subAsset}\n`,
153
- );
180
+ reportError(subAsset, `No track ID found for track: ${subAsset}`);
154
181
  break;
155
182
  }
156
183
  const [track] = probe.streams;
157
184
  if (!track) {
158
- process.stderr.write(
159
- `🚫 No track stream found in track: ${subAsset}\n`,
185
+ reportError(
186
+ subAsset,
187
+ `No track stream found in track: ${subAsset}`,
160
188
  );
161
189
  break;
162
190
  }
163
191
  if (track.duration === undefined) {
164
- process.stderr.write(
165
- `🚫 No duration found in track: ${subAsset}\n`,
166
- );
192
+ reportError(subAsset, `No duration found in track: ${subAsset}`);
167
193
  break;
168
194
  }
169
195
 
170
196
  const stat = await fs.stat(subAssetPath);
171
197
 
172
198
  /**
173
- * Because the payload is a discriminated union, we nede to create these objects
199
+ * Because the payload is a discriminated union, we need to create these objects
174
200
  * only after the value for type has been narrowed, otherwise the object will
175
201
  * have type: "audio" | "video" which doesnt' match the payload type that
176
202
  * looks more like { type: "audio", ... } | { type: "video", ... }
@@ -201,79 +227,126 @@ export const syncAssetDirectory = async (
201
227
  );
202
228
 
203
229
  if (createdTrack) {
204
- if (
205
- createdTrack.last_received_byte ===
206
- createdTrack.byte_size - 1
207
- ) {
208
- process.stderr.write(" āœ” Track has already been synced.\n");
230
+ if (createdTrack.next_byte === createdTrack.byte_size) {
231
+ reportInfo(subAsset, "āœ” Track has already been synced.");
232
+ await syncStatus.markSynced();
209
233
  } else {
210
234
  await uploadISOBMFFTrack(
211
235
  getClient(),
212
236
  createdFile.id,
213
237
  Number(trackId),
214
238
  createReadStream(subAssetPath),
215
- );
216
- process.stderr.write(" āœ… Track has been synced.\n");
239
+ createdTrack.byte_size,
240
+ )
241
+ .then(() => {
242
+ reportSuccess(subAsset, "Track has been synced.");
243
+ return syncStatus.markSynced();
244
+ })
245
+ .catch((error) => {
246
+ reportError(
247
+ subAsset,
248
+ `Error uploading track: ${error.message}`,
249
+ );
250
+ });
217
251
  }
218
- await syncStatus.markSynced();
219
252
  }
220
253
  }
221
254
 
222
255
  break;
223
256
  }
224
257
  case fragmentIndexMatch.test(subAsset): {
225
- process.stderr.write(`šŸ“‹ Syncing fragment index: ${subAsset}\n`);
258
+ reportInfo(subAsset, `šŸ“‹ Syncing fragment index: ${subAsset}`);
226
259
  const createdFile = await createISOBMFFFile(getClient(), {
227
260
  id: asset,
228
261
  filename: subAsset.replace(/\.tracks.json$/, ""),
229
262
  });
230
263
  if (createdFile) {
231
264
  if (createdFile.fragment_index_complete) {
232
- process.stderr.write(
233
- " āœ” Fragment index has already been synced.\n",
234
- );
265
+ reportInfo(subAsset, "āœ” Fragment index has already been synced.");
266
+ await syncStatus.markSynced();
235
267
  } else {
268
+ const stats = statSync(subAssetPath);
236
269
  const readStream = createReadStream(subAssetPath);
237
- await uploadFragmentIndex(getClient(), asset, readStream);
238
- process.stderr.write(" āœ… Fragment index has been synced.\n");
270
+ await uploadFragmentIndex(
271
+ getClient(),
272
+ asset,
273
+ readStream,
274
+ stats.size,
275
+ )
276
+ .then(() => {
277
+ reportSuccess(subAsset, "Fragment index has been synced.");
278
+ return syncStatus.markSynced();
279
+ })
280
+ .catch((error) => {
281
+ reportError(
282
+ subAsset,
283
+ `Error uploading fragment index: ${error.message}`,
284
+ );
285
+ });
239
286
  }
240
- await syncStatus.markSynced();
241
287
  } else {
242
- process.stderr.write(
243
- `🚫 No file found for fragment index: ${subAsset}\n`,
288
+ reportError(
289
+ subAsset,
290
+ `No file found for fragment index: ${subAsset}`,
244
291
  );
245
292
  break;
246
293
  }
247
294
  break;
248
295
  }
249
296
  case captionsMatch.test(subAsset): {
250
- process.stderr.write(`šŸ“ Syncing captions: ${subAsset}\n`);
297
+ reportInfo(subAsset, `šŸ“ Syncing captions: ${subAsset}`);
251
298
  const createdFile = await createCaptionFile(getClient(), {
252
299
  id: asset,
253
300
  filename: subAsset.replace(/\.captions.json$/, ""),
301
+ byte_size: (await fs.stat(subAsset)).size,
254
302
  });
255
303
  if (createdFile) {
256
304
  if (createdFile.complete) {
257
- process.stderr.write(" āœ” Captions have already been synced.\n");
305
+ reportInfo(subAsset, "āœ” Captions have already been synced.");
306
+ await syncStatus.markSynced();
258
307
  } else {
259
308
  const readStream = createReadStream(subAssetPath);
260
- await uploadCaptionFile(getClient(), asset, readStream);
261
- process.stderr.write(" āœ… Captions have been synced.\n");
309
+ const stats = statSync(subAssetPath);
310
+ await uploadCaptionFile(
311
+ getClient(),
312
+ asset,
313
+ readStream,
314
+ stats.size,
315
+ )
316
+ .then(() => {
317
+ reportSuccess(subAsset, "Captions have been synced.");
318
+ return syncStatus.markSynced();
319
+ })
320
+ .catch((error) => {
321
+ reportError(
322
+ subAsset,
323
+ `Error uploading captions: ${error.message}`,
324
+ );
325
+ });
262
326
  }
263
- await syncStatus.markSynced();
264
327
  } else {
265
- process.stderr.write(
266
- `🚫 No file found for captions: ${subAsset}\n`,
267
- );
328
+ reportError(subAsset, `No file found for captions: ${subAsset}`);
268
329
  break;
269
330
  }
270
331
  break;
271
332
  }
272
333
  default: {
273
- process.stderr.write(`🚫 Unknown sub-asset: ${subAsset}\n`);
334
+ reportError(subAsset, `Unknown sub-asset: ${subAsset}`);
274
335
  break;
275
336
  }
276
337
  }
277
338
  }
278
339
  }
340
+
341
+ if (Object.keys(errors).length) {
342
+ process.stderr.write("\n\nāŒ Encountered errors while syncing assets:\n");
343
+ for (const [asset, messages] of Object.entries(errors)) {
344
+ process.stderr.write(`\n🚫 ${asset}\n`);
345
+ for (const message of messages) {
346
+ process.stderr.write(` - ${message}\n`);
347
+ }
348
+ }
349
+
350
+ throw new Error("Failed to sync assets");
351
+ }
279
352
  };
@@ -0,0 +1,24 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const getFolderSize = async (folderPath: string): Promise<number> => {
5
+ let totalSize = 0;
6
+
7
+ async function calculateSize(dir: string): Promise<void> {
8
+ const files = await fs.readdir(dir);
9
+
10
+ for (const file of files) {
11
+ const filePath = path.join(dir, file);
12
+ const stats = await fs.stat(filePath);
13
+
14
+ if (stats.isDirectory()) {
15
+ await calculateSize(filePath);
16
+ } else {
17
+ totalSize += stats.size;
18
+ }
19
+ }
20
+ }
21
+
22
+ await calculateSize(folderPath);
23
+ return totalSize;
24
+ };