@editframe/cli 0.11.0-beta.8 → 0.12.0-beta.1

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 (37) hide show
  1. package/dist/VERSION.d.ts +1 -1
  2. package/dist/VERSION.js +1 -1
  3. package/dist/commands/process-file.js +32 -21
  4. package/dist/commands/render.js +8 -2
  5. package/dist/operations/syncAssetsDirectory/SubAssetSync.js +3 -0
  6. package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +2 -2
  7. package/dist/operations/syncAssetsDirectory/SyncCaption.js +19 -8
  8. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +2 -2
  9. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +19 -12
  10. package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +2 -2
  11. package/dist/operations/syncAssetsDirectory/SyncImage.js +16 -11
  12. package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +7 -11
  13. package/dist/operations/syncAssetsDirectory/SyncStatus.js +1 -1
  14. package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +2 -2
  15. package/dist/operations/syncAssetsDirectory/SyncTrack.js +14 -12
  16. package/dist/utils/createReadableStreamFromReadable.d.ts +4 -0
  17. package/dist/utils/createReadableStreamFromReadable.js +82 -0
  18. package/dist/utils/launchBrowserAndWaitForSDK.js +2 -2
  19. package/package.json +5 -5
  20. package/src/commands/process-file.ts +36 -22
  21. package/src/commands/render.ts +9 -2
  22. package/src/operations/syncAssetsDirectory/SubAssetSync.ts +4 -0
  23. package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +29 -3
  24. package/src/operations/syncAssetsDirectory/SyncCaption.ts +22 -9
  25. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +30 -4
  26. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +22 -13
  27. package/src/operations/syncAssetsDirectory/SyncImage.test.ts +27 -3
  28. package/src/operations/syncAssetsDirectory/SyncImage.ts +23 -15
  29. package/src/operations/syncAssetsDirectory/SyncStatus.ts +3 -3
  30. package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +38 -3
  31. package/src/operations/syncAssetsDirectory/SyncTrack.ts +20 -13
  32. package/src/operations/syncAssetsDirectory.test.ts +22 -1
  33. package/src/utils/createReadableStreamFromReadable.ts +117 -0
  34. package/src/utils/launchBrowserAndWaitForSDK.ts +2 -2
  35. package/src/utils/startDevServer.ts +1 -1
  36. package/test-fixtures/fixture.ts +0 -1
  37. package/test-fixtures/network.ts +72 -29
@@ -1,18 +1,22 @@
1
- import { basename } from "node:path";
2
- import { stat } from "node:fs/promises";
3
1
  import { createReadStream } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
4
 
5
5
  import { program } from "commander";
6
6
 
7
7
  import { withSpinner } from "../utils/withSpinner.ts";
8
8
 
9
- import { getClient } from "../utils/index.ts";
10
9
  import {
11
10
  createUnprocessedFile,
12
- updateUnprocessedFile,
11
+ getIsobmffProcessInfo,
12
+ getIsobmffProcessProgress,
13
+ processIsobmffFile,
13
14
  uploadUnprocessedFile,
14
15
  } from "@editframe/api";
15
16
  import { md5FilePath } from "@editframe/assets";
17
+ import ora from "ora";
18
+ import { createReadableStreamFromReadable } from "../utils/createReadableStreamFromReadable.ts";
19
+ import { getClient } from "../utils/index.ts";
16
20
 
17
21
  program
18
22
  .command("process-file <file>")
@@ -29,7 +33,6 @@ program
29
33
  async () =>
30
34
  await createUnprocessedFile(client, {
31
35
  md5,
32
- processes: [],
33
36
  filename: basename(path),
34
37
  byte_size,
35
38
  }),
@@ -37,25 +40,36 @@ program
37
40
 
38
41
  const readStream = createReadStream(path);
39
42
 
