@editframe/api 0.26.3-beta.0 → 0.30.0-beta.13
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/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/package.json +5 -4
- package/tsdown.config.ts +3 -0
- package/types.json +1 -1
- package/src/resources/caption-file.test.ts +0 -172
- package/src/resources/caption-file.ts +0 -154
- package/src/resources/image-file.test.ts +0 -220
- package/src/resources/image-file.ts +0 -205
- package/src/resources/isobmff-file.test.ts +0 -159
- package/src/resources/isobmff-file.ts +0 -186
- package/src/resources/isobmff-track.test.ts +0 -126
- package/src/resources/isobmff-track.ts +0 -248
- package/src/resources/process-isobmff.test.ts +0 -68
- package/src/resources/process-isobmff.ts +0 -33
- package/src/resources/renders.bundle.ts +0 -53
- package/src/resources/renders.test.ts +0 -202
- package/src/resources/renders.ts +0 -282
- package/src/resources/test-av-file.txt +0 -1
- package/src/resources/transcriptions.test.ts +0 -49
- package/src/resources/transcriptions.ts +0 -67
- package/src/resources/unprocessed-file.test.ts +0 -171
- package/src/resources/unprocessed-file.ts +0 -138
- package/src/resources/url-token.test.ts +0 -46
- package/src/resources/url-token.ts +0 -27
- package/src/utils/assertTypesMatch.ts +0 -10
- package/src/utils/createReadableStreamFromReadable.ts +0 -115
package/src/resources/renders.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import debug from "debug";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type { Client } from "../client.js";
|
|
4
|
-
import { CompletionIterator } from "../ProgressIterator.js";
|
|
5
|
-
import { assertTypesMatch } from "../utils/assertTypesMatch.ts";
|
|
6
|
-
|
|
7
|
-
const log = debug("ef:api:renders");
|
|
8
|
-
|
|
9
|
-
const H264Configuration = z.object({
|
|
10
|
-
codec: z.literal("h264"),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const AACConfiguration = z.object({
|
|
14
|
-
codec: z.literal("aac"),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const MP4Configuration = z.object({
|
|
18
|
-
container: z.literal("mp4"),
|
|
19
|
-
video: H264Configuration,
|
|
20
|
-
audio: AACConfiguration,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const JpegConfiguration = z.object({
|
|
24
|
-
container: z.literal("jpeg"),
|
|
25
|
-
quality: z.number().int().min(1).max(100).default(80).optional(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const PngConfiguration = z.object({
|
|
29
|
-
container: z.literal("png"),
|
|
30
|
-
compression: z.number().int().min(1).max(100).default(80).optional(),
|
|
31
|
-
transparency: z.boolean().default(false).optional(),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const WebpConfiguration = z.object({
|
|
35
|
-
container: z.literal("webp"),
|
|
36
|
-
quality: z.number().int().min(1).max(100).default(80).optional(),
|
|
37
|
-
compression: z.number().int().min(0).max(6).default(4).optional(),
|
|
38
|
-
transparency: z.boolean().default(false).optional(),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
export const RenderOutputConfiguration = z.discriminatedUnion("container", [
|
|
42
|
-
MP4Configuration,
|
|
43
|
-
JpegConfiguration,
|
|
44
|
-
PngConfiguration,
|
|
45
|
-
WebpConfiguration,
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
export type RenderOutputConfiguration = z.infer<
|
|
49
|
-
typeof RenderOutputConfiguration
|
|
50
|
-
>;
|
|
51
|
-
|
|
52
|
-
export const CreateRenderPayload = z.object({
|
|
53
|
-
md5: z.string().optional(),
|
|
54
|
-
fps: z.number().int().min(1).max(120).default(30).optional(),
|
|
55
|
-
width: z.number().int().min(2).optional(),
|
|
56
|
-
height: z.number().int().min(2).optional(),
|
|
57
|
-
work_slice_ms: z
|
|
58
|
-
.number()
|
|
59
|
-
.int()
|
|
60
|
-
.min(1000)
|
|
61
|
-
.max(10_000)
|
|
62
|
-
.default(4_000)
|
|
63
|
-
.optional(),
|
|
64
|
-
html: z.string().optional(),
|
|
65
|
-
metadata: z.record(z.string(), z.string()).optional(),
|
|
66
|
-
duration_ms: z.number().int().optional(),
|
|
67
|
-
strategy: z.enum(["v1"]).default("v1").optional(),
|
|
68
|
-
output: RenderOutputConfiguration.default({
|
|
69
|
-
container: "mp4",
|
|
70
|
-
video: {
|
|
71
|
-
codec: "h264",
|
|
72
|
-
},
|
|
73
|
-
audio: {
|
|
74
|
-
codec: "aac",
|
|
75
|
-
},
|
|
76
|
-
}).optional(),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
export const CreateRenderPayloadWithOutput = CreateRenderPayload.extend({
|
|
80
|
-
output: RenderOutputConfiguration,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
export class OutputConfiguration {
|
|
84
|
-
static parse(input?: any) {
|
|
85
|
-
const output = RenderOutputConfiguration.parse(
|
|
86
|
-
input ?? {
|
|
87
|
-
container: "mp4",
|
|
88
|
-
video: {
|
|
89
|
-
codec: "h264",
|
|
90
|
-
},
|
|
91
|
-
audio: {
|
|
92
|
-
codec: "aac",
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
return new OutputConfiguration(output);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
constructor(public readonly output: RenderOutputConfiguration) {}
|
|
100
|
-
|
|
101
|
-
get isStill() {
|
|
102
|
-
return (
|
|
103
|
-
this.output.container === "jpeg" ||
|
|
104
|
-
this.output.container === "png" ||
|
|
105
|
-
this.output.container === "webp"
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get isVideo() {
|
|
110
|
-
return this.output.container === "mp4";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get fileExtension() {
|
|
114
|
-
return this.output.container;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
get contentType() {
|
|
118
|
-
if (this.isStill) {
|
|
119
|
-
return `image/${this.fileExtension}`;
|
|
120
|
-
}
|
|
121
|
-
return `video/${this.fileExtension}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
get container() {
|
|
125
|
-
return this.output.container;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
get jpegConfig() {
|
|
129
|
-
return this.output.container === "jpeg" ? this.output : null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
get pngConfig() {
|
|
133
|
-
return this.output.container === "png" ? this.output : null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
get webpConfig() {
|
|
137
|
-
return this.output.container === "webp" ? this.output : null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
get mp4Config() {
|
|
141
|
-
return this.output.container === "mp4" ? this.output : null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface CreateRenderPayload {
|
|
146
|
-
md5?: string;
|
|
147
|
-
fps?: number;
|
|
148
|
-
width?: number;
|
|
149
|
-
height?: number;
|
|
150
|
-
work_slice_ms?: number;
|
|
151
|
-
html?: string;
|
|
152
|
-
duration_ms?: number;
|
|
153
|
-
metadata?: Record<string, string>;
|
|
154
|
-
strategy?: "v1";
|
|
155
|
-
output?: z.infer<typeof RenderOutputConfiguration>;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
assertTypesMatch<CreateRenderPayload, z.infer<typeof CreateRenderPayload>>(
|
|
159
|
-
true,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
export interface CreateRenderResult {
|
|
163
|
-
id: string;
|
|
164
|
-
md5: string | null;
|
|
165
|
-
status: "complete" | "created" | "failed" | "pending" | "rendering" | string;
|
|
166
|
-
metadata: Record<string, string>;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface LookupRenderByMd5Result {
|
|
170
|
-
id: string;
|
|
171
|
-
md5: string | null;
|
|
172
|
-
status: "complete" | "created" | "failed" | "pending" | "rendering" | string;
|
|
173
|
-
metadata: Record<string, string>;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const createRender = async (
|
|
177
|
-
client: Client,
|
|
178
|
-
payload: CreateRenderPayload,
|
|
179
|
-
) => {
|
|
180
|
-
log("Creating render", payload);
|
|
181
|
-
// FIXME: The order of optional/default matters in zod
|
|
182
|
-
// And if we set the default last, the type is not inferred correctly
|
|
183
|
-
// Manually applying defaults here is a hack
|
|
184
|
-
payload.strategy ??= "v1";
|
|
185
|
-
payload.work_slice_ms ??= 4_000;
|
|
186
|
-
payload.output ??= {
|
|
187
|
-
container: "mp4",
|
|
188
|
-
video: {
|
|
189
|
-
codec: "h264",
|
|
190
|
-
},
|
|
191
|
-
audio: {
|
|
192
|
-
codec: "aac",
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const response = await client.authenticatedFetch("/api/v1/renders", {
|
|
197
|
-
method: "POST",
|
|
198
|
-
body: JSON.stringify(payload),
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
log("Render created", response);
|
|
202
|
-
if (response.ok) {
|
|
203
|
-
return (await response.json()) as CreateRenderResult;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
throw new Error(
|
|
207
|
-
`Failed to create render ${response.status} ${response.statusText} ${await response.text()}`,
|
|
208
|
-
);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
export const uploadRender = async (
|
|
212
|
-
client: Client,
|
|
213
|
-
renderId: string,
|
|
214
|
-
fileStream: ReadableStream,
|
|
215
|
-
) => {
|
|
216
|
-
log("Uploading render", renderId);
|
|
217
|
-
const response = await client.authenticatedFetch(
|
|
218
|
-
`/api/v1/renders/${renderId}/upload`,
|
|
219
|
-
{
|
|
220
|
-
method: "POST",
|
|
221
|
-
body: fileStream,
|
|
222
|
-
duplex: "half",
|
|
223
|
-
},
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
if (response.ok) {
|
|
227
|
-
return response.json();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
throw new Error(
|
|
231
|
-
`Failed to upload render ${response.status} ${response.statusText}`,
|
|
232
|
-
);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
export const getRenderInfo = async (client: Client, id: string) => {
|
|
236
|
-
const response = await client.authenticatedFetch(`/api/v1/renders/${id}`);
|
|
237
|
-
return response.json() as Promise<LookupRenderByMd5Result>;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
export const lookupRenderByMd5 = async (
|
|
241
|
-
client: Client,
|
|
242
|
-
md5: string,
|
|
243
|
-
): Promise<LookupRenderByMd5Result | null> => {
|
|
244
|
-
const response = await client.authenticatedFetch(
|
|
245
|
-
`/api/v1/renders/md5/${md5}`,
|
|
246
|
-
{
|
|
247
|
-
method: "GET",
|
|
248
|
-
},
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
if (response.ok) {
|
|
252
|
-
return (await response.json()) as LookupRenderByMd5Result;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (response.status === 404) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
throw new Error(
|
|
260
|
-
`Failed to lookup render by md5 ${md5} ${response.status} ${response.statusText}`,
|
|
261
|
-
);
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
export const getRenderProgress = async (client: Client, id: string) => {
|
|
265
|
-
const eventSource = await client.authenticatedEventSource(
|
|
266
|
-
`/api/v1/renders/${id}/progress`,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
return new CompletionIterator(eventSource);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
export const downloadRender = async (client: Client, id: string) => {
|
|
273
|
-
const response = await client.authenticatedFetch(`/api/v1/renders/${id}.mp4`);
|
|
274
|
-
|
|
275
|
-
if (response.ok) {
|
|
276
|
-
return response;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
throw new Error(
|
|
280
|
-
`Failed to download render ${id} ${response.status} ${response.statusText}`,
|
|
281
|
-
);
|
|
282
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
test
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { HttpResponse, http } from "msw";
|
|
2
|
-
import { setupServer } from "msw/node";
|
|
3
|
-
import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { Client } from "../client.js";
|
|
6
|
-
import { createTranscription } from "./transcriptions.js";
|
|
7
|
-
|
|
8
|
-
const server = setupServer();
|
|
9
|
-
const client = new Client("ef_TEST_TOKEN", "http://localhost");
|
|
10
|
-
|
|
11
|
-
describe("Transcriptions", () => {
|
|
12
|
-
beforeAll(() => server.listen());
|
|
13
|
-
afterEach(() => server.resetHandlers());
|
|
14
|
-
afterAll(() => server.close());
|
|
15
|
-
|
|
16
|
-
describe("createTranscription", () => {
|
|
17
|
-
test("throws if server returns an error", async () => {
|
|
18
|
-
server.use(
|
|
19
|
-
http.post("http://localhost/api/v1/transcriptions", () =>
|
|
20
|
-
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
21
|
-
),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
await expect(
|
|
25
|
-
createTranscription(client, { file_id: "test", track_id: 1 }),
|
|
26
|
-
).rejects.toThrowError(
|
|
27
|
-
"Failed to create transcription 500 Internal Server Error",
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("returns json data from the http response", async () => {
|
|
32
|
-
server.use(
|
|
33
|
-
http.post("http://localhost/api/v1/transcriptions", () =>
|
|
34
|
-
HttpResponse.json(
|
|
35
|
-
{ testResponse: "test" },
|
|
36
|
-
{ status: 200, statusText: "OK" },
|
|
37
|
-
),
|
|
38
|
-
),
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const response = await createTranscription(client, {
|
|
42
|
-
file_id: "test",
|
|
43
|
-
track_id: 1,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(response).toEqual({ testResponse: "test" });
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import debug from "debug";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type { Client } from "../client.js";
|
|
4
|
-
import { CompletionIterator } from "../ProgressIterator.js";
|
|
5
|
-
|
|
6
|
-
const log = debug("ef:api:transcriptions");
|
|
7
|
-
|
|
8
|
-
export const CreateTranscriptionPayload = z.object({
|
|
9
|
-
file_id: z.string(),
|
|
10
|
-
track_id: z.number().int(),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export type CreateTranscriptionPayload = z.infer<
|
|
14
|
-
typeof CreateTranscriptionPayload
|
|
15
|
-
>;
|
|
16
|
-
|
|
17
|
-
export interface CreateTranscriptionResult {
|
|
18
|
-
id: string;
|
|
19
|
-
status: "complete" | "created" | "failed" | "pending" | "transcribing";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface TranscriptionInfoResult {
|
|
23
|
-
id: string;
|
|
24
|
-
status: "complete" | "created" | "failed" | "pending" | "transcribing";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const createTranscription = async (
|
|
28
|
-
client: Client,
|
|
29
|
-
payload: CreateTranscriptionPayload,
|
|
30
|
-
) => {
|
|
31
|
-
log("Creating transcription", payload);
|
|
32
|
-
const response = await client.authenticatedFetch("/api/v1/transcriptions", {
|
|
33
|
-
method: "POST",
|
|
34
|
-
body: JSON.stringify(payload),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
log("Transcription created", response);
|
|
38
|
-
if (response.ok) {
|
|
39
|
-
return (await response.json()) as CreateTranscriptionResult;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Failed to create transcription ${response.status} ${response.statusText}`,
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const getTranscriptionProgress = async (client: Client, id: string) => {
|
|
48
|
-
const eventSource = await client.authenticatedEventSource(
|
|
49
|
-
`/api/v1/transcriptions/${id}/progress`,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return new CompletionIterator(eventSource);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const getTranscriptionInfo = async (client: Client, id: string) => {
|
|
56
|
-
const response = await client.authenticatedFetch(
|
|
57
|
-
`/api/v1/transcriptions/${id}`,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (response.ok) {
|
|
61
|
-
return (await response.json()) as TranscriptionInfoResult;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Failed to get transcription info ${response.status} ${response.statusText}`,
|
|
66
|
-
);
|
|
67
|
-
};
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { HttpResponse, http } from "msw";
|
|
2
|
-
import { setupServer } from "msw/node";
|
|
3
|
-
import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { Client } from "../client.js";
|
|
6
|
-
import { webReadableFromBuffers } from "../readableFromBuffers.js";
|
|
7
|
-
import {
|
|
8
|
-
createUnprocessedFile,
|
|
9
|
-
lookupUnprocessedFileByMd5,
|
|
10
|
-
uploadUnprocessedReadableStream,
|
|
11
|
-
} from "./unprocessed-file.js";
|
|
12
|
-
|
|
13
|
-
const server = setupServer();
|
|
14
|
-
const client = new Client("ef_TEST_TOKEN", "http://localhost");
|
|
15
|
-
|
|
16
|
-
const UploadMustContinue = (id = "test-file") =>
|
|
17
|
-
http.get(`http://localhost/api/v1/unprocessed_files/${id}/upload`, () =>
|
|
18
|
-
HttpResponse.json({}, { status: 202 }),
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
describe("Unprocessed File", () => {
|
|
22
|
-
beforeAll(() => server.listen());
|
|
23
|
-
afterEach(() => server.resetHandlers());
|
|
24
|
-
afterAll(() => server.close());
|
|
25
|
-
|
|
26
|
-
describe("createUnprocessedFile", () => {
|
|
27
|
-
test("Throws when file is too large", async () => {
|
|
28
|
-
await expect(
|
|
29
|
-
createUnprocessedFile(client, {
|
|
30
|
-
md5: "test-file",
|
|
31
|
-
filename: "test-file",
|
|
32
|
-
byte_size: 1024 * 1024 * 1025,
|
|
33
|
-
}),
|
|
34
|
-
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
35
|
-
[ZodError: [
|
|
36
|
-
{
|
|
37
|
-
"code": "too_big",
|
|
38
|
-
"maximum": 1073741824,
|
|
39
|
-
"type": "number",
|
|
40
|
-
"inclusive": true,
|
|
41
|
-
"exact": false,
|
|
42
|
-
"message": "Number must be less than or equal to 1073741824",
|
|
43
|
-
"path": [
|
|
44
|
-
"byte_size"
|
|
45
|
-
]
|
|
46
|
-
}
|
|
47
|
-
]]
|
|
48
|
-
`);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("Throws when server returns an error", async () => {
|
|
52
|
-
server.use(
|
|
53
|
-
http.post("http://localhost/api/v1/unprocessed_files", () =>
|
|
54
|
-
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
55
|
-
),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
await expect(
|
|
59
|
-
createUnprocessedFile(client, {
|
|
60
|
-
md5: "test-file",
|
|
61
|
-
filename: "test-file",
|
|
62
|
-
byte_size: 1024 * 1024,
|
|
63
|
-
}),
|
|
64
|
-
).rejects.toThrowError(
|
|
65
|
-
"Failed to create unprocessed file 500 Internal Server Error",
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("Returns json data from the http response", async () => {
|
|
70
|
-
server.use(
|
|
71
|
-
http.post("http://localhost/api/v1/unprocessed_files", () =>
|
|
72
|
-
HttpResponse.json(
|
|
73
|
-
{ testResponse: "test" },
|
|
74
|
-
{ status: 200, statusText: "OK" },
|
|
75
|
-
),
|
|
76
|
-
),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const result = await createUnprocessedFile(client, {
|
|
80
|
-
md5: "test-file",
|
|
81
|
-
filename: "test-file",
|
|
82
|
-
byte_size: 1024 * 1024,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
expect(result).toEqual({ testResponse: "test" });
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("uploadUnprocessedFile", () => {
|
|
90
|
-
test("Throws when server responds with an error", async () => {
|
|
91
|
-
server.use(
|
|
92
|
-
UploadMustContinue(),
|
|
93
|
-
http.post(
|
|
94
|
-
"http://localhost/api/v1/unprocessed_files/test-file/upload",
|
|
95
|
-
() => HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
await expect(
|
|
100
|
-
uploadUnprocessedReadableStream(
|
|
101
|
-
client,
|
|
102
|
-
{ id: "test-file", byte_size: 4 },
|
|
103
|
-
webReadableFromBuffers(Buffer.from("test")),
|
|
104
|
-
).whenUploaded(),
|
|
105
|
-
).rejects.toThrowError(
|
|
106
|
-
"Failed to upload chunk 0 for /api/v1/unprocessed_files/test-file/upload 500 Internal Server Error",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("Succeeds when server returns a success", async () => {
|
|
111
|
-
server.use(
|
|
112
|
-
UploadMustContinue(),
|
|
113
|
-
http.post(
|
|
114
|
-
"http://localhost/api/v1/unprocessed_files/test-file/upload",
|
|
115
|
-
() => HttpResponse.json({}, { status: 201 }),
|
|
116
|
-
),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
await expect(
|
|
120
|
-
uploadUnprocessedReadableStream(
|
|
121
|
-
client,
|
|
122
|
-
{ id: "test-file", byte_size: 4 },
|
|
123
|
-
webReadableFromBuffers(Buffer.from("test")),
|
|
124
|
-
).whenUploaded(),
|
|
125
|
-
).resolves.toEqual([
|
|
126
|
-
{ type: "progress", progress: 0 },
|
|
127
|
-
{ type: "progress", progress: 1 },
|
|
128
|
-
]);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("lookupUnprocessedFileByMd5", () => {
|
|
133
|
-
test("Returns json data from the http response", async () => {
|
|
134
|
-
server.use(
|
|
135
|
-
http.get("http://localhost/api/v1/unprocessed_files/md5/test-md5", () =>
|
|
136
|
-
HttpResponse.json({ test: "response" }, { status: 200 }),
|
|
137
|
-
),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const response = await lookupUnprocessedFileByMd5(client, "test-md5");
|
|
141
|
-
|
|
142
|
-
expect(response).toEqual({ test: "response" });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("Returns null when file is not found", async () => {
|
|
146
|
-
server.use(
|
|
147
|
-
http.get("http://localhost/api/v1/unprocessed_files/md5/test-md5", () =>
|
|
148
|
-
HttpResponse.json({}, { status: 404 }),
|
|
149
|
-
),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const response = await lookupUnprocessedFileByMd5(client, "test-md5");
|
|
153
|
-
|
|
154
|
-
expect(response).toBeNull();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("Throws when server returns an error", async () => {
|
|
158
|
-
server.use(
|
|
159
|
-
http.get("http://localhost/api/v1/unprocessed_files/md5/test-md5", () =>
|
|
160
|
-
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
await expect(
|
|
165
|
-
lookupUnprocessedFileByMd5(client, "test-md5"),
|
|
166
|
-
).rejects.toThrowError(
|
|
167
|
-
"Failed to lookup unprocessed file by md5 test-md5 500 Internal Server Error",
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import debug from "debug";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import type { Client } from "../client.js";
|
|
5
|
-
import { uploadChunks } from "../uploadChunks.js";
|
|
6
|
-
import { assertTypesMatch } from "../utils/assertTypesMatch.ts";
|
|
7
|
-
|
|
8
|
-
const log = debug("ef:api:unprocessed-file");
|
|
9
|
-
|
|
10
|
-
const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GiB
|
|
11
|
-
|
|
12
|
-
export const CreateUnprocessedFilePayload = z.object({
|
|
13
|
-
md5: z.string(),
|
|
14
|
-
filename: z.string(),
|
|
15
|
-
byte_size: z.number().int().max(MAX_FILE_SIZE),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const UpdateUnprocessedFilePayload = z.object({});
|
|
19
|
-
|
|
20
|
-
export type CreateUnprocessedFilePayload = z.infer<
|
|
21
|
-
typeof CreateUnprocessedFilePayload
|
|
22
|
-
>;
|
|
23
|
-
|
|
24
|
-
export interface UnprocessedFile {
|
|
25
|
-
byte_size: number;
|
|
26
|
-
next_byte: number;
|
|
27
|
-
complete: boolean;
|
|
28
|
-
id: string;
|
|
29
|
-
md5: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface UnprocessedFileUploadDetails {
|
|
33
|
-
id: string;
|
|
34
|
-
byte_size: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Ensure that the UnprocessedFileUploadDetails type matches the shape of the
|
|
38
|
-
// UnprocessedFile type, but without the optional fields.
|
|
39
|
-
assertTypesMatch<
|
|
40
|
-
Pick<UnprocessedFile, "id" | "byte_size">,
|
|
41
|
-
UnprocessedFileUploadDetails
|
|
42
|
-
>(true);
|
|
43
|
-
|
|
44
|
-
export interface CreateUnprocessedFileResult extends UnprocessedFile {}
|
|
45
|
-
|
|
46
|
-
export interface LookupUnprocessedFileByMd5Result extends UnprocessedFile {}
|
|
47
|
-
|
|
48
|
-
export interface UpdateUnprocessedFileResult extends UnprocessedFile {}
|
|
49
|
-
|
|
50
|
-
export interface ProcessIsobmffFileResult {
|
|
51
|
-
id: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const createUnprocessedFile = async (
|
|
55
|
-
client: Client,
|
|
56
|
-
payload: CreateUnprocessedFilePayload,
|
|
57
|
-
) => {
|
|
58
|
-
log("Creating an unprocessed file", payload);
|
|
59
|
-
CreateUnprocessedFilePayload.parse(payload);
|
|
60
|
-
const response = await client.authenticatedFetch(
|
|
61
|
-
"/api/v1/unprocessed_files",
|
|
62
|
-
{
|
|
63
|
-
method: "POST",
|
|
64
|
-
body: JSON.stringify(payload),
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
log(
|
|
69
|
-
"Unprocessed file created",
|
|
70
|
-
response.status,
|
|
71
|
-
response.statusText,
|
|
72
|
-
response.headers,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (response.ok) {
|
|
76
|
-
return (await response.json()) as CreateUnprocessedFileResult;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Failed to create unprocessed file ${response.status} ${response.statusText}`,
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const uploadUnprocessedReadableStream = (
|
|
85
|
-
client: Client,
|
|
86
|
-
uploadDetails: UnprocessedFileUploadDetails,
|
|
87
|
-
fileStream: ReadableStream,
|
|
88
|
-
) => {
|
|
89
|
-
log("Uploading unprocessed file", uploadDetails.id);
|
|
90
|
-
|
|
91
|
-
return uploadChunks(client, {
|
|
92
|
-
url: `/api/v1/unprocessed_files/${uploadDetails.id}/upload`,
|
|
93
|
-
fileSize: uploadDetails.byte_size,
|
|
94
|
-
fileStream,
|
|
95
|
-
maxSize: MAX_FILE_SIZE,
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const lookupUnprocessedFileByMd5 = async (
|
|
100
|
-
client: Client,
|
|
101
|
-
md5: string,
|
|
102
|
-
): Promise<LookupUnprocessedFileByMd5Result | null> => {
|
|
103
|
-
const response = await client.authenticatedFetch(
|
|
104
|
-
`/api/v1/unprocessed_files/md5/${md5}`,
|
|
105
|
-
{
|
|
106
|
-
method: "GET",
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
if (response.ok) {
|
|
111
|
-
return (await response.json()) as LookupUnprocessedFileByMd5Result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (response.status === 404) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Failed to lookup unprocessed file by md5 ${md5} ${response.status} ${response.statusText}`,
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export const processIsobmffFile = async (client: Client, id: string) => {
|
|
124
|
-
const response = await client.authenticatedFetch(
|
|
125
|
-
`/api/v1/unprocessed_files/${id}/isobmff`,
|
|
126
|
-
{
|
|
127
|
-
method: "POST",
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (response.ok) {
|
|
132
|
-
return (await response.json()) as ProcessIsobmffFileResult;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Failed to process isobmff file ${id} ${response.status} ${response.statusText}`,
|
|
137
|
-
);
|
|
138
|
-
};
|