@editframe/cli 0.10.0-beta.5 → 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 (53) 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 +55 -43
  5. package/dist/commands/sync.js +5 -2
  6. package/dist/operations/syncAssetsDirectory/SubAssetSync.d.ts +20 -0
  7. package/dist/operations/syncAssetsDirectory/SubAssetSync.js +26 -0
  8. package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +19 -0
  9. package/dist/operations/syncAssetsDirectory/SyncCaption.js +66 -0
  10. package/dist/operations/syncAssetsDirectory/SyncCaption.test.d.ts +1 -0
  11. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +20 -0
  12. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +79 -0
  13. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.test.d.ts +1 -0
  14. package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +23 -0
  15. package/dist/operations/syncAssetsDirectory/SyncImage.js +95 -0
  16. package/dist/operations/syncAssetsDirectory/SyncImage.test.d.ts +1 -0
  17. package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +41 -0
  18. package/dist/operations/syncAssetsDirectory/SyncStatus.js +43 -0
  19. package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +70 -0
  20. package/dist/operations/syncAssetsDirectory/SyncTrack.js +138 -0
  21. package/dist/operations/syncAssetsDirectory/SyncTrack.test.d.ts +1 -0
  22. package/dist/operations/syncAssetsDirectory/doAssetSync.d.ts +5 -0
  23. package/dist/operations/syncAssetsDirectory/doAssetSync.js +48 -0
  24. package/dist/operations/syncAssetsDirectory/doAssetSync.test.d.ts +1 -0
  25. package/dist/operations/syncAssetsDirectory.d.ts +1 -1
  26. package/dist/operations/syncAssetsDirectory.js +20 -240
  27. package/dist/test-fixtures/fixture.d.ts +26 -0
  28. package/dist/utils/index.js +4 -1
  29. package/package.json +5 -5
  30. package/src/commands/render.ts +61 -52
  31. package/src/commands/sync.ts +5 -2
  32. package/src/operations/syncAssetsDirectory/SubAssetSync.ts +42 -0
  33. package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +145 -0
  34. package/src/operations/syncAssetsDirectory/SyncCaption.ts +76 -0
  35. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +151 -0
  36. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +92 -0
  37. package/src/operations/syncAssetsDirectory/SyncImage.test.ts +131 -0
  38. package/src/operations/syncAssetsDirectory/SyncImage.ts +112 -0
  39. package/src/operations/syncAssetsDirectory/SyncStatus.ts +51 -0
  40. package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +222 -0
  41. package/src/operations/syncAssetsDirectory/SyncTrack.ts +164 -0
  42. package/src/operations/syncAssetsDirectory/doAssetSync.test.ts +134 -0
  43. package/src/operations/syncAssetsDirectory/doAssetSync.ts +62 -0
  44. package/src/operations/syncAssetsDirectory.test.ts +482 -0
  45. package/src/operations/syncAssetsDirectory.ts +22 -283
  46. package/src/utils/index.ts +4 -1
  47. package/test-fixtures/fixture.ts +141 -0
  48. package/test-fixtures/network.ts +181 -0
  49. package/test-fixtures/test-captions.json +9 -0
  50. package/test-fixtures/test.mp4 +0 -0
  51. package/test-fixtures/test.png +0 -0
  52. package/src/commands/render.test.ts +0 -34
  53. /package/dist/{commands/render.test.d.ts → operations/syncAssetsDirectory.test.d.ts} +0 -0