40
- await withSpinner("Uploading file", async () => {
41
- await uploadUnprocessedFile(
42
- client,
43
- unprocessedFile.id,
44
- readStream,
45
- byte_size,
46
- );
47
- });
48
-
49
- const updatedUnprocessedFile = await withSpinner(
43
+ const upload = uploadUnprocessedFile(
44
+ client,
45
+ unprocessedFile.id,
46
+ createReadableStreamFromReadable(readStream),
47
+ byte_size,
48
+ );
49
+ const uploadSpinner = ora("Uploading file");
50
+ for await (const event of upload) {
51
+ uploadSpinner.text = `Uploading file: ${(100 * event.progress).toFixed(2)}%`;
52
+ }
53
+ uploadSpinner.succeed("Upload complete");
54
+ const processor = await withSpinner(
50
55
  "Marking for processing",
51
- async () => {
52
- return await updateUnprocessedFile(client, unprocessedFile.id, {
53
- processes: ["isobmff"],
54
- });
55
- },
56
+ async () => await processIsobmffFile(client, unprocessedFile.id),
56
57
  );
57
58
 
58
- process.stderr.write("File uploaded and marked for processing.\n");
59
+ const processSpinner = ora("Waiting for processing to complete");
60
+ processSpinner.start();
61
+ const progress = await getIsobmffProcessProgress(client, processor.id);
62
+
63
+ for await (const event of progress) {
64
+ if (event.type === "progress") {
65
+ processSpinner.text = `Processing: ${(100 * event.data.progress).toFixed(2)}%`;
66
+ } else if (event.type === "complete") {
67
+ processSpinner.succeed("Processing complete");
68
+ }
69
+ }
70
+
71
+ const info = await getIsobmffProcessInfo(client, processor.id);
59
72
 
60
- console.log(JSON.stringify(updatedUnprocessedFile, null, 2));
73
+ console.log("Processed file info");
74
+ console.log(info);
61
75
  });
@@ -16,6 +16,7 @@ import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.ts";
16
16
  import { processRenderInfo } from "../operations/processRenderInfo.ts";
17
17
  import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
18
18
  import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.ts";
19
+ import { createReadableStreamFromReadable } from "../utils/createReadableStreamFromReadable.ts";
19
20
  import { getFolderSize } from "../utils/getFolderSize.ts";
20
21
  import { getClient } from "../utils/index.ts";
21
22
  import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.ts";
@@ -41,7 +42,7 @@ export const buildAssetId = async (
41
42
  throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);
42
43
  }
43
44
 
44
- return info.asset_id;
45
+ return info.id;
45
46
  };
46
47
 
47
48
  program
@@ -179,8 +180,14 @@ program
179
180
  );
180
181
  const readable = new PassThrough();
181
182
  tarStream.pipe(readable);
183
+
182
184
  const folderSize = await getFolderSize(distDir);
183
- await uploadRender(getClient(), render.id, readable, folderSize);
185
+ await uploadRender(
186
+ getClient(),
187
+ render.id,
188
+ createReadableStreamFromReadable(readable),
189
+ folderSize,
190
+ );
184
191
  process.stderr.write("Render assets uploaded\n");
185
192
  process.stderr.write(inspect(render));
186
193
  process.stderr.write("\n");
@@ -1,3 +1,4 @@
1
+ import debug from "debug";
1
2
  import type { SyncStatus } from "./SyncStatus.ts";
2
3
 
3
4
  import { SyncCaption } from "./SyncCaption.ts";
@@ -25,7 +26,10 @@ const fragmentIndexMatch = /\.tracks.json$/i;
25
26
  const captionsMatch = /\.captions.json$/i;
26
27
  const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
27
28
 
