@freqhole/api-client 0.3.6 → 0.3.8
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/package.json +1 -1
- package/src/WasmTransport.test.ts +106 -0
- package/src/WasmTransport.ts +139 -10
- package/src/codegen/routes.ts +6 -0
- package/src/codegen/schema.ts +110 -8
- package/src/domains/app.ts +89 -8
- package/src/domains/video.ts +12 -0
- package/src/index.ts +1 -0
- package/src/transport.ts +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tests for WasmTransport.upload()'s iroh-blobs chunked path - added while
|
|
2
|
+
// replacing a whole-file `file.arrayBuffer()` buffer-then-import call with
|
|
3
|
+
// genuine chunked streaming into `start_import()`'s ImportSession (no
|
|
4
|
+
// base64 involved at any point here - a direct wasm-bindgen call, unlike
|
|
5
|
+
// the tauri-IPC chunked path in CharnelTransport.ts). covers: chunking
|
|
6
|
+
// across multiple `push()` calls, real per-chunk progress, TempTag release,
|
|
7
|
+
// and the fallback to `import_blob` for older node builds without
|
|
8
|
+
// `start_import`.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { WasmTransport } from "./WasmTransport";
|
|
12
|
+
import type { MiddenNodeLike } from "./WasmTransport";
|
|
13
|
+
|
|
14
|
+
function fakeFile(sizeBytes: number, name = "song.mp3"): File {
|
|
15
|
+
const bytes = new Uint8Array(sizeBytes).fill(7);
|
|
16
|
+
return new File([bytes], name, { type: "audio/mpeg" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fakeApiResponse() {
|
|
20
|
+
return {
|
|
21
|
+
status: 200,
|
|
22
|
+
body: JSON.stringify({ success: true, data: { job_id: "job-1" } }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("WasmTransport.upload() via iroh-blobs (chunked, no base64)", () => {
|
|
27
|
+
it("streams a large file across multiple push() calls instead of one whole-buffer import_blob call", async () => {
|
|
28
|
+
const pushed: number[] = [];
|
|
29
|
+
const session = {
|
|
30
|
+
push: vi.fn(async (chunk: Uint8Array) => {
|
|
31
|
+
pushed.push(chunk.byteLength);
|
|
32
|
+
}),
|
|
33
|
+
finish: vi.fn(async () => "blake3hash"),
|
|
34
|
+
abort: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
const node: Partial<MiddenNodeLike> = {
|
|
37
|
+
node_id: () => "local-node",
|
|
38
|
+
import_blob: vi.fn(async () => "should-not-be-used"),
|
|
39
|
+
start_import: vi.fn(() => session),
|
|
40
|
+
release_blob: vi.fn(),
|
|
41
|
+
api_request: vi.fn(async () => fakeApiResponse()),
|
|
42
|
+
};
|
|
43
|
+
const transport = new WasmTransport(node as MiddenNodeLike, "peer-1");
|
|
44
|
+
|
|
45
|
+
// 4MB chunk size -> a 9MB file should push 3 chunks (4, 4, 1 MB).
|
|
46
|
+
const fileSize = 9 * 1024 * 1024;
|
|
47
|
+
const formData = new FormData();
|
|
48
|
+
formData.set("file", fakeFile(fileSize));
|
|
49
|
+
|
|
50
|
+
const onProgress = vi.fn();
|
|
51
|
+
await transport.upload("/api/upload/music", formData, onProgress);
|
|
52
|
+
|
|
53
|
+
expect(node.import_blob).not.toHaveBeenCalled();
|
|
54
|
+
expect(session.push).toHaveBeenCalledTimes(3);
|
|
55
|
+
expect(pushed.reduce((a, b) => a + b, 0)).toBe(fileSize);
|
|
56
|
+
expect(session.finish).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(node.release_blob).toHaveBeenCalledWith("blake3hash");
|
|
58
|
+
|
|
59
|
+
// progress is real and monotonically increasing, ending at the full size.
|
|
60
|
+
expect(onProgress).toHaveBeenCalled();
|
|
61
|
+
const calls = onProgress.mock.calls.map((c) => c[0] as number);
|
|
62
|
+
expect(calls[calls.length - 1]).toBe(fileSize);
|
|
63
|
+
expect([...calls].sort((a, b) => a - b)).toEqual(calls);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to a single import_blob call when start_import isn't available", async () => {
|
|
67
|
+
const node: Partial<MiddenNodeLike> = {
|
|
68
|
+
node_id: () => "local-node",
|
|
69
|
+
import_blob: vi.fn(async () => "blake3hash"),
|
|
70
|
+
release_blob: vi.fn(),
|
|
71
|
+
api_request: vi.fn(async () => fakeApiResponse()),
|
|
72
|
+
};
|
|
73
|
+
const transport = new WasmTransport(node as MiddenNodeLike, "peer-1");
|
|
74
|
+
|
|
75
|
+
const formData = new FormData();
|
|
76
|
+
formData.set("file", fakeFile(1024));
|
|
77
|
+
await transport.upload("/api/upload/music", formData);
|
|
78
|
+
|
|
79
|
+
expect(node.import_blob).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("aborts the import session and propagates the error if a push() fails", async () => {
|
|
83
|
+
const session = {
|
|
84
|
+
push: vi.fn(async () => {
|
|
85
|
+
throw new Error("push failed");
|
|
86
|
+
}),
|
|
87
|
+
finish: vi.fn(async () => "should-not-be-called"),
|
|
88
|
+
abort: vi.fn(),
|
|
89
|
+
};
|
|
90
|
+
const node: Partial<MiddenNodeLike> = {
|
|
91
|
+
node_id: () => "local-node",
|
|
92
|
+
import_blob: vi.fn(async () => "should-not-be-used"),
|
|
93
|
+
start_import: vi.fn(() => session),
|
|
94
|
+
release_blob: vi.fn(),
|
|
95
|
+
api_request: vi.fn(async () => fakeApiResponse()),
|
|
96
|
+
};
|
|
97
|
+
const transport = new WasmTransport(node as MiddenNodeLike, "peer-1");
|
|
98
|
+
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.set("file", fakeFile(1024));
|
|
101
|
+
|
|
102
|
+
await expect(transport.upload("/api/upload/music", formData)).rejects.toThrow("upload failed");
|
|
103
|
+
expect(session.abort).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(session.finish).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/WasmTransport.ts
CHANGED
|
@@ -97,6 +97,12 @@ export interface MiddenNodeLike {
|
|
|
97
97
|
download_verified?(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
|
|
98
98
|
// download blob with automatic ensure + retry - optional
|
|
99
99
|
download_verified_with_ensure?(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
|
|
100
|
+
// download blob DIRECTLY into this node's own local store, never
|
|
101
|
+
// returning bytes to JS - see
|
|
102
|
+
// `download_verified_to_store_with_ensure`'s doc comment in
|
|
103
|
+
// lib/midden/src/lib.rs. use instead of download_verified_with_ensure
|
|
104
|
+
// whenever the caller only needs the blob to become locally servable.
|
|
105
|
+
download_verified_to_store_with_ensure?(peer_addr: string, blake3_hash: string): Promise<void>;
|
|
100
106
|
// download blob by ID with on-demand blake3 computation - optional
|
|
101
107
|
// returns [Uint8Array, string] but typed as any[] for wasm-bindgen compatibility
|
|
102
108
|
download_verified_by_id?(peer_addr: string, blob_id: string): Promise<any[]>;
|
|
@@ -153,6 +159,35 @@ export interface MiddenNodeLike {
|
|
|
153
159
|
// phase 1. optional because charnel's CharnelTransport has no wasm
|
|
154
160
|
// accept-loop equivalent (yet).
|
|
155
161
|
accept?(): Promise<BiStreamLike | null>;
|
|
162
|
+
// snapshot of this node's own OUTGOING blob transfers currently in
|
|
163
|
+
// flight (this node serving a blob to a peer) - see
|
|
164
|
+
// docs/transfer-unification-plan.md phase 1 (bucket A). does NOT cover
|
|
165
|
+
// incoming/download transfers - iroh-blobs only instruments the serving
|
|
166
|
+
// side; downloads are tracked via the on_progress callbacks the
|
|
167
|
+
// download_verified_* methods above already take.
|
|
168
|
+
get_active_transfers?(): Promise<ActiveTransferLike[]>;
|
|
169
|
+
// pause every in-flight DOWNLOAD of this blake3 hash (no effect on
|
|
170
|
+
// outgoing/serving transfers - only the puller can pace a pull).
|
|
171
|
+
// the partial stays pinned in the local store; resuming just means
|
|
172
|
+
// calling a download_verified_* method again with the same hash.
|
|
173
|
+
// returns how many downloads were flagged (0 = none in flight).
|
|
174
|
+
download_cancel_by_blake3?(blake3_hash: string): Promise<number>;
|
|
175
|
+
// pin a hash against gc (keep a paused partial download alive).
|
|
176
|
+
protect_blob?(blake3_hash: string): Promise<void>;
|
|
177
|
+
// remove a gc pin added by protect_blob or a cancelled download.
|
|
178
|
+
unprotect_blob?(blake3_hash: string): Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* one outgoing blob transfer in flight, as returned by
|
|
183
|
+
* `get_active_transfers()` - see `ActiveTransfer` in reliquary's
|
|
184
|
+
* `gate.rs` / `WorkerActiveTransfer` in reliquary's worker contract.
|
|
185
|
+
*/
|
|
186
|
+
export interface ActiveTransferLike {
|
|
187
|
+
peerId: string;
|
|
188
|
+
blake3: string;
|
|
189
|
+
bytesSent: number;
|
|
190
|
+
totalSize: number;
|
|
156
191
|
}
|
|
157
192
|
|
|
158
193
|
/**
|
|
@@ -392,14 +427,9 @@ export class WasmTransport implements Transport {
|
|
|
392
427
|
async upload(
|
|
393
428
|
path: string,
|
|
394
429
|
formData: FormData,
|
|
395
|
-
|
|
430
|
+
onProgress?: (loaded: number, total: number) => void,
|
|
396
431
|
metadata?: UploadMetadata,
|
|
397
432
|
): Promise<TransportResponse> {
|
|
398
|
-
// no byte-level progress possible here - unlike CharnelTransport's tauri
|
|
399
|
-
// IPC path (which self-chunks and can report per-chunk progress), the
|
|
400
|
-
// midden wasm binding's `import_blob(bytes)` is a single opaque call
|
|
401
|
-
// with no progress callback exposed, and the base64 fallback also sends
|
|
402
|
-
// one JSON request.
|
|
403
433
|
const file = formData.get("file") as File | null;
|
|
404
434
|
if (!file) {
|
|
405
435
|
return {
|
|
@@ -422,7 +452,7 @@ export class WasmTransport implements Transport {
|
|
|
422
452
|
// available - chunked/verified streaming, no base64/raw-bytes framing.
|
|
423
453
|
const blobPullPaths = ["/api/upload/music", "/api/upload/video"];
|
|
424
454
|
if (blobPullPaths.includes(path) && this.node.import_blob) {
|
|
425
|
-
return this.uploadViaIrohBlobs(path, file, metadata);
|
|
455
|
+
return this.uploadViaIrohBlobs(path, file, onProgress, metadata);
|
|
426
456
|
}
|
|
427
457
|
|
|
428
458
|
// fallback: base64 encode and send via api_request (works for image uploads)
|
|
@@ -435,21 +465,55 @@ export class WasmTransport implements Transport {
|
|
|
435
465
|
* 2. tell remote peer the hash via `${path}-by-blake3` (e.g. /api/upload/music-by-blake3)
|
|
436
466
|
* 3. remote peer pulls the blob from us via iroh-blobs verified streaming
|
|
437
467
|
* 4. release the TempTag so local GC can reclaim the blob
|
|
468
|
+
*
|
|
469
|
+
* streams the file into `start_import()`'s chunked `ImportSession` in
|
|
470
|
+
* bounded slices (mirrors `CharnelTransport.uploadMediaViaBytes`'s same
|
|
471
|
+
* chunk size) instead of reading the whole file into one buffer via
|
|
472
|
+
* `file.arrayBuffer()` - this is a direct wasm-bindgen call (`push`
|
|
473
|
+
* takes a `Uint8Array` straight through, no JSON/IPC boundary to cross),
|
|
474
|
+
* so there's no base64 involved at any point, unlike the tauri-IPC
|
|
475
|
+
* chunked path. this also yields real per-chunk progress, previously
|
|
476
|
+
* unavailable here (a single opaque `import_blob(wholeBytes)` call had
|
|
477
|
+
* nothing to report between start and finish). falls back to the old
|
|
478
|
+
* whole-buffer `import_blob` call only if this node build predates
|
|
479
|
+
* `start_import` (progress-less in that case, matching prior behavior).
|
|
438
480
|
*/
|
|
439
481
|
private async uploadViaIrohBlobs(
|
|
440
482
|
path: string,
|
|
441
483
|
file: File,
|
|
484
|
+
onProgress?: (loaded: number, total: number) => void,
|
|
442
485
|
metadata?: UploadMetadata,
|
|
443
486
|
): Promise<TransportResponse> {
|
|
487
|
+
// ~4MB per chunk - same size CharnelTransport's tauri IPC chunking
|
|
488
|
+
// uses, though the reason differs (there it bounds IPC payload size;
|
|
489
|
+
// here it just bounds how much of the file is held at once).
|
|
490
|
+
const CHUNK_SIZE = 4 * 1024 * 1024;
|
|
444
491
|
try {
|
|
445
|
-
const
|
|
446
|
-
|
|
492
|
+
const hash = await (async () => {
|
|
493
|
+
if (!this.node.start_import) {
|
|
494
|
+
// old node build - no chunked import session available.
|
|
495
|
+
return this.node.import_blob!(new Uint8Array(await file.arrayBuffer()));
|
|
496
|
+
}
|
|
497
|
+
const session = this.node.start_import();
|
|
498
|
+
try {
|
|
499
|
+
for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
|
|
500
|
+
const slice = file.slice(offset, Math.min(offset + CHUNK_SIZE, file.size));
|
|
501
|
+
const chunk = new Uint8Array(await slice.arrayBuffer());
|
|
502
|
+
await session.push(chunk);
|
|
503
|
+
onProgress?.(Math.min(offset + chunk.byteLength, file.size), file.size);
|
|
504
|
+
}
|
|
505
|
+
return await session.finish();
|
|
506
|
+
} catch (err) {
|
|
507
|
+
session.abort();
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
})();
|
|
447
511
|
|
|
448
512
|
try {
|
|
449
513
|
const body: Record<string, unknown> = {
|
|
450
514
|
blake3: hash,
|
|
451
515
|
filename: file.name,
|
|
452
|
-
size:
|
|
516
|
+
size: file.size,
|
|
453
517
|
...metadata,
|
|
454
518
|
};
|
|
455
519
|
|
|
@@ -975,6 +1039,71 @@ export class WasmTransport implements Transport {
|
|
|
975
1039
|
return url;
|
|
976
1040
|
}
|
|
977
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* stream a blob's bytes straight to a caller-supplied sink, without ever
|
|
1044
|
+
* assembling the whole blob in one buffer here - see the `Transport`
|
|
1045
|
+
* interface doc comment for why this exists (docs/blob-transfer-opfs-
|
|
1046
|
+
* and-sha256-refactor-plan.md phase 2). only the verified iroh-blobs
|
|
1047
|
+
* path (blake3 required) is supported - callers without a blake3 should
|
|
1048
|
+
* use `getBlobUrlWithProgress`/`fetchBlob` instead, same as before.
|
|
1049
|
+
*/
|
|
1050
|
+
async streamBlobToSink(
|
|
1051
|
+
blobId: string,
|
|
1052
|
+
onChunk: (chunk: Uint8Array, offset: number) => void | Promise<void>,
|
|
1053
|
+
onProgress: (received: number, total: number) => void,
|
|
1054
|
+
blake3?: string,
|
|
1055
|
+
totalBytes?: number,
|
|
1056
|
+
mimeType?: string,
|
|
1057
|
+
): Promise<{ contentType: string; totalBytes: number }> {
|
|
1058
|
+
if (!blake3) {
|
|
1059
|
+
throw new TransportError(
|
|
1060
|
+
`streamBlobToSink requires a blake3 hash for verified streaming (blob ${blobId})`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const streamingFn =
|
|
1064
|
+
this.node.download_verified_streaming_with_ensure ?? this.node.download_verified_streaming;
|
|
1065
|
+
if (!streamingFn) {
|
|
1066
|
+
throw new TransportError(
|
|
1067
|
+
`streamBlobToSink: node has no streaming download method (blob ${blobId})`,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
let totalReceived = 0;
|
|
1072
|
+
// chunk delivery is fire-and-forget from the wasm side (see
|
|
1073
|
+
// download_verified_streaming_with_ensure's own doc comment) - queue
|
|
1074
|
+
// each onChunk call after the previous one finishes so a slow sink
|
|
1075
|
+
// (e.g. an OPFS write) applies real backpressure instead of racing
|
|
1076
|
+
// ahead of disk writes.
|
|
1077
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
1078
|
+
|
|
1079
|
+
await streamingFn.call(
|
|
1080
|
+
this.node,
|
|
1081
|
+
this.peerAddr,
|
|
1082
|
+
blake3,
|
|
1083
|
+
totalBytes ?? 0,
|
|
1084
|
+
(chunk: Uint8Array, offset: number) => {
|
|
1085
|
+
// chunk is a wasm-owned Uint8Array view - copy to detach before
|
|
1086
|
+
// it's handed off across the write chain / a later tick.
|
|
1087
|
+
const owned = new Uint8Array(chunk.length);
|
|
1088
|
+
owned.set(chunk);
|
|
1089
|
+
totalReceived += owned.length;
|
|
1090
|
+
writeChain = writeChain.then(() => onChunk(owned, offset));
|
|
1091
|
+
if (!totalBytes || totalBytes <= 0) {
|
|
1092
|
+
onProgress(totalReceived, totalReceived);
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
(fraction: number) => {
|
|
1096
|
+
if (totalBytes && totalBytes > 0) {
|
|
1097
|
+
const received = Math.min(totalBytes, Math.floor(fraction * totalBytes));
|
|
1098
|
+
onProgress(received, totalBytes);
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
await writeChain;
|
|
1103
|
+
|
|
1104
|
+
return { contentType: mimeType ?? "application/octet-stream", totalBytes: totalReceived };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
978
1107
|
/**
|
|
979
1108
|
* revoke a blob URL to free memory
|
|
980
1109
|
*/
|
package/src/codegen/routes.ts
CHANGED
|
@@ -34,12 +34,17 @@ export const routes = {
|
|
|
34
34
|
},
|
|
35
35
|
app: {
|
|
36
36
|
health_check: { method: 'GET', path: '/health', req: null, resp: s.HealthResponseSchema, auth: { type: 'public' } as const },
|
|
37
|
+
radio_clear_requests: { method: 'POST', path: '/api/radio/requests/clear', req: s.RadioStationRequestsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
37
38
|
radio_info: { method: 'GET', path: '/api/radio/info', req: null, resp: s.RadioInfoResponseSchema, auth: { type: 'public' } as const },
|
|
39
|
+
radio_list_requests: { method: 'POST', path: '/api/radio/requests/list', req: s.RadioStationRequestsRequestSchema, resp: s.RadioRequestsListResponseSchema, auth: { type: 'authenticated' } as const },
|
|
38
40
|
radio_public_blob: { method: 'GET', path: '/api/radio/stations/{station_id}/blobs/{blob_id}', req: null, resp: null, auth: { type: 'public' } as const },
|
|
39
41
|
radio_public_blob_data: { method: 'GET', path: '/api/radio/stations/{station_id}/blobs/{blob_id}/data', req: null, resp: null, auth: { type: 'public' } as const },
|
|
40
42
|
radio_public_blob_thumbnail: { method: 'GET', path: '/api/radio/stations/{station_id}/blobs/{blob_id}/thumb/{size}', req: null, resp: null, auth: { type: 'public' } as const },
|
|
41
43
|
radio_public_timeline: { method: 'GET', path: '/api/radio/stations/{station_id}/timeline', req: null, resp: s.PublicTimelineManifestSchema, auth: { type: 'public' } as const },
|
|
44
|
+
radio_remove_request: { method: 'POST', path: '/api/radio/requests/remove', req: s.RemoveRadioRequestRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
42
45
|
radio_stations: { method: 'GET', path: '/api/radio/stations', req: null, resp: s.RadioStationsResponseSchema, auth: { type: 'public' } as const },
|
|
46
|
+
radio_stations_full: { method: 'GET', path: '/api/radio/stations/full', req: null, resp: s.RadioStationsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
47
|
+
radio_submit_request: { method: 'POST', path: '/api/radio/requests/submit', req: s.SubmitRadioRequestRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
43
48
|
server_info: { method: 'GET', path: '/api/hello', req: null, resp: s.ServerInfoResponseSchema, auth: { type: 'public' } as const },
|
|
44
49
|
},
|
|
45
50
|
auth: {
|
|
@@ -252,6 +257,7 @@ export const routes = {
|
|
|
252
257
|
import_video_paths: { method: 'POST', path: '/api/upload/video-paths', req: null, resp: s.VideoImportResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
253
258
|
list_pending_video_import_review: { method: 'POST', path: '/api/video/import/pending', req: s.ListPendingVideoReviewRequestSchema, resp: s.PendingVideoReviewSessionSchema.array(), auth: { type: 'authenticated' } as const },
|
|
254
259
|
list_playback_progress: { method: 'POST', path: '/api/video/progress/list', req: s.ListPlaybackProgressRequestSchema, resp: s.PlaybackProgressSchema.array(), auth: { type: 'authenticated' } as const },
|
|
260
|
+
list_video_extras: { method: 'POST', path: '/api/video/videos/list-extras', req: s.ListVideoExtrasRequestSchema, resp: s.VideoSchema.array(), auth: { type: 'authenticated' } as const },
|
|
255
261
|
list_video_seasons: { method: 'POST', path: '/api/video/seasons/list', req: s.ListVideoSeasonsRequestSchema, resp: s.VideoSeasonSchema.array(), auth: { type: 'authenticated' } as const },
|
|
256
262
|
list_video_series: { method: 'POST', path: '/api/video/series/list', req: s.ListVideoSeriesRequestSchema, resp: s.VideoSeriesSchema.array(), auth: { type: 'authenticated' } as const },
|
|
257
263
|
list_videos_by_season: { method: 'POST', path: '/api/video/videos/list-by-season', req: s.ListVideosBySeasonRequestSchema, resp: s.VideoSchema.array(), auth: { type: 'authenticated' } as const },
|
package/src/codegen/schema.ts
CHANGED
|
@@ -1508,7 +1508,9 @@ export const CreateStationRequestSchema = z.object({
|
|
|
1508
1508
|
encode_args: z.string().nullish(),
|
|
1509
1509
|
codec: z.string().nullish(),
|
|
1510
1510
|
play_mode: z.string().nullish(),
|
|
1511
|
-
timeline_only_mode: z.boolean().nullish()
|
|
1511
|
+
timeline_only_mode: z.boolean().nullish(),
|
|
1512
|
+
content_mode: z.string().nullish(),
|
|
1513
|
+
accepts_requests: z.boolean().nullish()
|
|
1512
1514
|
});
|
|
1513
1515
|
export type CreateStationRequest = z.infer<typeof CreateStationRequestSchema>;
|
|
1514
1516
|
|
|
@@ -1545,6 +1547,7 @@ export const CreateVideoRequestSchema = z.object({
|
|
|
1545
1547
|
title: z.string(),
|
|
1546
1548
|
description: z.string().nullish(),
|
|
1547
1549
|
media_blob_id: z.string(),
|
|
1550
|
+
parent_video_id: z.string().nullish(),
|
|
1548
1551
|
poster_blob_id: z.string().nullish(),
|
|
1549
1552
|
duration_seconds: z.number().nullish(),
|
|
1550
1553
|
release_date: z.string().nullish(),
|
|
@@ -2394,6 +2397,8 @@ export const FavoriteItemSchema = z.union([z.intersection(z.object({
|
|
|
2394
2397
|
title: z.string(),
|
|
2395
2398
|
description: z.string().nullish(),
|
|
2396
2399
|
media_blob_id: z.string(),
|
|
2400
|
+
blake3: z.string().nullish(),
|
|
2401
|
+
parent_video_id: z.string().nullish(),
|
|
2397
2402
|
poster_blob_id: z.string().nullish(),
|
|
2398
2403
|
duration_seconds: z.number().nullish(),
|
|
2399
2404
|
release_date: z.string().nullish(),
|
|
@@ -3701,6 +3706,8 @@ export const ListFavoritesResponseSchema = z.object({
|
|
|
3701
3706
|
title: z.string(),
|
|
3702
3707
|
description: z.string().nullish(),
|
|
3703
3708
|
media_blob_id: z.string(),
|
|
3709
|
+
blake3: z.string().nullish(),
|
|
3710
|
+
parent_video_id: z.string().nullish(),
|
|
3704
3711
|
poster_blob_id: z.string().nullish(),
|
|
3705
3712
|
duration_seconds: z.number().nullish(),
|
|
3706
3713
|
release_date: z.string().nullish(),
|
|
@@ -3896,6 +3903,11 @@ export const ListTaxonsByKindRequestSchema = z.object({
|
|
|
3896
3903
|
});
|
|
3897
3904
|
export type ListTaxonsByKindRequest = z.infer<typeof ListTaxonsByKindRequestSchema>;
|
|
3898
3905
|
|
|
3906
|
+
export const ListVideoExtrasRequestSchema = z.object({
|
|
3907
|
+
parent_video_id: z.string()
|
|
3908
|
+
});
|
|
3909
|
+
export type ListVideoExtrasRequest = z.infer<typeof ListVideoExtrasRequestSchema>;
|
|
3910
|
+
|
|
3899
3911
|
export const ListVideoSeasonsRequestSchema = z.object({
|
|
3900
3912
|
series_id: z.string().nullish()
|
|
3901
3913
|
});
|
|
@@ -5190,6 +5202,7 @@ export const PublicAssetRefSchema = z.object({
|
|
|
5190
5202
|
export type PublicAssetRef = z.infer<typeof PublicAssetRefSchema>;
|
|
5191
5203
|
|
|
5192
5204
|
export const PublicNowPlayingSchema = z.object({
|
|
5205
|
+
kind: z.string(),
|
|
5193
5206
|
song_id: z.string(),
|
|
5194
5207
|
title: z.string(),
|
|
5195
5208
|
artist: z.string().nullish(),
|
|
@@ -5210,7 +5223,10 @@ export const PublicStationSchema = z.object({
|
|
|
5210
5223
|
listener_count: z.number(),
|
|
5211
5224
|
is_default: z.boolean(),
|
|
5212
5225
|
is_public: z.boolean(),
|
|
5226
|
+
is_running: z.boolean(),
|
|
5227
|
+
accepts_requests: z.boolean(),
|
|
5213
5228
|
now_playing: z.object({
|
|
5229
|
+
kind: z.string(),
|
|
5214
5230
|
song_id: z.string(),
|
|
5215
5231
|
title: z.string(),
|
|
5216
5232
|
artist: z.string().nullish(),
|
|
@@ -5391,7 +5407,8 @@ export type QueryVideosRequest = z.infer<typeof QueryVideosRequestSchema>;
|
|
|
5391
5407
|
export const RadioBumperSchema = z.object({
|
|
5392
5408
|
id: z.string(),
|
|
5393
5409
|
station_id: z.string(),
|
|
5394
|
-
song_id: z.string(),
|
|
5410
|
+
song_id: z.string().nullish(),
|
|
5411
|
+
video_id: z.string().nullish(),
|
|
5395
5412
|
label: z.string(),
|
|
5396
5413
|
weight: z.number(),
|
|
5397
5414
|
created_at: z.number()
|
|
@@ -5400,7 +5417,8 @@ export type RadioBumper = z.infer<typeof RadioBumperSchema>;
|
|
|
5400
5417
|
|
|
5401
5418
|
export const RadioBumpersAddRequestSchema = z.object({
|
|
5402
5419
|
station_id: z.string(),
|
|
5403
|
-
song_id: z.string(),
|
|
5420
|
+
song_id: z.string().nullish(),
|
|
5421
|
+
video_id: z.string().nullish(),
|
|
5404
5422
|
label: z.string(),
|
|
5405
5423
|
weight: z.number().nullish()
|
|
5406
5424
|
});
|
|
@@ -5424,8 +5442,12 @@ export type RadioBumpersSetFrequencyRequest = z.infer<typeof RadioBumpersSetFreq
|
|
|
5424
5442
|
|
|
5425
5443
|
export const RadioConfigPayloadSchema = z.object({
|
|
5426
5444
|
enabled: z.boolean(),
|
|
5427
|
-
encode_args: z.string(),
|
|
5428
|
-
|
|
5445
|
+
encode_args: z.string().nullish(),
|
|
5446
|
+
video_encode_args: z.string().nullish(),
|
|
5447
|
+
video_codec: z.string().nullish(),
|
|
5448
|
+
ffmpeg_available: z.boolean(),
|
|
5449
|
+
max_concurrent_audio_streams: z.number(),
|
|
5450
|
+
max_concurrent_video_streams: z.number()
|
|
5429
5451
|
});
|
|
5430
5452
|
export type RadioConfigPayload = z.infer<typeof RadioConfigPayloadSchema>;
|
|
5431
5453
|
|
|
@@ -5451,7 +5473,10 @@ export const RadioInfoResponseSchema = z.object({
|
|
|
5451
5473
|
listener_count: z.number(),
|
|
5452
5474
|
is_default: z.boolean(),
|
|
5453
5475
|
is_public: z.boolean(),
|
|
5476
|
+
is_running: z.boolean(),
|
|
5477
|
+
accepts_requests: z.boolean(),
|
|
5454
5478
|
now_playing: z.object({
|
|
5479
|
+
kind: z.string(),
|
|
5455
5480
|
song_id: z.string(),
|
|
5456
5481
|
title: z.string(),
|
|
5457
5482
|
artist: z.string().nullish(),
|
|
@@ -5468,6 +5493,34 @@ export const RadioInfoResponseSchema = z.object({
|
|
|
5468
5493
|
});
|
|
5469
5494
|
export type RadioInfoResponse = z.infer<typeof RadioInfoResponseSchema>;
|
|
5470
5495
|
|
|
5496
|
+
export const RadioQueuedRequestInfoSchema = z.object({
|
|
5497
|
+
id: z.string(),
|
|
5498
|
+
kind: z.string(),
|
|
5499
|
+
item_id: z.string(),
|
|
5500
|
+
requested_by: z.string(),
|
|
5501
|
+
title: z.string(),
|
|
5502
|
+
artist: z.string().nullish(),
|
|
5503
|
+
album: z.string().nullish(),
|
|
5504
|
+
duration_ms: z.number().nullish(),
|
|
5505
|
+
art_blob_id: z.string().nullish()
|
|
5506
|
+
});
|
|
5507
|
+
export type RadioQueuedRequestInfo = z.infer<typeof RadioQueuedRequestInfoSchema>;
|
|
5508
|
+
|
|
5509
|
+
export const RadioRequestsListResponseSchema = z.object({
|
|
5510
|
+
requests: z.array(z.object({
|
|
5511
|
+
id: z.string(),
|
|
5512
|
+
kind: z.string(),
|
|
5513
|
+
item_id: z.string(),
|
|
5514
|
+
requested_by: z.string(),
|
|
5515
|
+
title: z.string(),
|
|
5516
|
+
artist: z.string().nullish(),
|
|
5517
|
+
album: z.string().nullish(),
|
|
5518
|
+
duration_ms: z.number().nullish(),
|
|
5519
|
+
art_blob_id: z.string().nullish()
|
|
5520
|
+
}))
|
|
5521
|
+
});
|
|
5522
|
+
export type RadioRequestsListResponse = z.infer<typeof RadioRequestsListResponseSchema>;
|
|
5523
|
+
|
|
5471
5524
|
export const RadioSeedSuggestRequestSchema = z.object({
|
|
5472
5525
|
kind: z.string(),
|
|
5473
5526
|
query: z.string(),
|
|
@@ -5493,6 +5546,9 @@ export const RadioStationSchema = z.object({
|
|
|
5493
5546
|
codec: z.string(),
|
|
5494
5547
|
play_mode: z.string(),
|
|
5495
5548
|
timeline_only_mode: z.number(),
|
|
5549
|
+
content_mode: z.string(),
|
|
5550
|
+
bumper_frequency_seconds: z.number().nullish(),
|
|
5551
|
+
accepts_requests: z.number(),
|
|
5496
5552
|
created_at: z.number(),
|
|
5497
5553
|
updated_at: z.number()
|
|
5498
5554
|
});
|
|
@@ -5503,6 +5559,11 @@ export const RadioStationByStationIdRequestSchema = z.object({
|
|
|
5503
5559
|
});
|
|
5504
5560
|
export type RadioStationByStationIdRequest = z.infer<typeof RadioStationByStationIdRequestSchema>;
|
|
5505
5561
|
|
|
5562
|
+
export const RadioStationRequestsRequestSchema = z.object({
|
|
5563
|
+
station_id: z.string()
|
|
5564
|
+
});
|
|
5565
|
+
export type RadioStationRequestsRequest = z.infer<typeof RadioStationRequestsRequestSchema>;
|
|
5566
|
+
|
|
5506
5567
|
export const RadioStationSupervisorStatusSchema = z.object({
|
|
5507
5568
|
station_id: z.string(),
|
|
5508
5569
|
name: z.string(),
|
|
@@ -5530,7 +5591,10 @@ export const RadioStationsResponseSchema = z.object({
|
|
|
5530
5591
|
listener_count: z.number(),
|
|
5531
5592
|
is_default: z.boolean(),
|
|
5532
5593
|
is_public: z.boolean(),
|
|
5594
|
+
is_running: z.boolean(),
|
|
5595
|
+
accepts_requests: z.boolean(),
|
|
5533
5596
|
now_playing: z.object({
|
|
5597
|
+
kind: z.string(),
|
|
5534
5598
|
song_id: z.string(),
|
|
5535
5599
|
title: z.string(),
|
|
5536
5600
|
artist: z.string().nullish(),
|
|
@@ -5687,6 +5751,8 @@ export const RecentlyAddedVideosResponseSchema = z.object({
|
|
|
5687
5751
|
title: z.string(),
|
|
5688
5752
|
description: z.string().nullish(),
|
|
5689
5753
|
media_blob_id: z.string(),
|
|
5754
|
+
blake3: z.string().nullish(),
|
|
5755
|
+
parent_video_id: z.string().nullish(),
|
|
5690
5756
|
poster_blob_id: z.string().nullish(),
|
|
5691
5757
|
duration_seconds: z.number().nullish(),
|
|
5692
5758
|
release_date: z.string().nullish(),
|
|
@@ -5900,6 +5966,12 @@ export const RemovePlaylistThumbnailRequestSchema = z.object({
|
|
|
5900
5966
|
});
|
|
5901
5967
|
export type RemovePlaylistThumbnailRequest = z.infer<typeof RemovePlaylistThumbnailRequestSchema>;
|
|
5902
5968
|
|
|
5969
|
+
export const RemoveRadioRequestRequestSchema = z.object({
|
|
5970
|
+
station_id: z.string(),
|
|
5971
|
+
request_id: z.string()
|
|
5972
|
+
});
|
|
5973
|
+
export type RemoveRadioRequestRequest = z.infer<typeof RemoveRadioRequestRequestSchema>;
|
|
5974
|
+
|
|
5903
5975
|
export const RemoveRatingRequestSchema = z.object({
|
|
5904
5976
|
user_id: z.string().nullish(),
|
|
5905
5977
|
target_type: z.union([z.literal("song"), z.literal("artist"), z.literal("album"), z.literal("video")]),
|
|
@@ -6161,6 +6233,8 @@ export const SeasonWithVideosSchema = z.object({
|
|
|
6161
6233
|
title: z.string(),
|
|
6162
6234
|
description: z.string().nullish(),
|
|
6163
6235
|
media_blob_id: z.string(),
|
|
6236
|
+
blake3: z.string().nullish(),
|
|
6237
|
+
parent_video_id: z.string().nullish(),
|
|
6164
6238
|
poster_blob_id: z.string().nullish(),
|
|
6165
6239
|
duration_seconds: z.number().nullish(),
|
|
6166
6240
|
release_date: z.string().nullish(),
|
|
@@ -6220,6 +6294,8 @@ export const SeriesDetailSchema = z.object({
|
|
|
6220
6294
|
title: z.string(),
|
|
6221
6295
|
description: z.string().nullish(),
|
|
6222
6296
|
media_blob_id: z.string(),
|
|
6297
|
+
blake3: z.string().nullish(),
|
|
6298
|
+
parent_video_id: z.string().nullish(),
|
|
6223
6299
|
poster_blob_id: z.string().nullish(),
|
|
6224
6300
|
duration_seconds: z.number().nullish(),
|
|
6225
6301
|
release_date: z.string().nullish(),
|
|
@@ -6246,6 +6322,8 @@ export const SeriesDetailSchema = z.object({
|
|
|
6246
6322
|
title: z.string(),
|
|
6247
6323
|
description: z.string().nullish(),
|
|
6248
6324
|
media_blob_id: z.string(),
|
|
6325
|
+
blake3: z.string().nullish(),
|
|
6326
|
+
parent_video_id: z.string().nullish(),
|
|
6249
6327
|
poster_blob_id: z.string().nullish(),
|
|
6250
6328
|
duration_seconds: z.number().nullish(),
|
|
6251
6329
|
release_date: z.string().nullish(),
|
|
@@ -6809,6 +6887,13 @@ export const StationFilterSchema = z.object({
|
|
|
6809
6887
|
});
|
|
6810
6888
|
export type StationFilter = z.infer<typeof StationFilterSchema>;
|
|
6811
6889
|
|
|
6890
|
+
export const SubmitRadioRequestRequestSchema = z.object({
|
|
6891
|
+
station_id: z.string(),
|
|
6892
|
+
kind: z.string(),
|
|
6893
|
+
item_id: z.string()
|
|
6894
|
+
});
|
|
6895
|
+
export type SubmitRadioRequestRequest = z.infer<typeof SubmitRadioRequestRequestSchema>;
|
|
6896
|
+
|
|
6812
6897
|
export const SuggestionSchema = z.object({
|
|
6813
6898
|
value: z.string(),
|
|
6814
6899
|
display: z.string(),
|
|
@@ -7007,6 +7092,7 @@ export const SyncVideoByBlake3RequestSchema = z.object({
|
|
|
7007
7092
|
series_description: z.string().nullish(),
|
|
7008
7093
|
season_number: z.number().nullish(),
|
|
7009
7094
|
season_title: z.string().nullish(),
|
|
7095
|
+
parent_blake3: z.string().nullish(),
|
|
7010
7096
|
video_images: z.array(z.object({
|
|
7011
7097
|
content_sha256: z.string(),
|
|
7012
7098
|
data_base64: z.string().nullish(),
|
|
@@ -7303,6 +7389,8 @@ export const UnassignedVideosResponseSchema = z.object({
|
|
|
7303
7389
|
title: z.string(),
|
|
7304
7390
|
description: z.string().nullish(),
|
|
7305
7391
|
media_blob_id: z.string(),
|
|
7392
|
+
blake3: z.string().nullish(),
|
|
7393
|
+
parent_video_id: z.string().nullish(),
|
|
7306
7394
|
poster_blob_id: z.string().nullish(),
|
|
7307
7395
|
duration_seconds: z.number().nullish(),
|
|
7308
7396
|
release_date: z.string().nullish(),
|
|
@@ -7631,7 +7719,9 @@ export const UpdateStationRequestSchema = z.object({
|
|
|
7631
7719
|
encode_args: z.string().nullish(),
|
|
7632
7720
|
codec: z.string().nullish(),
|
|
7633
7721
|
play_mode: z.string().nullish(),
|
|
7634
|
-
timeline_only_mode: z.boolean().nullish()
|
|
7722
|
+
timeline_only_mode: z.boolean().nullish(),
|
|
7723
|
+
content_mode: z.string().nullish(),
|
|
7724
|
+
accepts_requests: z.boolean().nullish()
|
|
7635
7725
|
});
|
|
7636
7726
|
export type UpdateStationRequest = z.infer<typeof UpdateStationRequestSchema>;
|
|
7637
7727
|
|
|
@@ -7648,12 +7738,14 @@ export const UpdateVideoRequestSchema = z.object({
|
|
|
7648
7738
|
content_type: z.string().nullish(),
|
|
7649
7739
|
title: z.string().nullish(),
|
|
7650
7740
|
description: z.string().nullish(),
|
|
7741
|
+
parent_video_id: z.string().nullish(),
|
|
7651
7742
|
poster_blob_id: z.string().nullish(),
|
|
7652
7743
|
duration_seconds: z.number().nullish(),
|
|
7653
7744
|
release_date: z.string().nullish(),
|
|
7654
7745
|
updated_by: z.string().nullish(),
|
|
7655
7746
|
clear_series_id: z.boolean(),
|
|
7656
|
-
clear_season_id: z.boolean()
|
|
7747
|
+
clear_season_id: z.boolean(),
|
|
7748
|
+
clear_parent_video_id: z.boolean()
|
|
7657
7749
|
});
|
|
7658
7750
|
export type UpdateVideoRequest = z.infer<typeof UpdateVideoRequestSchema>;
|
|
7659
7751
|
|
|
@@ -7687,8 +7779,10 @@ export const UpdateVideosRequestSchema = z.object({
|
|
|
7687
7779
|
duration_seconds: z.number().nullish(),
|
|
7688
7780
|
release_date: z.string().nullish(),
|
|
7689
7781
|
updated_by: z.string().nullish(),
|
|
7782
|
+
parent_video_id: z.string().nullish(),
|
|
7690
7783
|
clear_series_id: z.boolean(),
|
|
7691
|
-
clear_season_id: z.boolean()
|
|
7784
|
+
clear_season_id: z.boolean(),
|
|
7785
|
+
clear_parent_video_id: z.boolean()
|
|
7692
7786
|
});
|
|
7693
7787
|
export type UpdateVideosRequest = z.infer<typeof UpdateVideosRequestSchema>;
|
|
7694
7788
|
|
|
@@ -7764,6 +7858,8 @@ export const VideoSchema = z.object({
|
|
|
7764
7858
|
title: z.string(),
|
|
7765
7859
|
description: z.string().nullish(),
|
|
7766
7860
|
media_blob_id: z.string(),
|
|
7861
|
+
blake3: z.string().nullish(),
|
|
7862
|
+
parent_video_id: z.string().nullish(),
|
|
7767
7863
|
poster_blob_id: z.string().nullish(),
|
|
7768
7864
|
duration_seconds: z.number().nullish(),
|
|
7769
7865
|
release_date: z.string().nullish(),
|
|
@@ -7923,6 +8019,8 @@ export const VideoWithMetadataSchema = z.object({
|
|
|
7923
8019
|
title: z.string(),
|
|
7924
8020
|
description: z.string().nullish(),
|
|
7925
8021
|
media_blob_id: z.string(),
|
|
8022
|
+
blake3: z.string().nullish(),
|
|
8023
|
+
parent_video_id: z.string().nullish(),
|
|
7926
8024
|
poster_blob_id: z.string().nullish(),
|
|
7927
8025
|
duration_seconds: z.number().nullish(),
|
|
7928
8026
|
release_date: z.string().nullish(),
|
|
@@ -7971,6 +8069,8 @@ export const VideosByValueResponseSchema = z.object({
|
|
|
7971
8069
|
title: z.string(),
|
|
7972
8070
|
description: z.string().nullish(),
|
|
7973
8071
|
media_blob_id: z.string(),
|
|
8072
|
+
blake3: z.string().nullish(),
|
|
8073
|
+
parent_video_id: z.string().nullish(),
|
|
7974
8074
|
poster_blob_id: z.string().nullish(),
|
|
7975
8075
|
duration_seconds: z.number().nullish(),
|
|
7976
8076
|
release_date: z.string().nullish(),
|
|
@@ -8001,6 +8101,8 @@ export const VideosQueryResultSchema = z.object({
|
|
|
8001
8101
|
title: z.string(),
|
|
8002
8102
|
description: z.string().nullish(),
|
|
8003
8103
|
media_blob_id: z.string(),
|
|
8104
|
+
blake3: z.string().nullish(),
|
|
8105
|
+
parent_video_id: z.string().nullish(),
|
|
8004
8106
|
poster_blob_id: z.string().nullish(),
|
|
8005
8107
|
duration_seconds: z.number().nullish(),
|
|
8006
8108
|
release_date: z.string().nullish(),
|
package/src/domains/app.ts
CHANGED
|
@@ -7,7 +7,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
7
7
|
return {
|
|
8
8
|
healthCheck: () => {
|
|
9
9
|
return call(
|
|
10
|
-
"app",
|
|
10
|
+
"app",
|
|
11
|
+
"health_check",
|
|
11
12
|
routes.app.health_check.resp,
|
|
12
13
|
routes.app.health_check.req,
|
|
13
14
|
routes.app.health_check.method,
|
|
@@ -17,7 +18,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
17
18
|
|
|
18
19
|
serverInfo: () => {
|
|
19
20
|
return call(
|
|
20
|
-
"app",
|
|
21
|
+
"app",
|
|
22
|
+
"server_info",
|
|
21
23
|
routes.app.server_info.resp,
|
|
22
24
|
routes.app.server_info.req,
|
|
23
25
|
routes.app.server_info.method,
|
|
@@ -27,7 +29,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
27
29
|
|
|
28
30
|
radioInfo: () => {
|
|
29
31
|
return call(
|
|
30
|
-
"app",
|
|
32
|
+
"app",
|
|
33
|
+
"radio_info",
|
|
31
34
|
routes.app.radio_info.resp,
|
|
32
35
|
routes.app.radio_info.req,
|
|
33
36
|
routes.app.radio_info.method,
|
|
@@ -37,7 +40,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
37
40
|
|
|
38
41
|
radioStations: () => {
|
|
39
42
|
return call(
|
|
40
|
-
"app",
|
|
43
|
+
"app",
|
|
44
|
+
"radio_stations",
|
|
41
45
|
routes.app.radio_stations.resp,
|
|
42
46
|
routes.app.radio_stations.req,
|
|
43
47
|
routes.app.radio_stations.method,
|
|
@@ -45,10 +49,84 @@ export function createAppMethods(call: CallFn) {
|
|
|
45
49
|
);
|
|
46
50
|
},
|
|
47
51
|
|
|
52
|
+
// authenticated counterpart of radioStations() - includes non-public
|
|
53
|
+
// stations too, for a caller who actually resolved (real session or
|
|
54
|
+
// known iroh peer). see offal::music::radio's doc comment.
|
|
55
|
+
radioStationsFull: () => {
|
|
56
|
+
return call(
|
|
57
|
+
"app",
|
|
58
|
+
"radio_stations_full",
|
|
59
|
+
routes.app.radio_stations_full.resp,
|
|
60
|
+
routes.app.radio_stations_full.req,
|
|
61
|
+
routes.app.radio_stations_full.method,
|
|
62
|
+
routes.app.radio_stations_full.path,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// submit a member request to an `accepts_requests` station's queue -
|
|
67
|
+
// any authenticated caller may call this (not admin-only, unlike
|
|
68
|
+
// radio_filters_add's "add to station").
|
|
69
|
+
radioSubmitRequest: (params: {
|
|
70
|
+
station_id: string;
|
|
71
|
+
kind: "song" | "video";
|
|
72
|
+
item_id: string;
|
|
73
|
+
}) => {
|
|
74
|
+
return call(
|
|
75
|
+
"app",
|
|
76
|
+
"radio_submit_request",
|
|
77
|
+
routes.app.radio_submit_request.resp,
|
|
78
|
+
routes.app.radio_submit_request.req,
|
|
79
|
+
routes.app.radio_submit_request.method,
|
|
80
|
+
routes.app.radio_submit_request.path,
|
|
81
|
+
params,
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// list every currently-queued request for a station (the "queue" tab)
|
|
86
|
+
radioListRequests: (params: { station_id: string }) => {
|
|
87
|
+
return call(
|
|
88
|
+
"app",
|
|
89
|
+
"radio_list_requests",
|
|
90
|
+
routes.app.radio_list_requests.resp,
|
|
91
|
+
routes.app.radio_list_requests.req,
|
|
92
|
+
routes.app.radio_list_requests.method,
|
|
93
|
+
routes.app.radio_list_requests.path,
|
|
94
|
+
params,
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// remove one queued request - any authenticated caller may call this
|
|
99
|
+
// (member-submitted content, not admin-owned configuration).
|
|
100
|
+
radioRemoveRequest: (params: { station_id: string; request_id: string }) => {
|
|
101
|
+
return call(
|
|
102
|
+
"app",
|
|
103
|
+
"radio_remove_request",
|
|
104
|
+
routes.app.radio_remove_request.resp,
|
|
105
|
+
routes.app.radio_remove_request.req,
|
|
106
|
+
routes.app.radio_remove_request.method,
|
|
107
|
+
routes.app.radio_remove_request.path,
|
|
108
|
+
params,
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// clear every queued request for a station at once
|
|
113
|
+
radioClearRequests: (params: { station_id: string }) => {
|
|
114
|
+
return call(
|
|
115
|
+
"app",
|
|
116
|
+
"radio_clear_requests",
|
|
117
|
+
routes.app.radio_clear_requests.resp,
|
|
118
|
+
routes.app.radio_clear_requests.req,
|
|
119
|
+
routes.app.radio_clear_requests.method,
|
|
120
|
+
routes.app.radio_clear_requests.path,
|
|
121
|
+
params,
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
|
|
48
125
|
// get blob metadata for a station blob
|
|
49
126
|
radioPublicBlob: (params: { station_id: string; blob_id: string }) => {
|
|
50
127
|
return call(
|
|
51
|
-
"app",
|
|
128
|
+
"app",
|
|
129
|
+
"radio_public_blob",
|
|
52
130
|
routes.app.radio_public_blob.resp,
|
|
53
131
|
routes.app.radio_public_blob.req,
|
|
54
132
|
routes.app.radio_public_blob.method,
|
|
@@ -60,7 +138,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
60
138
|
// stream the binary data for a station blob
|
|
61
139
|
radioPublicBlobData: (params: { station_id: string; blob_id: string }) => {
|
|
62
140
|
return call(
|
|
63
|
-
"app",
|
|
141
|
+
"app",
|
|
142
|
+
"radio_public_blob_data",
|
|
64
143
|
routes.app.radio_public_blob_data.resp,
|
|
65
144
|
routes.app.radio_public_blob_data.req,
|
|
66
145
|
routes.app.radio_public_blob_data.method,
|
|
@@ -72,7 +151,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
72
151
|
// get a thumbnail for a station blob at a given size
|
|
73
152
|
radioPublicBlobThumbnail: (params: { station_id: string; blob_id: string; size: string }) => {
|
|
74
153
|
return call(
|
|
75
|
-
"app",
|
|
154
|
+
"app",
|
|
155
|
+
"radio_public_blob_thumbnail",
|
|
76
156
|
routes.app.radio_public_blob_thumbnail.resp,
|
|
77
157
|
routes.app.radio_public_blob_thumbnail.req,
|
|
78
158
|
routes.app.radio_public_blob_thumbnail.method,
|
|
@@ -84,7 +164,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
84
164
|
// get the public timeline manifest for a station
|
|
85
165
|
radioPublicTimeline: (params: { station_id: string }) => {
|
|
86
166
|
return call(
|
|
87
|
-
"app",
|
|
167
|
+
"app",
|
|
168
|
+
"radio_public_timeline",
|
|
88
169
|
routes.app.radio_public_timeline.resp,
|
|
89
170
|
routes.app.radio_public_timeline.req,
|
|
90
171
|
routes.app.radio_public_timeline.method,
|
package/src/domains/video.ts
CHANGED
|
@@ -107,6 +107,18 @@ export function createVideoMethods(call: CallFn) {
|
|
|
107
107
|
);
|
|
108
108
|
},
|
|
109
109
|
|
|
110
|
+
listVideoExtras: (params: s.ListVideoExtrasRequest) => {
|
|
111
|
+
return call(
|
|
112
|
+
"video",
|
|
113
|
+
"list_video_extras",
|
|
114
|
+
routes.video.list_video_extras.resp,
|
|
115
|
+
routes.video.list_video_extras.req,
|
|
116
|
+
routes.video.list_video_extras.method,
|
|
117
|
+
routes.video.list_video_extras.path,
|
|
118
|
+
params,
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
|
|
110
122
|
updateVideos: (params: Partial<s.UpdateVideosRequest> & { video_ids: string[] }) => {
|
|
111
123
|
return call(
|
|
112
124
|
"video",
|
package/src/index.ts
CHANGED
package/src/transport.ts
CHANGED
|
@@ -137,6 +137,34 @@ export interface Transport {
|
|
|
137
137
|
opts?: BlobFetchOptions,
|
|
138
138
|
): Promise<string>;
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* stream a blob's bytes to a caller-supplied sink, chunk by chunk,
|
|
142
|
+
* instead of assembling the whole blob in memory first (optional).
|
|
143
|
+
* only implemented by WasmTransport - HTTP callers should stream
|
|
144
|
+
* directly from `fetch()`'s own `response.body` reader instead (see
|
|
145
|
+
* `streamVideoToOPFSWithResume`), and CharnelLocalTransport's sync-to-
|
|
146
|
+
* local path never goes through JS at all (grimoire's own disk-backed
|
|
147
|
+
* pull). intended for a caller writing straight to OPFS (or anywhere
|
|
148
|
+
* else) without ever holding the full blob in one buffer - see
|
|
149
|
+
* docs/blob-transfer-opfs-and-sha256-refactor-plan.md phase 2.
|
|
150
|
+
* @param blobId - the blob ID to fetch
|
|
151
|
+
* @param onChunk - called with each chunk as it arrives, in order
|
|
152
|
+
* @param onProgress - callback with (received, total) bytes
|
|
153
|
+
* @param blake3 - optional blake3 hash for verified streaming via iroh-blobs
|
|
154
|
+
* @param totalBytes - optional known total size in bytes
|
|
155
|
+
* @param mimeType - optional content type (midden's streaming path doesn't
|
|
156
|
+
* surface the source mime)
|
|
157
|
+
* @returns the content type and total bytes actually streamed
|
|
158
|
+
*/
|
|
159
|
+
streamBlobToSink?(
|
|
160
|
+
blobId: string,
|
|
161
|
+
onChunk: (chunk: Uint8Array, offset: number) => void | Promise<void>,
|
|
162
|
+
onProgress: (received: number, total: number) => void,
|
|
163
|
+
blake3?: string,
|
|
164
|
+
totalBytes?: number,
|
|
165
|
+
mimeType?: string,
|
|
166
|
+
): Promise<{ contentType: string; totalBytes: number }>;
|
|
167
|
+
|
|
140
168
|
/**
|
|
141
169
|
* fetch server image (public, no auth required)
|
|
142
170
|
* used during "add remote" flow before user is authenticated
|