@@ -0,0 +1,131 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
+ import {
4
+ mockCreateImageFile,
5
+ mockGetUploadImageFile,
6
+ useMSW,
7
+ } from "../../../test-fixtures/network.ts";
8
+ import { SyncImage } from "./SyncImage.ts";
9
+
10
+ describe("SyncImage", async () => {
11
+ const server = useMSW();
12
+ await withFixtures(
13
+ [fixture("test.png", "test.png")],
14
+ async ({ files: [image], cacheImage }) => {
15
+ test("Reads byte size", async () => {
16
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
17
+ await expect(syncImage.byteSize()).resolves.toEqual(276);
18
+ });
19
+ test("prepare() probes image", async () => {
20
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
21
+ await expect(syncImage.prepare()).resolves.toBeUndefined();
22
+ console.log(syncImage.probeResult);
23
+ expect(syncImage.probeResult.data.format).toMatchObject({
24
+ format_name: "png_pipe",
25
+ });
26
+ });
27
+ describe("validate()", () => {
28
+ test("Throws when prepare() is not called", async () => {
29
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
30
+ await expect(syncImage.validate()).rejects.toThrow();
31
+ });
32
+ test.skip("throws when probe is not a supported image", async () => {});
33
+ });
34
+
35
+ describe(".create()", () => {
36
+ test("Throws when prepare() is not called", async () => {
37
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
38
+ await expect(syncImage.create()).rejects.toThrow();
39
+ });
40
+ test("isComplete() returns false when not created", async () => {
41
+ server.use(
42
+ mockCreateImageFile({
43
+ complete: false,
44
+ id: "123",
45
+ filename: "test.mp4",
46
+ fixture: image!,
47
+ }),
48
+ );
49
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
50
+ await syncImage.prepare();
51
+ await syncImage.create();
52
+ expect(syncImage.isComplete()).toBe(false);
53
+ });
54
+ test("isComplete() returns true when created", async () => {
55
+ server.use(
56
+ mockCreateImageFile({
57
+ complete: true,
58
+ id: "123",
59
+ filename: "test.mp4",
60
+ fixture: image!,
61
+ }),
62
+ );
63
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
64
+ await syncImage.prepare();
65
+ await syncImage.create();
66
+ expect(syncImage.isComplete()).toBe(true);
67
+ });
68
+ });
69
+ describe(".upload()", () => {
70
+ test("throws when not created", async () => {
71
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
72
+ await syncImage.prepare();
73
+ // This is throwing syncronously because the error is thrown before
74
+ // any async operations are awaited.
75
+ expect(() => syncImage.upload()).toThrow();
76
+ });
77
+ test("uploads image", async () => {
78
+ server.use(
79
+ mockCreateImageFile({
80
+ complete: false,
81
+ id: "123",
82
+ filename: "test.mp4",
83
+ fixture: image!,
84
+ }),
85
+ mockGetUploadImageFile({
86
+ id: "123",
87
+ fixture: image!,
88
+ }),
89
+ );
90
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
91
+ await syncImage.prepare();
92
+ await syncImage.create();
93
+ await expect(syncImage.upload()).resolves.toBeUndefined();
94
+ });
95
+ });
96
+ describe(".markSynced()", () => {
97
+ test("throws when not created", async () => {
98
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
99
+ await expect(syncImage.markSynced()).rejects.toThrow();
100
+ });
101
+ test("marks synced", async () => {
102
+ server.use(
103
+ mockCreateImageFile({
104
+ complete: true,
105
+ id: "123",
106
+ fixture: image!,
107
+ }),
108
+ mockGetUploadImageFile({
109
+ id: "123",
110
+ fixture: image!,
111
+ }),
112
+ );
113
+ const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
114
+ await syncImage.prepare();
115
+ await syncImage.create();
116
+ await syncImage.upload();
117
+ await syncImage.markSynced();
118
+ await expect(syncImage.syncStatus.isSynced()).resolves.toBe(true);
119
+ await expect(syncImage.syncStatus.readInfo()).resolves.toEqual({
120
+ version: "1",
121
+ complete: true,
122
+ id: "123",
123
+ md5: image!.md5,
124
+ asset_id: `${image!.md5}:test.png`,
125
+ byte_size: 276,
126
+ });
127
+ });
128
+ });
129
+ },
130
+ );
131
+ });
@@ -0,0 +1,112 @@
1
+ import { createReadStream } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path, { basename } from "node:path";
4
+
5
+ import {
6
+ type CreateImageFileResult,
7
+ createImageFile,
8
+ uploadImageFile,
9
+ } from "@editframe/api";
10
+
11
+ import { Probe } from "@editframe/assets";
12
+
13
+ import { getClient } from "../../utils/index.ts";
14
+ import type { SubAssetSync } from "./SubAssetSync.ts";
15
+ import { SyncStatus } from "./SyncStatus.ts";
16
+
17
+ export class SyncImage implements SubAssetSync<CreateImageFileResult> {
18
+ icon = "🖼️";
19
+ label = "image";
20
+ syncStatus: SyncStatus = new SyncStatus(this.path);
21
+ created: CreateImageFileResult | null = null;
22
+
23
+ constructor(
24
+ public path: string,
25
+ public md5: string,
26
+ ) {}
27
+
28
+ private _probeResult: Probe | null = null;
29
+
30
+ async prepare() {
31
+ this._probeResult = await Probe.probePath(this.path);
32
+ }
33
+
34
+ get probeResult() {
35
+ if (!this._probeResult) {
36
+ throw new Error("Probe result not found. Call prepare() first.");
37
+ }
38
+ return this._probeResult;
39
+ }
40
+
41
+ get extension() {
42
+ return path.extname(this.path).slice(1);
43
+ }
44
+
45
+ async byteSize() {
46
+ return (await fs.stat(this.path)).size;
47
+ }
48
+
49
+ async validate() {
50
+ const [videoProbe] = this.probeResult.videoStreams;
51
+ if (!videoProbe) {
52
+ throw new Error(`No media info found in image: ${this.path}`);
53
+ }
54
+ const ext = this.extension;
55
+ if (!(ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "webp")) {
56
+ throw new Error(`Invalid image format: ${this.path}`);
57
+ }
58
+ }
59
+ async create() {
60
+ const byteSize = (await fs.stat(this.path)).size;
61
+ const [videoProbe] = this.probeResult.videoStreams;
62
+ if (!videoProbe) {
63
+ throw new Error(
64
+ "No video stream found in image. Should have been prevented by .validate()",
65
+ );
66
+ }
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
+ });
79
+ }
80
+ isComplete() {
81
+ return !!this.created?.complete;
82
+ }
83
+ upload() {
84
+ if (!this.created) {
85
+ throw new Error(
86
+ "Image not created. Should have been prevented by .isComplete()",
87
+ );
88
+ }
89
+ return uploadImageFile(
90
+ getClient(),
91
+ this.created.id,
92
+ createReadStream(this.path),
93
+ Number.parseInt(this.probeResult.format.size || "0"),
94
+ );
95
+ }
96
+ async markSynced() {
97
+ if (!this.created) {
98
+ throw new Error(
99
+ "Image not created. Should have been prevented by .isComplete()",
100
+ );
101
+ }
102
+ const byteSize = await this.byteSize();
103
+ return this.syncStatus.markSynced({
104
+ version: "1",
105
+ complete: true,
106
+ id: this.created.id,
107
+ md5: this.md5,
108
+ asset_id: this.created.asset_id,
109
+ byte_size: byteSize,
110
+ });
111
+ }
112
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ import { z } from "zod";
4
+
5
+ const SYNC_VERSION = "1";
6
+
7
+ const SyncStatusSchema = z.object({
8
+ version: z.string(),
9
+ complete: z.boolean(),
10
+ id: z.string(),
11
+ md5: z.string(),
12
+ asset_id: z.string(),
13
+ byte_size: z.number(),
14
+ });
15
+
16
+ export interface SyncStatusInfo extends z.infer<typeof SyncStatusSchema> {}
17
+
18
+ export class SyncStatus {
19
+ constructor(private basePath: string) {}
20
+
21
+ infoPath = `${this.basePath}.info`;
22
+
23
+ async isSynced() {
24
+ const syncInfo = await this.readInfo();
25
+ if (!syncInfo) {
26
+ return false;
27
+ }
28
+ return syncInfo.version === SYNC_VERSION && syncInfo.complete;
29
+ }
30
+
31
+ async readInfo() {
32
+ try {
33
+ const info = await fs.readFile(this.infoPath, "utf-8");
34
+ return SyncStatusSchema.parse(JSON.parse(info));
35
+ } catch (error) {
36
+ if (
37
+ error instanceof Error &&
38
+ "code" in error &&
39
+ error.code === "ENOENT"
40
+ ) {
41
+ return null;
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ async markSynced(info: SyncStatusInfo) {
48
+ process.stderr.write(`✏️ Marking asset as synced: ${this.basePath}\n`);
49
+ await fs.writeFile(this.infoPath, JSON.stringify(info, null, 2), "utf-8");
50
+ }
51
+ }
@@ -0,0 +1,222 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
+ import {
4
+ mockCreateIsobmffFile,
5
+ mockCreateIsobmffTrack,
6
+ mockGetIsobmffTrackUpload,
7
+ useMSW,
8
+ } from "../../../test-fixtures/network.ts";
9
+ import { SyncTrack } from "./SyncTrack.ts";
10
+
11
+ describe("SyncTrack", async () => {
12
+ const server = useMSW();
13
+ await withFixtures(
14
+ [fixture("test.mp4", "test.mp4")],
15
+ async ({ files: [video], generateTrack }) => {
16
+ test("Reads byte size", async () => {
17
+ const syncTrack = new SyncTrack(
18
+ await generateTrack(video!, 1),
19
+ video!.md5,
20
+ );
21
+ await expect(syncTrack.byteSize()).resolves.toEqual(26434);
22
+ });
23
+ describe("prepare()", () => {
24
+ test("Creates ISOBMFF file", async () => {
25
+ server.use(
26
+ mockCreateIsobmffFile({
27
+ complete: true,
28
+ id: "123",
29
+ filename: "test.mp4",
30
+ fixture: video!,
31
+ }),
32
+ );
33
+ const syncTrack = new SyncTrack(
34
+ await generateTrack(video!, 1),
35
+ video!.md5,
36
+ );
37
+ await syncTrack.prepare();
38
+ expect(syncTrack.isoFile).toBeDefined();
39
+ });
40
+
41
+ test("Probes track", async () => {
42
+ server.use(
43
+ mockCreateIsobmffFile({
44
+ complete: true,
45
+ id: "123",
46
+ filename: "test.mp4",
47
+ fixture: video!,
48
+ }),
49
+ );
50
+ const syncTrack = new SyncTrack(
51
+ await generateTrack(video!, 1),
52
+ video!.md5,
53
+ );
54
+ await syncTrack.prepare();
55
+ expect(syncTrack.probeResult).toBeDefined();
56
+ });
57
+ });
58
+
59
+ describe("validate()", () => {
60
+ test("throws when prepare() is not called", async () => {
61
+ const syncTrack = new SyncTrack(
62
+ await generateTrack(video!, 1),
63
+ video!.md5,
64
+ );
65
+ await expect(syncTrack.validate()).rejects.toThrow();
66
+ });
67
+
68
+ test.skip("throws when track has bad duration", async () => {});
69
+ test.skip("throws when track has bad codec_type", async () => {});
70
+ test.skip("throws when no isoFile exists", async () => {});
71
+ });
72
+
73
+ describe(".create()", () => {
74
+ test("throws if prepare() is not called", async () => {
75
+ const syncTrack = new SyncTrack(
76
+ await generateTrack(video!, 1),
77
+ video!.md5,
78
+ );
79
+ await expect(syncTrack.create()).rejects.toThrow();
80
+ });
81
+
82
+ test("isComplete() returns false when not complete", async () => {
83
+ server.use(
84
+ mockCreateIsobmffFile({
85
+ complete: true,
86
+ id: "123",
87
+ filename: "test.mp4",
88
+ fixture: video!,
89
+ }),
90
+ mockCreateIsobmffTrack({
91
+ complete: false,
92
+ id: "track-1",
93
+ fileId: "123",
94
+ fixture: video!,
95
+ }),
96
+ );
97
+
98
+ const syncTrack = new SyncTrack(
99
+ await generateTrack(video!, 1),
100
+ video!.md5,
101
+ );
102
+ await syncTrack.prepare();
103
+ await syncTrack.create();
104
+ expect(syncTrack.isComplete()).toBe(false);
105
+ });
106
+
107
+ test("isComplete() returns true when complete", async () => {
108
+ server.use(
109
+ mockCreateIsobmffFile({
110
+ complete: true,
111
+ id: "123",
112
+ filename: "test.mp4",
113
+ fixture: video!,
114
+ }),
115
+ mockCreateIsobmffTrack({
116
+ complete: true,
117
+ id: "track-1",
118
+ fileId: "123",
119
+ fixture: video!,
120
+ }),
121
+ );
122
+
123
+ const syncTrack = new SyncTrack(
124
+ await generateTrack(video!, 1),
125
+ video!.md5,
126
+ );
127
+ await syncTrack.prepare();
128
+ await syncTrack.create();
129
+ expect(syncTrack.isComplete()).toBe(true);
130
+ });
131
+ });
132
+
133
+ describe(".upload()", () => {
134
+ test("throws when not created", async () => {
135
+ const syncTrack = new SyncTrack(
136
+ await generateTrack(video!, 1),
137
+ video!.md5,
138
+ );
139
+ await expect(syncTrack.upload()).rejects.toThrow();
140
+ });
141
+ test("uploads track", async () => {
142
+ server.use(
143
+ mockCreateIsobmffFile({
144
+ complete: true,
145
+ id: "123",
146
+ filename: "test.mp4",
147
+ fixture: video!,
148
+ }),
149
+ mockCreateIsobmffTrack({
150
+ complete: true,
151
+ id: "track-1",
152
+ fileId: "123",
153
+ fixture: video!,
154
+ }),
155
+ mockGetIsobmffTrackUpload({
156
+ complete: true,
157
+ id: "track-1",
158
+ trackId: 1,
159
+ fileId: "123",
160
+ }),
161
+ );
162
+ const syncTrack = new SyncTrack(
163
+ await generateTrack(video!, 1),
164
+ video!.md5,
165
+ );
166
+ await syncTrack.prepare();
167
+ await syncTrack.create();
168
+ await expect(syncTrack.upload()).resolves.toBeUndefined();
169
+ });
170
+ });
171
+ describe(".markSynced()", () => {
172
+ test("throws when not created", async () => {
173
+ const syncTrack = new SyncTrack(
174
+ await generateTrack(video!, 1),
175
+ video!.md5,
176
+ );
177
+ await expect(syncTrack.markSynced()).rejects.toThrow();
178
+ });
179
+ test("marks synced", async () => {
180
+ server.use(
181
+ mockCreateIsobmffFile({
182
+ complete: true,
183
+ id: "123",
184
+ fixture: video!,
185
+ }),
186
+ mockCreateIsobmffTrack({
187
+ complete: true,
188
+ id: "track-1",
189
+ fileId: "123",
190
+ fixture: video!,
191
+ }),
192
+ );
193
+ const syncTrack = new SyncTrack(
194
+ await generateTrack(video!, 1),
195
+ video!.md5,
196
+ );
197
+ await syncTrack.prepare();
198
+ await syncTrack.create();
199
+ await syncTrack.markSynced();
200
+ await expect(syncTrack.syncStatus.isSynced()).resolves.toBe(true);
201
+ await expect(syncTrack.syncStatus.readInfo()).resolves.toEqual({
202
+ version: "1",
203
+ complete: true,
204
+ id: "123:track-1",
205
+ md5: video!.md5,
206
+ asset_id: `${video!.md5}:test.mp4`,
207
+ byte_size: 26434,
208
+ });
209
+
210
+ await expect(syncTrack.fileSyncStatus.readInfo()).resolves.toEqual({
211
+ version: "1",
212
+ complete: true,
213
+ id: "123",
214
+ md5: video!.md5,
215
+ asset_id: `${video!.md5}:test.mp4`,
216
+ byte_size: 26434,
217
+ });
218
+ });
219
+ });
220
+ },
221
+ );
222
+ });
@@ -0,0 +1,164 @@
1
+ import { createReadStream } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+
5
+ import type { z } from "zod";
6
+
7
+ import {
8
+ type CreateISOBMFFFileResult,
9
+ type CreateISOBMFFTrackPayload,
10
+ type CreateISOBMFFTrackResult,
11
+ createISOBMFFFile,
12
+ createISOBMFFTrack,
13
+ uploadISOBMFFTrack,
14
+ } from "@editframe/api";
15
+ import { Probe } from "@editframe/assets";
16
+
17
+ import { getClient } from "../../utils/index.ts";
18
+ import type { SubAssetSync } from "./SubAssetSync.ts";
19
+ import { SyncStatus } from "./SyncStatus.ts";
20
+
21
+ export class SyncTrack implements SubAssetSync<CreateISOBMFFTrackResult> {
22
+ icon = "📼";
23
+ label = "track";
24
+ syncStatus: SyncStatus = new SyncStatus(this.path);
25
+ fileSyncStatus: SyncStatus = new SyncStatus(
26
+ join(dirname(this.path), "isobmff"),
27
+ );
28
+ created: CreateISOBMFFTrackResult | null = null;
29
+
30
+ constructor(
31
+ public path: string,
32
+ public md5: string,
33
+ ) {}
34
+
35
+ private _isoFile: CreateISOBMFFFileResult | null = null;
36
+
37
+ get isoFile() {
38
+ if (this._isoFile) {
39
+ return this._isoFile;
40
+ }
41
+ throw new Error("ISOBMFF file not found. Call prepare() first.");
42
+ }
43
+
44
+ async byteSize() {
45
+ return (await fs.stat(this.path)).size;
46
+ }
47
+
48
+ private _probeResult: Probe | null = null;
49
+ get probeResult() {
50
+ if (this._probeResult) {
51
+ return this._probeResult;
52
+ }
53
+ throw new Error("Probe result not found. Call prepare() first.");
54
+ }
55
+
56
+ get track() {
57
+ const [track] = this.probeResult.streams;
58
+ if (track) {
59
+ return track;
60
+ }
61
+ throw new Error(`No track found in track: ${this.path}`);
62
+ }
63
+
64
+ async prepare() {
65
+ this._isoFile = await createISOBMFFFile(getClient(), {
66
+ md5: this.md5,
67
+ filename: this.path.replace(/\.track-[\d]+.mp4$/, ""),
68
+ });
69
+ this._probeResult = await Probe.probePath(this.path);
70
+ }
71
+
72
+ get trackId() {
73
+ const trackId = this.path.match(/track-([\d]+).mp4/)?.[1];
74
+ if (!trackId) {
75
+ throw new Error(`No track ID found for track: ${this.path}`);
76
+ }
77
+ return trackId;
78
+ }
79
+
80
+ get trackDuration() {
81
+ const track = this.track;
82
+ if (!track.duration) {
83
+ throw new Error(`No duration found in track: ${this.path}`);
84
+ }
85
+ return track.duration;
86
+ }
87
+
88
+ async validate() {
89
+ this.trackId;
90
+ this.isoFile;
91
+ this.trackDuration;
92
+ }
93
+
94
+ async create(): Promise<void> {
95
+ const track = this.track;
96
+ const isoFile = this.isoFile;
97
+
98
+ const createPayload: z.infer<typeof CreateISOBMFFTrackPayload> =
99
+ track.codec_type === "audio"
100
+ ? {
101
+ type: track.codec_type,
102
+ file_id: isoFile.id,
103
+ track_id: Number(this.trackId),
104
+ probe_info: track,
105
+ duration_ms: Math.round(this.trackDuration * 1000),
106
+ codec_name: track.codec_name,
107
+ byte_size: await this.byteSize(),
108
+ }
109
+ : {
110
+ type: track.codec_type,
111
+ file_id: isoFile.id,
112
+ track_id: Number(this.trackId),
113
+ probe_info: track,
114
+ duration_ms: Math.round(this.trackDuration * 1000),
115
+ codec_name: track.codec_name,
116
+ byte_size: await this.byteSize(),
117
+ };
118
+
119
+ this.created = await createISOBMFFTrack(getClient(), createPayload);
120
+ }
121
+ isComplete() {
122
+ return !!this.created?.complete;
123
+ }
124
+ async upload() {
125
+ if (!this.created) {
126
+ throw new Error(
127
+ "Track not created. Should have been prevented by .isComplete()",
128
+ );
129
+ }
130
+ return uploadISOBMFFTrack(
131
+ getClient(),
132
+ this.isoFile.id,
133
+ Number(this.trackId),
134
+ createReadStream(this.path),
135
+ this.created?.byte_size,
136
+ );
137
+ }
138
+ async markSynced() {
139
+ if (!this.created) {
140
+ throw new Error(
141
+ "Track not created. Should have been prevented by .isComplete()",
142
+ );
143
+ }
144
+ const byteSize = await this.byteSize();
145
+ await Promise.all([
146
+ this.syncStatus.markSynced({
147
+ version: "1",
148
+ complete: true,
149
+ id: `${this.created.file_id}:${this.created.track_id}`,
150
+ md5: this.md5,
151
+ asset_id: this.created.asset_id,
152
+ byte_size: byteSize,
153
+ }),
154
+ this.fileSyncStatus.markSynced({
155
+ version: "1",
156
+ complete: true,
157
+ id: this.created.file_id,
158
+ md5: this.md5,
159
+ asset_id: this.created.asset_id,
160
+ byte_size: byteSize,
161
+ }),
162
+ ]);
163
+ }
164
+ }