29
+ const log = debug("ef:SubAssetSync");
30
+
28
31
  export const getAssetSync = (subAssetPath: string, md5: string) => {
32
+ log("getAssetSync", { subAssetPath, md5 });
29
33
  if (imageMatch.test(subAssetPath)) {
30
34
  return new SyncImage(subAssetPath, md5);
31
35
  }
@@ -1,12 +1,13 @@
1
+ import { useMSW } from "TEST/useMSW.ts";
1
2
  import { describe, expect, test } from "vitest";
2
3
  import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
4
  import {
4
5
  mockCreateCaptionFile,
6
+ mockLookupCaptionFileByMd5,
7
+ mockLookupCaptionFileByMd5NotFound,
5
8
  mockUploadCaptionFile,
6
- useMSW,
7
9
  } from "../../../test-fixtures/network.ts";
8
10
  import { SyncCaption } from "./SyncCaption.ts";
9
-
10
11
  describe("SyncCaption", async () => {
11
12
  const server = useMSW();
12
13
  await withFixtures(
@@ -37,8 +38,25 @@ describe("SyncCaption", async () => {
37
38
  });
38
39
 
39
40
  describe(".create()", () => {
41
+ test("uses matching data when file matches md5", async () => {
42
+ server.use(
43
+ mockLookupCaptionFileByMd5({
44
+ md5: video!.md5,
45
+ fixture: video!,
46
+ }),
47
+ );
48
+ const syncCaption = new SyncCaption(
49
+ await generateCaptions(video!),
50
+ video!.md5,
51
+ );
52
+ await syncCaption.create();
53
+ expect(syncCaption.isComplete()).toBe(true);
54
+ });
40
55
  test("isComplete() returns false when not created", async () => {
41
56
  server.use(
57
+ mockLookupCaptionFileByMd5NotFound({
58
+ md5: video!.md5,
59
+ }),
42
60
  mockCreateCaptionFile({
43
61
  complete: false,
44
62
  id: "123",
@@ -56,6 +74,9 @@ describe("SyncCaption", async () => {
56
74
 
57
75
  test("isComplete() returns true when created", async () => {
58
76
  server.use(
77
+ mockLookupCaptionFileByMd5NotFound({
78
+ md5: video!.md5,
79
+ }),
59
80
  mockCreateCaptionFile({
60
81
  complete: true,
61
82
  id: "123",
@@ -83,6 +104,9 @@ describe("SyncCaption", async () => {
83
104
 
84
105
  test("uploads caption", async () => {
85
106
  server.use(
107
+ mockLookupCaptionFileByMd5NotFound({
108
+ md5: video!.md5,
109
+ }),
86
110
  mockCreateCaptionFile({
87
111
  complete: true,
88
112
  id: "123",
@@ -115,6 +139,9 @@ describe("SyncCaption", async () => {
115
139
 
116
140
  test("marks synced", async () => {
117
141
  server.use(
142
+ mockLookupCaptionFileByMd5NotFound({
143
+ md5: video!.md5,
144
+ }),
118
145
  mockCreateCaptionFile({
119
146
  complete: true,
120
147
  id: "123",
@@ -135,7 +162,6 @@ describe("SyncCaption", async () => {
135
162
  complete: true,
136
163
  id: "123",
137
164
  md5: video!.md5,
138
- asset_id: `${video!.md5}:test.mp4`,
139
165
  byte_size: 35,
140
166
  });
141
167
  });
@@ -2,20 +2,24 @@ import fs from "node:fs/promises";
2
2
 
3
3
  import {
4
4
  type CreateCaptionFileResult,
5
+ type LookupCaptionFileByMd5Result,
5
6
  createCaptionFile,
7
+ lookupCaptionFileByMd5,
6
8
  uploadCaptionFile,
7
9
  } from "@editframe/api";
8
10
 
9
11
  import { Readable } from "node:stream";
10
12
  import { getClient } from "../../utils/index.ts";
13
+
14
+ import { basename } from "node:path";
15
+ import { createReadableStreamFromReadable } from "../../utils/createReadableStreamFromReadable.ts";
11
16
  import type { SubAssetSync } from "./SubAssetSync.ts";
12
17
  import { SyncStatus } from "./SyncStatus.ts";
13
-
14
18
  export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
15
19
  icon = "📝";
16
20
  label = "captions";
17
21
  syncStatus: SyncStatus = new SyncStatus(this.path);
18
- created: CreateCaptionFileResult | null = null;
22
+ created: CreateCaptionFileResult | LookupCaptionFileByMd5Result | null = null;
19
23
  constructor(
20
24
  public path: string,
21
25
  public md5: string,
@@ -30,11 +34,19 @@ export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
30
34
  async validate() {}
31
35
 
32
36
  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
- });
37
+ const maybeCaptionFile = await lookupCaptionFileByMd5(
38
+ getClient(),
39
+ this.md5,
40
+ );
41
+ if (maybeCaptionFile) {
42
+ this.created = maybeCaptionFile;
43
+ } else {
44
+ this.created = await createCaptionFile(getClient(), {
45
+ md5: this.md5,
46
+ filename: basename(this.path).replace(/\.captions.json$/, ""),
47
+ byte_size: await this.byteSize(),
48
+ });
49
+ }
38
50
  }
39
51
 
40
52
  isComplete() {
@@ -52,7 +64,9 @@ export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
52
64
  this.created.id,
53
65
  // It's not clear why we need to use Readable.from here, but it seems
54
66
  // to fix an issue where the request is closed early in tests
55
- Readable.from(await fs.readFile(this.path)),
67
+ createReadableStreamFromReadable(
68
+ Readable.from(await fs.readFile(this.path)),
69
+ ),
56
70
  await this.byteSize(),
57
71
  );
58
72
  }
@@ -69,7 +83,6 @@ export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
69
83
  complete: true,
70
84
  id: this.created.id,
71
85
  md5: this.md5,
72
- asset_id: this.created.asset_id,
73
86
  byte_size: byteSize,
74
87
  });
75
88
  }
@@ -1,9 +1,11 @@
1
+ import { useMSW } from "TEST/useMSW.ts";
1
2
  import { describe, expect, test } from "vitest";
2
3
  import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
4
  import {
4
5
  mockCreateIsobmffFile,
6
+ mockLookupISOBMFFFileByMd5,
7
+ mockLookupISOBMFFFileByMd5NotFound,
5
8
  mockUploadIsobmffFileIndex,
6
- useMSW,
7
9
  } from "../../../test-fixtures/network.ts";
8
10
  import { SyncFragmentIndex } from "./SyncFragmentIndex.ts";
9
11
 
@@ -34,8 +36,25 @@ describe("SyncFragmentIndex", async () => {
34
36
  await expect(syncFragmentIndex.validate()).resolves.toBeUndefined();
35
37
  });
36
38
  describe(".create()", () => {
39
+ test("uses matching data when file matches md5", async () => {
40
+ server.use(
41
+ mockLookupISOBMFFFileByMd5({
42
+ md5: video!.md5,
43
+ fixture: video!,
44
+ }),
45
+ );
46
+ const syncFragmentIndex = new SyncFragmentIndex(
47
+ await generateTrackFragmentIndex(video!),
48
+ video!.md5,
49
+ );
50
+ await syncFragmentIndex.create();
51
+ expect(syncFragmentIndex.isComplete()).toBe(false);
52
+ });
37
53
  test("isComplete() returns false when not created", async () => {
38
54
  server.use(
55
+ mockLookupISOBMFFFileByMd5NotFound({
56
+ md5: video!.md5,
57
+ }),
39
58
  mockCreateIsobmffFile({
40
59
  complete: false,
41
60
  id: "123",
@@ -52,6 +71,9 @@ describe("SyncFragmentIndex", async () => {
52
71
  });
53
72
  test("isComplete() returns true when created", async () => {
54
73
  server.use(
74
+ mockLookupISOBMFFFileByMd5NotFound({
75
+ md5: video!.md5,
76
+ }),
55
77
  mockCreateIsobmffFile({
56
78
  complete: true,
57
79
  id: "123",
@@ -75,8 +97,11 @@ describe("SyncFragmentIndex", async () => {
75
97
  );
76
98
  await expect(syncFragmentIndex.upload()).rejects.toThrow();
77
99
  });
78
- test("uploads caption", async () => {
100
+ test("uploads index", async () => {
79
101
  server.use(
102
+ mockLookupISOBMFFFileByMd5NotFound({
103
+ md5: video!.md5,
104
+ }),
80
105
  mockCreateIsobmffFile({
81
106
  complete: true,
82
107
  id: "123",
@@ -105,6 +130,9 @@ describe("SyncFragmentIndex", async () => {
105
130
  });
106
131
  test("marks synced", async () => {
107
132
  server.use(
133
+ mockLookupISOBMFFFileByMd5NotFound({
134
+ md5: video!.md5,
135
+ }),
108
136
  mockCreateIsobmffFile({
109
137
  complete: true,
110
138
  id: "123",
@@ -130,7 +158,6 @@ describe("SyncFragmentIndex", async () => {
130
158
  complete: true,
131
159
  id: "123",
132
160
  md5: video!.md5,
133
- asset_id: `${video!.md5}:test.mp4`,
134
161
  byte_size: 31,
135
162
  });
136
163
 
@@ -141,7 +168,6 @@ describe("SyncFragmentIndex", async () => {
141
168
  complete: true,
142
169
  id: "123",
143
170
  md5: video!.md5,
144
- asset_id: `${video!.md5}:test.mp4`,
145
171
  byte_size: 31,
146
172
  });
147
173
  });
@@ -1,13 +1,16 @@
1
1
  import fs from "node:fs/promises";
2
- import { dirname, join } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { Readable } from "node:stream";
4
4
 
5
5
  import {
6
6
  type CreateISOBMFFFileResult,
7
+ type LookupISOBMFFFileByMd5Result,
7
8
  createISOBMFFFile,
9
+ lookupISOBMFFFileByMd5,
8
10
  uploadFragmentIndex,
9
11
  } from "@editframe/api";
10
12
 
13
+ import { createReadableStreamFromReadable } from "../../utils/createReadableStreamFromReadable.ts";
11
14
  import { getClient } from "../../utils/index.ts";
12
15
  import type { SubAssetSync } from "./SubAssetSync.ts";
13
16
  import { SyncStatus } from "./SyncStatus.ts";
@@ -17,11 +20,9 @@ export class SyncFragmentIndex
17
20
  {
18
21
  icon = "📋";
19
22
  label = "fragment index";
20
- syncStatus: SyncStatus = new SyncStatus(this.path);
21
- fileSyncStatus: SyncStatus = new SyncStatus(
22
- join(dirname(this.path), "isobmff"),
23
- );
24
- created: CreateISOBMFFFileResult | null = null;
23
+ syncStatus = new SyncStatus(this.path);
24
+ fileSyncStatus = new SyncStatus(join(dirname(this.path), "isobmff"));
25
+ created: CreateISOBMFFFileResult | LookupISOBMFFFileByMd5Result | null = null;
25
26
 
26
27
  constructor(
27
28
  public path: string,
@@ -37,10 +38,18 @@ export class SyncFragmentIndex
37
38
  async validate() {}
38
39
 
39
40
  async create() {
40
- this.created = await createISOBMFFFile(getClient(), {
41
- md5: this.md5,
42
- filename: this.path.replace(/\.tracks.json$/, ""),
43
- });
41
+ const maybeISOBMFFFile = await lookupISOBMFFFileByMd5(
42
+ getClient(),
43
+ this.md5,
44
+ );
45
+ if (maybeISOBMFFFile) {
46
+ this.created = maybeISOBMFFFile;
47
+ } else {
48
+ this.created = await createISOBMFFFile(getClient(), {
49
+ md5: this.md5,
50
+ filename: basename(this.path).replace(/\.tracks.json$/, ""),
51
+ });
52
+ }
44
53
  }
45
54
 
46
55
  isComplete() {
@@ -58,7 +67,9 @@ export class SyncFragmentIndex
58
67
  this.created.id,
59
68
  // It is unclear why we need to use Readable.from here
60
69
  // Tests fail when using createReadStream
61
- Readable.from(await fs.readFile(this.path)),
70
+ createReadableStreamFromReadable(
71
+ Readable.from(await fs.readFile(this.path)),
72
+ ),
62
73
  await this.byteSize(),
63
74
  );
64
75
  }
@@ -76,7 +87,6 @@ export class SyncFragmentIndex
76
87
  complete: true,
77
88
  id: this.created.id,
78
89
  md5: this.md5,
79
- asset_id: this.created.asset_id,
80
90
  byte_size: byteSize,
81
91
  }),
82
92
  this.fileSyncStatus.markSynced({
@@ -84,7 +94,6 @@ export class SyncFragmentIndex
84
94
  complete: true,
85
95
  id: this.created.id,
86
96
  md5: this.md5,
87
- asset_id: this.created.asset_id,
88
97
  byte_size: byteSize,
89
98
  }),
90
99
  ]);
@@ -1,12 +1,13 @@
1
+ import { useMSW } from "TEST/useMSW.ts";
1
2
  import { describe, expect, test } from "vitest";
2
3
  import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
4
  import {
4
5
  mockCreateImageFile,
5
6
  mockGetUploadImageFile,
6
- useMSW,
7
+ mockLookupImageFileByMd5,
8
+ mockLookupImageFileByMd5NotFound,
7
9
  } from "../../../test-fixtures/network.ts";
8
10
  import { SyncImage } from "./SyncImage.ts";
9
-
10
11
  describe("SyncImage", async () => {
11
12
  const server = useMSW();
12
13
  await withFixtures(
@@ -37,8 +38,23 @@ describe("SyncImage", async () => {
37
38
  const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
38
39
  await expect(syncImage.create()).rejects.toThrow();
39
40
  });
41
+ test("Skips creation if image file matches md5", async () => {
42
+ server.use(
43
+ mockLookupImageFileByMd5({
44
+ md5: image!.md5,
45
+ fixture: image!,
46
+ }),
47
+ );
48
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
49
+ await syncImage.prepare();
50
+ await syncImage.create();
51
+ expect(syncImage.isComplete()).toBe(true);
52
+ });
40
53
  test("isComplete() returns false when not created", async () => {
41
54
  server.use(
55
+ mockLookupImageFileByMd5NotFound({
56
+ md5: image!.md5,
57
+ }),
42
58
  mockCreateImageFile({
43
59
  complete: false,
44
60
  id: "123",
@@ -53,6 +69,9 @@ describe("SyncImage", async () => {
53
69
  });
54
70
  test("isComplete() returns true when created", async () => {
55
71
  server.use(
72
+ mockLookupImageFileByMd5NotFound({
73
+ md5: image!.md5,
74
+ }),
56
75
  mockCreateImageFile({
57
76
  complete: true,
58
77
  id: "123",
@@ -74,6 +93,9 @@ describe("SyncImage", async () => {
74
93
  });
75
94
  test("uploads image", async () => {
76
95
  server.use(
96
+ mockLookupImageFileByMd5NotFound({
97
+ md5: image!.md5,
98
+ }),
77
99
  mockCreateImageFile({
78
100
  complete: false,
79
101
  id: "123",
@@ -98,6 +120,9 @@ describe("SyncImage", async () => {
98
120
  });
99
121
  test("marks synced", async () => {
100
122
  server.use(
123
+ mockLookupImageFileByMd5NotFound({
124
+ md5: image!.md5,
125
+ }),
101
126
  mockCreateImageFile({
102
127
  complete: true,
103
128
  id: "123",
@@ -119,7 +144,6 @@ describe("SyncImage", async () => {
119
144
  complete: true,
120
145
  id: "123",
121
146
  md5: image!.md5,
122
- asset_id: `${image!.md5}:test.png`,
123
147
  byte_size: 276,
124
148
  });
125
149
  });
@@ -4,12 +4,15 @@ import path, { basename } from "node:path";
4
4
 
5
5
  import {
6
6
  type CreateImageFileResult,
7
+ type LookupImageFileByMd5Result,
7
8
  createImageFile,
9
+ lookupImageFileByMd5,
8
10
  uploadImageFile,
9
11
  } from "@editframe/api";
10
12
 
11
13
  import { Probe } from "@editframe/assets";
12
14
 
15
+ import { createReadableStreamFromReadable } from "../../utils/createReadableStreamFromReadable.ts";
13
16
  import { getClient } from "../../utils/index.ts";
14
17
  import type { SubAssetSync } from "./SubAssetSync.ts";
15
18
  import { SyncStatus } from "./SyncStatus.ts";
@@ -18,7 +21,7 @@ export class SyncImage implements SubAssetSync<CreateImageFileResult> {
18
21
  icon = "🖼️";
19
22
  label = "image";
20
23
  syncStatus: SyncStatus = new SyncStatus(this.path);
21
- created: CreateImageFileResult | null = null;
24
+ created: CreateImageFileResult | LookupImageFileByMd5Result | null = null;
22
25
 
23
26
  constructor(
24
27
  public path: string,
@@ -64,18 +67,24 @@ export class SyncImage implements SubAssetSync<CreateImageFileResult> {
64
67
  "No video stream found in image. Should have been prevented by .validate()",
65
68
  );
66
69
  }
67
- this.created = await createImageFile(getClient(), {
68
- md5: this.md5,
69
- filename: basename(this.path),
70
- width: videoProbe.width,
71
- height: videoProbe.height,
72
- mime_type: `image/${this.extension}` as
73
- | "image/jpeg"
74
- | "image/png"
75
- | "image/jpg"
76
- | "image/webp",
77
- byte_size: byteSize,
78
- });
70
+
71
+ const maybeImageFile = await lookupImageFileByMd5(getClient(), this.md5);
72
+ if (maybeImageFile) {
73
+ this.created = maybeImageFile;
74
+ } else {
75
+ this.created = await createImageFile(getClient(), {
76
+ md5: this.md5,
77
+ filename: basename(this.path),
78
+ width: videoProbe.width,
79
+ height: videoProbe.height,
80
+ mime_type: `image/${this.extension}` as
81
+ | "image/jpeg"
82
+ | "image/png"
83
+ | "image/jpg"
84
+ | "image/webp",
85
+ byte_size: byteSize,
86
+ });
87
+ }
79
88
  }
80
89
  isComplete() {
81
90
  return !!this.created?.complete;
@@ -89,7 +98,7 @@ export class SyncImage implements SubAssetSync<CreateImageFileResult> {
89
98
  await uploadImageFile(
90
99
  getClient(),
91
100
  this.created.id,
92
- createReadStream(this.path),
101
+ createReadableStreamFromReadable(createReadStream(this.path)),
93
102
  Number.parseInt(this.probeResult.format.size || "0"),
94
103
  ).whenUploaded();
95
104
  }
@@ -105,7 +114,6 @@ export class SyncImage implements SubAssetSync<CreateImageFileResult> {
105
114
  complete: true,
106
115
  id: this.created.id,
107
116
  md5: this.md5,
108
- asset_id: this.created.asset_id,
109
117
  byte_size: byteSize,
110
118
  });
111
119
  }
@@ -9,22 +9,22 @@ const SyncStatusSchema = z.object({
9
9
  complete: z.boolean(),
10
10
  id: z.string(),
11
11
  md5: z.string(),
12
- asset_id: z.string(),
13
12
  byte_size: z.number(),
14
13
  });
15
14
 
16
15
  export interface SyncStatusInfo extends z.infer<typeof SyncStatusSchema> {}
17
16
 
18
17
  export class SyncStatus {
19
- constructor(private basePath: string) {}
20
-
21
18
  infoPath = `${this.basePath}.info`;
22
19
 
20
+ constructor(private basePath: string) {}
21
+
23
22
  async isSynced() {
24
23
  const syncInfo = await this.readInfo();
25
24
  if (!syncInfo) {
26
25
  return false;
27
26
  }
27
+ console.log("syncInfo.infoPath", this.infoPath);
28
28
  return syncInfo.version === SYNC_VERSION && syncInfo.complete;
29
29
  }
30
30