@freqhole/api-client 0.3.7 → 0.3.9
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 +74 -10
- package/src/codegen/routes.ts +5 -0
- package/src/codegen/schema.ts +89 -4
- package/src/domains/app.ts +59 -0
- package/src/domains/video.ts +12 -0
- package/src/index.ts +1 -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
|
|
package/src/codegen/routes.ts
CHANGED
|
@@ -34,13 +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 },
|
|
43
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 },
|
|
44
48
|
server_info: { method: 'GET', path: '/api/hello', req: null, resp: s.ServerInfoResponseSchema, auth: { type: 'public' } as const },
|
|
45
49
|
},
|
|
46
50
|
auth: {
|
|
@@ -253,6 +257,7 @@ export const routes = {
|
|
|
253
257
|
import_video_paths: { method: 'POST', path: '/api/upload/video-paths', req: null, resp: s.VideoImportResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
254
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 },
|
|
255
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 },
|
|
256
261
|
list_video_seasons: { method: 'POST', path: '/api/video/seasons/list', req: s.ListVideoSeasonsRequestSchema, resp: s.VideoSeasonSchema.array(), auth: { type: 'authenticated' } as const },
|
|
257
262
|
list_video_series: { method: 'POST', path: '/api/video/series/list', req: s.ListVideoSeriesRequestSchema, resp: s.VideoSeriesSchema.array(), auth: { type: 'authenticated' } as const },
|
|
258
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
|
@@ -1509,7 +1509,8 @@ export const CreateStationRequestSchema = z.object({
|
|
|
1509
1509
|
codec: z.string().nullish(),
|
|
1510
1510
|
play_mode: z.string().nullish(),
|
|
1511
1511
|
timeline_only_mode: z.boolean().nullish(),
|
|
1512
|
-
content_mode: z.string().nullish()
|
|
1512
|
+
content_mode: z.string().nullish(),
|
|
1513
|
+
accepts_requests: z.boolean().nullish()
|
|
1513
1514
|
});
|
|
1514
1515
|
export type CreateStationRequest = z.infer<typeof CreateStationRequestSchema>;
|
|
1515
1516
|
|
|
@@ -1546,6 +1547,7 @@ export const CreateVideoRequestSchema = z.object({
|
|
|
1546
1547
|
title: z.string(),
|
|
1547
1548
|
description: z.string().nullish(),
|
|
1548
1549
|
media_blob_id: z.string(),
|
|
1550
|
+
parent_video_id: z.string().nullish(),
|
|
1549
1551
|
poster_blob_id: z.string().nullish(),
|
|
1550
1552
|
duration_seconds: z.number().nullish(),
|
|
1551
1553
|
release_date: z.string().nullish(),
|
|
@@ -2395,6 +2397,8 @@ export const FavoriteItemSchema = z.union([z.intersection(z.object({
|
|
|
2395
2397
|
title: z.string(),
|
|
2396
2398
|
description: z.string().nullish(),
|
|
2397
2399
|
media_blob_id: z.string(),
|
|
2400
|
+
blake3: z.string().nullish(),
|
|
2401
|
+
parent_video_id: z.string().nullish(),
|
|
2398
2402
|
poster_blob_id: z.string().nullish(),
|
|
2399
2403
|
duration_seconds: z.number().nullish(),
|
|
2400
2404
|
release_date: z.string().nullish(),
|
|
@@ -3702,6 +3706,8 @@ export const ListFavoritesResponseSchema = z.object({
|
|
|
3702
3706
|
title: z.string(),
|
|
3703
3707
|
description: z.string().nullish(),
|
|
3704
3708
|
media_blob_id: z.string(),
|
|
3709
|
+
blake3: z.string().nullish(),
|
|
3710
|
+
parent_video_id: z.string().nullish(),
|
|
3705
3711
|
poster_blob_id: z.string().nullish(),
|
|
3706
3712
|
duration_seconds: z.number().nullish(),
|
|
3707
3713
|
release_date: z.string().nullish(),
|
|
@@ -3897,6 +3903,11 @@ export const ListTaxonsByKindRequestSchema = z.object({
|
|
|
3897
3903
|
});
|
|
3898
3904
|
export type ListTaxonsByKindRequest = z.infer<typeof ListTaxonsByKindRequestSchema>;
|
|
3899
3905
|
|
|
3906
|
+
export const ListVideoExtrasRequestSchema = z.object({
|
|
3907
|
+
parent_video_id: z.string()
|
|
3908
|
+
});
|
|
3909
|
+
export type ListVideoExtrasRequest = z.infer<typeof ListVideoExtrasRequestSchema>;
|
|
3910
|
+
|
|
3900
3911
|
export const ListVideoSeasonsRequestSchema = z.object({
|
|
3901
3912
|
series_id: z.string().nullish()
|
|
3902
3913
|
});
|
|
@@ -5213,6 +5224,7 @@ export const PublicStationSchema = z.object({
|
|
|
5213
5224
|
is_default: z.boolean(),
|
|
5214
5225
|
is_public: z.boolean(),
|
|
5215
5226
|
is_running: z.boolean(),
|
|
5227
|
+
accepts_requests: z.boolean(),
|
|
5216
5228
|
now_playing: z.object({
|
|
5217
5229
|
kind: z.string(),
|
|
5218
5230
|
song_id: z.string(),
|
|
@@ -5462,6 +5474,7 @@ export const RadioInfoResponseSchema = z.object({
|
|
|
5462
5474
|
is_default: z.boolean(),
|
|
5463
5475
|
is_public: z.boolean(),
|
|
5464
5476
|
is_running: z.boolean(),
|
|
5477
|
+
accepts_requests: z.boolean(),
|
|
5465
5478
|
now_playing: z.object({
|
|
5466
5479
|
kind: z.string(),
|
|
5467
5480
|
song_id: z.string(),
|
|
@@ -5480,6 +5493,34 @@ export const RadioInfoResponseSchema = z.object({
|
|
|
5480
5493
|
});
|
|
5481
5494
|
export type RadioInfoResponse = z.infer<typeof RadioInfoResponseSchema>;
|
|
5482
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
|
+
|
|
5483
5524
|
export const RadioSeedSuggestRequestSchema = z.object({
|
|
5484
5525
|
kind: z.string(),
|
|
5485
5526
|
query: z.string(),
|
|
@@ -5507,6 +5548,7 @@ export const RadioStationSchema = z.object({
|
|
|
5507
5548
|
timeline_only_mode: z.number(),
|
|
5508
5549
|
content_mode: z.string(),
|
|
5509
5550
|
bumper_frequency_seconds: z.number().nullish(),
|
|
5551
|
+
accepts_requests: z.number(),
|
|
5510
5552
|
created_at: z.number(),
|
|
5511
5553
|
updated_at: z.number()
|
|
5512
5554
|
});
|
|
@@ -5517,6 +5559,11 @@ export const RadioStationByStationIdRequestSchema = z.object({
|
|
|
5517
5559
|
});
|
|
5518
5560
|
export type RadioStationByStationIdRequest = z.infer<typeof RadioStationByStationIdRequestSchema>;
|
|
5519
5561
|
|
|
5562
|
+
export const RadioStationRequestsRequestSchema = z.object({
|
|
5563
|
+
station_id: z.string()
|
|
5564
|
+
});
|
|
5565
|
+
export type RadioStationRequestsRequest = z.infer<typeof RadioStationRequestsRequestSchema>;
|
|
5566
|
+
|
|
5520
5567
|
export const RadioStationSupervisorStatusSchema = z.object({
|
|
5521
5568
|
station_id: z.string(),
|
|
5522
5569
|
name: z.string(),
|
|
@@ -5545,6 +5592,7 @@ export const RadioStationsResponseSchema = z.object({
|
|
|
5545
5592
|
is_default: z.boolean(),
|
|
5546
5593
|
is_public: z.boolean(),
|
|
5547
5594
|
is_running: z.boolean(),
|
|
5595
|
+
accepts_requests: z.boolean(),
|
|
5548
5596
|
now_playing: z.object({
|
|
5549
5597
|
kind: z.string(),
|
|
5550
5598
|
song_id: z.string(),
|
|
@@ -5703,6 +5751,8 @@ export const RecentlyAddedVideosResponseSchema = z.object({
|
|
|
5703
5751
|
title: z.string(),
|
|
5704
5752
|
description: z.string().nullish(),
|
|
5705
5753
|
media_blob_id: z.string(),
|
|
5754
|
+
blake3: z.string().nullish(),
|
|
5755
|
+
parent_video_id: z.string().nullish(),
|
|
5706
5756
|
poster_blob_id: z.string().nullish(),
|
|
5707
5757
|
duration_seconds: z.number().nullish(),
|
|
5708
5758
|
release_date: z.string().nullish(),
|
|
@@ -5916,6 +5966,12 @@ export const RemovePlaylistThumbnailRequestSchema = z.object({
|
|
|
5916
5966
|
});
|
|
5917
5967
|
export type RemovePlaylistThumbnailRequest = z.infer<typeof RemovePlaylistThumbnailRequestSchema>;
|
|
5918
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
|
+
|
|
5919
5975
|
export const RemoveRatingRequestSchema = z.object({
|
|
5920
5976
|
user_id: z.string().nullish(),
|
|
5921
5977
|
target_type: z.union([z.literal("song"), z.literal("artist"), z.literal("album"), z.literal("video")]),
|
|
@@ -6177,6 +6233,8 @@ export const SeasonWithVideosSchema = z.object({
|
|
|
6177
6233
|
title: z.string(),
|
|
6178
6234
|
description: z.string().nullish(),
|
|
6179
6235
|
media_blob_id: z.string(),
|
|
6236
|
+
blake3: z.string().nullish(),
|
|
6237
|
+
parent_video_id: z.string().nullish(),
|
|
6180
6238
|
poster_blob_id: z.string().nullish(),
|
|
6181
6239
|
duration_seconds: z.number().nullish(),
|
|
6182
6240
|
release_date: z.string().nullish(),
|
|
@@ -6236,6 +6294,8 @@ export const SeriesDetailSchema = z.object({
|
|
|
6236
6294
|
title: z.string(),
|
|
6237
6295
|
description: z.string().nullish(),
|
|
6238
6296
|
media_blob_id: z.string(),
|
|
6297
|
+
blake3: z.string().nullish(),
|
|
6298
|
+
parent_video_id: z.string().nullish(),
|
|
6239
6299
|
poster_blob_id: z.string().nullish(),
|
|
6240
6300
|
duration_seconds: z.number().nullish(),
|
|
6241
6301
|
release_date: z.string().nullish(),
|
|
@@ -6262,6 +6322,8 @@ export const SeriesDetailSchema = z.object({
|
|
|
6262
6322
|
title: z.string(),
|
|
6263
6323
|
description: z.string().nullish(),
|
|
6264
6324
|
media_blob_id: z.string(),
|
|
6325
|
+
blake3: z.string().nullish(),
|
|
6326
|
+
parent_video_id: z.string().nullish(),
|
|
6265
6327
|
poster_blob_id: z.string().nullish(),
|
|
6266
6328
|
duration_seconds: z.number().nullish(),
|
|
6267
6329
|
release_date: z.string().nullish(),
|
|
@@ -6825,6 +6887,13 @@ export const StationFilterSchema = z.object({
|
|
|
6825
6887
|
});
|
|
6826
6888
|
export type StationFilter = z.infer<typeof StationFilterSchema>;
|
|
6827
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
|
+
|
|
6828
6897
|
export const SuggestionSchema = z.object({
|
|
6829
6898
|
value: z.string(),
|
|
6830
6899
|
display: z.string(),
|
|
@@ -7023,6 +7092,7 @@ export const SyncVideoByBlake3RequestSchema = z.object({
|
|
|
7023
7092
|
series_description: z.string().nullish(),
|
|
7024
7093
|
season_number: z.number().nullish(),
|
|
7025
7094
|
season_title: z.string().nullish(),
|
|
7095
|
+
parent_blake3: z.string().nullish(),
|
|
7026
7096
|
video_images: z.array(z.object({
|
|
7027
7097
|
content_sha256: z.string(),
|
|
7028
7098
|
data_base64: z.string().nullish(),
|
|
@@ -7319,6 +7389,8 @@ export const UnassignedVideosResponseSchema = z.object({
|
|
|
7319
7389
|
title: z.string(),
|
|
7320
7390
|
description: z.string().nullish(),
|
|
7321
7391
|
media_blob_id: z.string(),
|
|
7392
|
+
blake3: z.string().nullish(),
|
|
7393
|
+
parent_video_id: z.string().nullish(),
|
|
7322
7394
|
poster_blob_id: z.string().nullish(),
|
|
7323
7395
|
duration_seconds: z.number().nullish(),
|
|
7324
7396
|
release_date: z.string().nullish(),
|
|
@@ -7648,7 +7720,8 @@ export const UpdateStationRequestSchema = z.object({
|
|
|
7648
7720
|
codec: z.string().nullish(),
|
|
7649
7721
|
play_mode: z.string().nullish(),
|
|
7650
7722
|
timeline_only_mode: z.boolean().nullish(),
|
|
7651
|
-
content_mode: z.string().nullish()
|
|
7723
|
+
content_mode: z.string().nullish(),
|
|
7724
|
+
accepts_requests: z.boolean().nullish()
|
|
7652
7725
|
});
|
|
7653
7726
|
export type UpdateStationRequest = z.infer<typeof UpdateStationRequestSchema>;
|
|
7654
7727
|
|
|
@@ -7665,12 +7738,14 @@ export const UpdateVideoRequestSchema = z.object({
|
|
|
7665
7738
|
content_type: z.string().nullish(),
|
|
7666
7739
|
title: z.string().nullish(),
|
|
7667
7740
|
description: z.string().nullish(),
|
|
7741
|
+
parent_video_id: z.string().nullish(),
|
|
7668
7742
|
poster_blob_id: z.string().nullish(),
|
|
7669
7743
|
duration_seconds: z.number().nullish(),
|
|
7670
7744
|
release_date: z.string().nullish(),
|
|
7671
7745
|
updated_by: z.string().nullish(),
|
|
7672
7746
|
clear_series_id: z.boolean(),
|
|
7673
|
-
clear_season_id: z.boolean()
|
|
7747
|
+
clear_season_id: z.boolean(),
|
|
7748
|
+
clear_parent_video_id: z.boolean()
|
|
7674
7749
|
});
|
|
7675
7750
|
export type UpdateVideoRequest = z.infer<typeof UpdateVideoRequestSchema>;
|
|
7676
7751
|
|
|
@@ -7704,8 +7779,10 @@ export const UpdateVideosRequestSchema = z.object({
|
|
|
7704
7779
|
duration_seconds: z.number().nullish(),
|
|
7705
7780
|
release_date: z.string().nullish(),
|
|
7706
7781
|
updated_by: z.string().nullish(),
|
|
7782
|
+
parent_video_id: z.string().nullish(),
|
|
7707
7783
|
clear_series_id: z.boolean(),
|
|
7708
|
-
clear_season_id: z.boolean()
|
|
7784
|
+
clear_season_id: z.boolean(),
|
|
7785
|
+
clear_parent_video_id: z.boolean()
|
|
7709
7786
|
});
|
|
7710
7787
|
export type UpdateVideosRequest = z.infer<typeof UpdateVideosRequestSchema>;
|
|
7711
7788
|
|
|
@@ -7781,6 +7858,8 @@ export const VideoSchema = z.object({
|
|
|
7781
7858
|
title: z.string(),
|
|
7782
7859
|
description: z.string().nullish(),
|
|
7783
7860
|
media_blob_id: z.string(),
|
|
7861
|
+
blake3: z.string().nullish(),
|
|
7862
|
+
parent_video_id: z.string().nullish(),
|
|
7784
7863
|
poster_blob_id: z.string().nullish(),
|
|
7785
7864
|
duration_seconds: z.number().nullish(),
|
|
7786
7865
|
release_date: z.string().nullish(),
|
|
@@ -7940,6 +8019,8 @@ export const VideoWithMetadataSchema = z.object({
|
|
|
7940
8019
|
title: z.string(),
|
|
7941
8020
|
description: z.string().nullish(),
|
|
7942
8021
|
media_blob_id: z.string(),
|
|
8022
|
+
blake3: z.string().nullish(),
|
|
8023
|
+
parent_video_id: z.string().nullish(),
|
|
7943
8024
|
poster_blob_id: z.string().nullish(),
|
|
7944
8025
|
duration_seconds: z.number().nullish(),
|
|
7945
8026
|
release_date: z.string().nullish(),
|
|
@@ -7988,6 +8069,8 @@ export const VideosByValueResponseSchema = z.object({
|
|
|
7988
8069
|
title: z.string(),
|
|
7989
8070
|
description: z.string().nullish(),
|
|
7990
8071
|
media_blob_id: z.string(),
|
|
8072
|
+
blake3: z.string().nullish(),
|
|
8073
|
+
parent_video_id: z.string().nullish(),
|
|
7991
8074
|
poster_blob_id: z.string().nullish(),
|
|
7992
8075
|
duration_seconds: z.number().nullish(),
|
|
7993
8076
|
release_date: z.string().nullish(),
|
|
@@ -8018,6 +8101,8 @@ export const VideosQueryResultSchema = z.object({
|
|
|
8018
8101
|
title: z.string(),
|
|
8019
8102
|
description: z.string().nullish(),
|
|
8020
8103
|
media_blob_id: z.string(),
|
|
8104
|
+
blake3: z.string().nullish(),
|
|
8105
|
+
parent_video_id: z.string().nullish(),
|
|
8021
8106
|
poster_blob_id: z.string().nullish(),
|
|
8022
8107
|
duration_seconds: z.number().nullish(),
|
|
8023
8108
|
release_date: z.string().nullish(),
|
package/src/domains/app.ts
CHANGED
|
@@ -63,6 +63,65 @@ export function createAppMethods(call: CallFn) {
|
|
|
63
63
|
);
|
|
64
64
|
},
|
|
65
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
|
+
|
|
66
125
|
// get blob metadata for a station blob
|
|
67
126
|
radioPublicBlob: (params: { station_id: string; blob_id: string }) => {
|
|
68
127
|
return call(
|
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",
|