@freqhole/api-client 0.2.14 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -5
- package/src/CharnelLocalTransport.ts +70 -35
- package/src/CharnelTransport.ts +391 -106
- package/src/FreqholeClient.ts +46 -83
- package/src/WasmTransport.ts +265 -219
- package/src/blobCachePolicy.test.ts +199 -0
- package/src/codegen/routes.ts +75 -13
- package/src/codegen/schema.ts +1436 -225
- package/src/domains/entities.ts +304 -0
- package/src/domains/favorites.types.ts +13 -3
- package/src/domains/music.ts +74 -126
- package/src/domains/upload.ts +54 -53
- package/src/domains/video.ts +463 -0
- package/src/errors.ts +142 -0
- package/src/index.ts +47 -10
- package/src/permissions.ts +18 -36
- package/src/tauriRuntime.ts +22 -0
- package/src/test/fixtures.ts +13 -23
- package/src/test/integration.ts +15 -28
- package/src/test/stateful.ts +11 -29
- package/src/transport.ts +72 -39
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// tests for the blob cache policy.
|
|
2
|
+
//
|
|
3
|
+
// the transports back their blob fetches with the Cache API and, before
|
|
4
|
+
// `BlobFetchOptions` existed, wrote every blob they fetched unconditionally.
|
|
5
|
+
// that made the sync-to-local path store the same audio twice: once in the
|
|
6
|
+
// api cache (as a side effect of getting the bytes) and once in the library.
|
|
7
|
+
//
|
|
8
|
+
// `cache: "skip"` must suppress the write on *every* branch of the
|
|
9
|
+
// fallback chain, since which branch runs depends on what the peer supports.
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { WasmTransport, type MiddenNodeLike } from "./WasmTransport.js";
|
|
13
|
+
|
|
14
|
+
// minimal in-memory Cache API double. only the three methods the transports
|
|
15
|
+
// use (`open`, `match`, `put`) are implemented.
|
|
16
|
+
class FakeCache {
|
|
17
|
+
readonly entries = new Map<string, Response>();
|
|
18
|
+
|
|
19
|
+
async match(key: string): Promise<Response | undefined> {
|
|
20
|
+
return this.entries.get(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async put(key: string, response: Response): Promise<void> {
|
|
24
|
+
this.entries.set(key, response);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let fakeCache: FakeCache;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
fakeCache = new FakeCache();
|
|
32
|
+
vi.stubGlobal("caches", {
|
|
33
|
+
open: async () => fakeCache,
|
|
34
|
+
});
|
|
35
|
+
// jsdom/node have no URL.createObjectURL
|
|
36
|
+
vi.stubGlobal("URL", {
|
|
37
|
+
...URL,
|
|
38
|
+
createObjectURL: () => "blob:fake",
|
|
39
|
+
revokeObjectURL: () => {},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const BYTES = new Uint8Array([1, 2, 3, 4]);
|
|
44
|
+
const BLAKE3 = "a".repeat(64);
|
|
45
|
+
|
|
46
|
+
/** a node whose only working download path is the one named. lets each test
|
|
47
|
+
* force a specific branch of the fallback chain. */
|
|
48
|
+
function nodeWith(branch: "verified" | "api_request" | "by_id" | "legacy"): MiddenNodeLike {
|
|
49
|
+
const node: Partial<MiddenNodeLike> = {
|
|
50
|
+
node_id: () => "test-node",
|
|
51
|
+
api_request: async () => {
|
|
52
|
+
if (branch !== "api_request") throw new Error("api_request unavailable");
|
|
53
|
+
return {
|
|
54
|
+
status: 200,
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
success: true,
|
|
57
|
+
data: { data: btoa(String.fromCharCode(...BYTES)), mime: "audio/mpeg" },
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (branch === "verified") {
|
|
64
|
+
node.download_verified_with_ensure = async () => BYTES;
|
|
65
|
+
}
|
|
66
|
+
if (branch === "by_id") {
|
|
67
|
+
node.download_verified_by_id = async () => [BYTES, BLAKE3];
|
|
68
|
+
}
|
|
69
|
+
if (branch === "legacy") {
|
|
70
|
+
node.fetch_blob = async () => ({
|
|
71
|
+
data: () => BYTES,
|
|
72
|
+
content_type: () => "audio/mpeg",
|
|
73
|
+
size: () => BYTES.length,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return node as MiddenNodeLike;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const BRANCHES = ["verified", "api_request", "by_id", "legacy"] as const;
|
|
80
|
+
|
|
81
|
+
describe("WasmTransport.fetchBlob cache policy", () => {
|
|
82
|
+
describe("default (no options) still writes to the Cache API", () => {
|
|
83
|
+
for (const branch of BRANCHES) {
|
|
84
|
+
it(`writes on the ${branch} branch`, async () => {
|
|
85
|
+
const transport = new WasmTransport(nodeWith(branch), "peer");
|
|
86
|
+
// the verified branch is only tried when a blake3 is supplied, and
|
|
87
|
+
// by_id/legacy are only reached when it is not.
|
|
88
|
+
await transport.fetchBlob("blob-1", branch === "verified" ? BLAKE3 : undefined);
|
|
89
|
+
expect(fakeCache.entries.size).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('cache: "skip" suppresses the write', () => {
|
|
95
|
+
for (const branch of BRANCHES) {
|
|
96
|
+
it(`does not write on the ${branch} branch`, async () => {
|
|
97
|
+
const transport = new WasmTransport(nodeWith(branch), "peer");
|
|
98
|
+
await transport.fetchBlob("blob-1", branch === "verified" ? BLAKE3 : undefined, {
|
|
99
|
+
cache: "skip",
|
|
100
|
+
});
|
|
101
|
+
expect(fakeCache.entries.size).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("still returns the bytes when the write is skipped", async () => {
|
|
107
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
108
|
+
const result = await transport.fetchBlob("blob-1", BLAKE3, { cache: "skip" });
|
|
109
|
+
expect(Array.from(result.data)).toEqual(Array.from(BYTES));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reads an existing cache entry even with cache: "skip"', async () => {
|
|
113
|
+
// "skip" suppresses writes only - bytes already cached are still served,
|
|
114
|
+
// so a sync does not re-fetch something already sitting in the cache.
|
|
115
|
+
await fakeCache.put(
|
|
116
|
+
"https://blob.local/blob-1",
|
|
117
|
+
new Response(BYTES, { headers: { "Content-Type": "audio/mpeg" } }),
|
|
118
|
+
);
|
|
119
|
+
const node = nodeWith("verified");
|
|
120
|
+
const spy = vi.fn(async () => BYTES);
|
|
121
|
+
node.download_verified_with_ensure = spy;
|
|
122
|
+
|
|
123
|
+
const transport = new WasmTransport(node, "peer");
|
|
124
|
+
const result = await transport.fetchBlob("blob-1", BLAKE3, { cache: "skip" });
|
|
125
|
+
|
|
126
|
+
expect(Array.from(result.data)).toEqual(Array.from(BYTES));
|
|
127
|
+
expect(spy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("WasmTransport.fetchBlobWithProgress cache policy", () => {
|
|
132
|
+
describe("default (no options) still writes to the Cache API", () => {
|
|
133
|
+
for (const branch of BRANCHES) {
|
|
134
|
+
it(`writes on the ${branch} branch`, async () => {
|
|
135
|
+
const transport = new WasmTransport(nodeWith(branch), "peer");
|
|
136
|
+
await transport.fetchBlobWithProgress(
|
|
137
|
+
"blob-1",
|
|
138
|
+
() => {},
|
|
139
|
+
branch === "verified" ? BLAKE3 : undefined,
|
|
140
|
+
);
|
|
141
|
+
expect(fakeCache.entries.size).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('cache: "skip" suppresses the write', () => {
|
|
147
|
+
for (const branch of BRANCHES) {
|
|
148
|
+
it(`does not write on the ${branch} branch`, async () => {
|
|
149
|
+
const transport = new WasmTransport(nodeWith(branch), "peer");
|
|
150
|
+
await transport.fetchBlobWithProgress(
|
|
151
|
+
"blob-1",
|
|
152
|
+
() => {},
|
|
153
|
+
branch === "verified" ? BLAKE3 : undefined,
|
|
154
|
+
undefined,
|
|
155
|
+
undefined,
|
|
156
|
+
{ cache: "skip" },
|
|
157
|
+
);
|
|
158
|
+
expect(fakeCache.entries.size).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("still reports progress when the write is skipped", async () => {
|
|
164
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
165
|
+
const onProgress = vi.fn();
|
|
166
|
+
await transport.fetchBlobWithProgress("blob-1", onProgress, BLAKE3, undefined, undefined, {
|
|
167
|
+
cache: "skip",
|
|
168
|
+
});
|
|
169
|
+
expect(onProgress).toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("WasmTransport.getBlobUrl* cache policy", () => {
|
|
174
|
+
it('getBlobUrl with cache: "skip" does not write', async () => {
|
|
175
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
176
|
+
await transport.getBlobUrl("blob-1", BLAKE3, { cache: "skip" });
|
|
177
|
+
expect(fakeCache.entries.size).toBe(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("getBlobUrl writes by default", async () => {
|
|
181
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
182
|
+
await transport.getBlobUrl("blob-1", BLAKE3);
|
|
183
|
+
expect(fakeCache.entries.size).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('getBlobUrlWithProgress with cache: "skip" does not write', async () => {
|
|
187
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
188
|
+
await transport.getBlobUrlWithProgress("blob-1", () => {}, BLAKE3, undefined, undefined, {
|
|
189
|
+
cache: "skip",
|
|
190
|
+
});
|
|
191
|
+
expect(fakeCache.entries.size).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("getBlobUrlWithProgress writes by default", async () => {
|
|
195
|
+
const transport = new WasmTransport(nodeWith("verified"), "peer");
|
|
196
|
+
await transport.getBlobUrlWithProgress("blob-1", () => {}, BLAKE3);
|
|
197
|
+
expect(fakeCache.entries.size).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
});
|
package/src/codegen/routes.ts
CHANGED
|
@@ -61,6 +61,31 @@ export const routes = {
|
|
|
61
61
|
update_username: { method: 'POST', path: '/api/auth/profile/update-username', req: s.UpdateUsernameRequestSchema, resp: s.WhoAmIResponseSchema, auth: { type: 'authenticated' } as const },
|
|
62
62
|
whoami: { method: 'GET', path: '/api/auth/whoami', req: null, resp: s.WhoAmIResponseSchema, auth: { type: 'authenticated' } as const },
|
|
63
63
|
},
|
|
64
|
+
entities: {
|
|
65
|
+
add_entities_tags: { method: 'POST', path: '/api/entities/tags/add', req: s.AddEntitiesTagsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
66
|
+
add_entity_taxon: { method: 'POST', path: '/api/entities/taxons/add', req: s.AddEntityTaxonRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
67
|
+
add_entity_url: { method: 'POST', path: '/api/entities/urls/add', req: s.AddEntityUrlRequestSchema, resp: s.EntityUrlSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
68
|
+
add_playlist_item: { method: 'POST', path: '/api/entities/playlists/items/add', req: s.AddPlaylistItemRequestSchema, resp: s.PlaylistItemSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
69
|
+
add_playlist_items: { method: 'POST', path: '/api/entities/playlists/items/add-bulk', req: s.AddPlaylistItemsRequestSchema, resp: s.PlaylistItemSchema.array(), auth: { type: 'owner_or', role: 'admin' } as const },
|
|
70
|
+
get_entities_tags: { method: 'POST', path: '/api/entities/tags/get', req: s.GetEntitiesTagsRequestSchema, resp: s.EntityTagCountSchema.array(), auth: { type: 'authenticated' } as const },
|
|
71
|
+
get_entity_images: { method: 'POST', path: '/api/entities/images/get', req: s.GetEntityImagesRequestSchema, resp: s.ImageMetadataSchema.array(), auth: { type: 'authenticated' } as const },
|
|
72
|
+
get_entity_taxons: { method: 'POST', path: '/api/entities/taxons/get', req: s.GetEntityTaxonsRequestSchema, resp: s.EntityTaxonLinkSchema.array(), auth: { type: 'authenticated' } as const },
|
|
73
|
+
get_entity_urls: { method: 'POST', path: '/api/entities/urls/get', req: s.GetEntityUrlsRequestSchema, resp: s.EntityUrlSchema.array(), auth: { type: 'authenticated' } as const },
|
|
74
|
+
get_favorite_status_bulk: { method: 'POST', path: '/api/entities/favorites/status-bulk', req: s.GetFavoriteStatusBulkRequestSchema, resp: s.FavoriteStatusItemSchema.array(), auth: { type: 'authenticated' } as const },
|
|
75
|
+
get_rating_stats: { method: 'POST', path: '/api/entities/ratings/stats', req: s.GetRatingStatsRequestSchema, resp: s.RatingStatsSchema, auth: { type: 'authenticated' } as const },
|
|
76
|
+
get_rating_status_bulk: { method: 'POST', path: '/api/entities/ratings/status-bulk', req: s.GetRatingStatusBulkRequestSchema, resp: s.RatingStatusItemSchema.array(), auth: { type: 'authenticated' } as const },
|
|
77
|
+
list_entity_type_tags: { method: 'POST', path: '/api/entities/tags/list-by-type', req: s.ListEntityTypeTagsRequestSchema, resp: s.EntityTagCountSchema.array(), auth: { type: 'authenticated' } as const },
|
|
78
|
+
list_playlist_items: { method: 'POST', path: '/api/entities/playlists/items/list', req: s.ListPlaylistItemsRequestSchema, resp: s.PlaylistItemSchema.array(), auth: { type: 'authenticated' } as const },
|
|
79
|
+
remove_entities_tags: { method: 'POST', path: '/api/entities/tags/remove', req: s.RemoveEntitiesTagsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
80
|
+
remove_entity_taxon: { method: 'POST', path: '/api/entities/taxons/remove', req: s.RemoveEntityTaxonRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
81
|
+
remove_entity_url: { method: 'POST', path: '/api/entities/urls/remove', req: s.RemoveEntityUrlRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
82
|
+
remove_playlist_item: { method: 'POST', path: '/api/entities/playlists/items/remove', req: s.RemovePlaylistItemRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
83
|
+
remove_playlist_items: { method: 'POST', path: '/api/entities/playlists/items/remove-bulk', req: s.RemovePlaylistItemsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
84
|
+
remove_rating: { method: 'POST', path: '/api/entities/ratings/remove', req: s.RemoveRatingRequestSchema, resp: s.RemoveRatingResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
85
|
+
reorder_playlist_items: { method: 'POST', path: '/api/entities/playlists/items/reorder', req: s.ReorderPlaylistItemsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
86
|
+
set_favorite: { method: 'POST', path: '/api/entities/favorites/set', req: s.SetFavoriteRequestSchema, resp: s.SetFavoriteResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
87
|
+
set_rating: { method: 'POST', path: '/api/entities/ratings/set', req: s.SetRatingRequestSchema, resp: s.SetRatingResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
88
|
+
},
|
|
64
89
|
music: {
|
|
65
90
|
activity_feed: { method: 'POST', path: '/api/analytics/feed', req: s.FeedRequestSchema, resp: s.FeedResponseSchema, auth: { type: 'authenticated' } as const },
|
|
66
91
|
add_album_taxon: { method: 'POST', path: '/api/taxonomy/album-links/add', req: s.AddAlbumTaxonRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
@@ -84,7 +109,7 @@ export const routes = {
|
|
|
84
109
|
confirm_mb_match: { method: 'POST', path: '/api/albums/mb-confirm', req: s.ConfirmMbMatchRequestSchema, resp: s.MbMatchActionResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
85
110
|
create_artist: { method: 'POST', path: '/api/music/artists', req: s.CreateArtistRequestSchema, resp: s.ArtistSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
86
111
|
create_fetch_job: { method: 'POST', path: '/api/music/fetch', req: s.FetchMediaParamsSchema, resp: s.JobResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
87
|
-
|
|
112
|
+
create_playback_session: { method: 'POST', path: '/api/analytics/sessions', req: s.CreatePlaybackSessionRequestSchema, resp: s.PlaybackSessionSchema, auth: { type: 'authenticated' } as const },
|
|
88
113
|
create_playlist: { method: 'POST', path: '/api/music/playlists', req: s.CreatePlaylistRequestSchema, resp: s.PlaylistSchema, auth: { type: 'role', role: 'member' } as const },
|
|
89
114
|
create_precheck_fetch_job: { method: 'POST', path: '/api/music/fetch/precheck', req: s.PreCheckFetchParamsSchema, resp: s.JobResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
90
115
|
create_taxon: { method: 'POST', path: '/api/taxonomy/taxons/create', req: s.CreateTaxonRequestSchema, resp: s.TaxonSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
@@ -93,7 +118,7 @@ export const routes = {
|
|
|
93
118
|
delete_artist: { method: 'POST', path: '/api/artists/delete', req: s.DeleteArtistRequestSchema, resp: s.DeleteArtistResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
94
119
|
delete_feed_event: { method: 'POST', path: '/api/analytics/feed/delete', req: s.DeleteFeedEventRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
95
120
|
delete_image: { method: 'POST', path: '/api/music/images/delete', req: s.DeleteImageRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
96
|
-
|
|
121
|
+
delete_playback_session: { method: 'POST', path: '/api/analytics/sessions/delete', req: s.DeletePlaybackSessionRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
97
122
|
delete_playlist: { method: 'POST', path: '/api/playlists/delete', req: s.DeletePlaylistRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
98
123
|
delete_song: { method: 'POST', path: '/api/songs/delete', req: s.DeleteSongRequestSchema, resp: s.DeleteSongResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
99
124
|
delete_tag: { method: 'POST', path: '/api/tags/delete', req: s.DeleteTagRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
@@ -118,12 +143,11 @@ export const routes = {
|
|
|
118
143
|
get_enrichment_progress: { method: 'POST', path: '/api/music/albums/enrichment/progress', req: s.GetEnrichmentProgressRequestSchema, resp: s.GetEnrichmentProgressResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
119
144
|
get_fetch_job: { method: 'POST', path: '/api/music/fetch/status', req: s.GetJobRequestSchema, resp: s.JobResponseSchema, auth: { type: 'authenticated' } as const },
|
|
120
145
|
get_job_status: { method: 'POST', path: '/api/jobs/status', req: s.GetJobsStatusRequestSchema, resp: s.GetJobsStatusResponseSchema, auth: { type: 'authenticated' } as const },
|
|
121
|
-
get_listen_session: { method: 'POST', path: '/api/analytics/sessions/get', req: s.GetListenSessionRequestSchema, resp: s.ListenSessionSchema, auth: { type: 'authenticated' } as const },
|
|
122
146
|
get_musicbrainz_release: { method: 'POST', path: '/api/musicbrainz/release', req: s.GetReleaseRequestSchema, resp: s.MbReleaseDetailSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
147
|
+
get_playback_session: { method: 'POST', path: '/api/analytics/sessions/get', req: s.GetPlaybackSessionRequestSchema, resp: s.PlaybackSessionSchema, auth: { type: 'authenticated' } as const },
|
|
123
148
|
get_playlist_by_id: { method: 'POST', path: '/api/music/playlists/get', req: s.GetPlaylistRequestSchema, resp: s.PlaylistSchema, auth: { type: 'authenticated' } as const },
|
|
124
149
|
get_playlist_etag: { method: 'POST', path: '/api/music/playlists/etag', req: s.GetPlaylistRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
125
150
|
get_playlist_images: { method: 'POST', path: '/api/playlists/images', req: s.GetPlaylistRequestSchema, resp: z.string().array(), auth: { type: 'authenticated' } as const },
|
|
126
|
-
get_rating_stats: { method: 'POST', path: '/api/ratings/stats', req: s.GetRatingStatsRequestSchema, resp: s.RatingStatsSchema, auth: { type: 'authenticated' } as const },
|
|
127
151
|
get_tag: { method: 'POST', path: '/api/tags/get', req: s.GetTagRequestSchema, resp: s.TagSchema, auth: { type: 'authenticated' } as const },
|
|
128
152
|
get_taxon: { method: 'POST', path: '/api/taxonomy/taxons/get', req: s.GetTaxonRequestSchema, resp: s.TaxonSchema, auth: { type: 'authenticated' } as const },
|
|
129
153
|
get_taxon_ancestors: { method: 'POST', path: '/api/taxonomy/taxons/ancestors', req: s.GetTaxonRequestSchema, resp: s.TaxonRefSchema.array(), auth: { type: 'authenticated' } as const },
|
|
@@ -135,13 +159,13 @@ export const routes = {
|
|
|
135
159
|
list_beloved: { method: 'POST', path: '/api/favorites/beloved', req: s.ListBelovedRequestSchema, resp: s.ListBelovedResponseSchema, auth: { type: 'authenticated' } as const },
|
|
136
160
|
list_favorites: { method: 'POST', path: '/api/favorites/list', req: s.ListFavoritesRequestSchema, resp: s.ListFavoritesResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
137
161
|
list_jobs: { method: 'POST', path: '/api/jobs/list', req: s.ListJobsRequestSchema, resp: s.JobResponseSchema.array(), auth: { type: 'authenticated' } as const },
|
|
138
|
-
list_listen_sessions: { method: 'POST', path: '/api/analytics/sessions/list', req: s.ListListenSessionsRequestSchema, resp: s.ListListenSessionsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
139
162
|
list_pending_import_review: { method: 'POST', path: '/api/music/import/pending', req: s.ListPendingReviewRequestSchema, resp: s.PendingReviewSessionSchema.array(), auth: { type: 'authenticated' } as const },
|
|
163
|
+
list_playback_sessions: { method: 'POST', path: '/api/analytics/sessions/list', req: s.ListPlaybackSessionsRequestSchema, resp: s.ListPlaybackSessionsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
140
164
|
list_playlists: { method: 'POST', path: '/api/music/playlists/list', req: s.QueryParamsSchema, resp: s.PlaylistsQueryResultSchema, auth: { type: 'authenticated' } as const },
|
|
141
165
|
list_related_artists: { method: 'POST', path: '/api/related-artists/list', req: s.ListRelatedArtistsRequestSchema, resp: s.ListRelatedArtistsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
142
166
|
list_related_artists_batch: { method: 'POST', path: '/api/related-artists/list-batch', req: s.ListRelatedArtistsBatchRequestSchema, resp: s.ListRelatedArtistsBatchResponseSchema, auth: { type: 'authenticated' } as const },
|
|
143
167
|
list_tags: { method: 'GET', path: '/api/tags/list', req: null, resp: s.TagSchema.array(), auth: { type: 'authenticated' } as const },
|
|
144
|
-
list_taxon_kinds: { method: 'POST', path: '/api/taxonomy/kinds/list', req:
|
|
168
|
+
list_taxon_kinds: { method: 'POST', path: '/api/taxonomy/kinds/list', req: s.ListTaxonKindsRequestSchema, resp: s.TaxonKindSchema.array(), auth: { type: 'authenticated' } as const },
|
|
145
169
|
list_taxon_parents_for_kind: { method: 'POST', path: '/api/taxonomy/parents/list-by-kind', req: s.ListTaxonParentsForKindRequestSchema, resp: s.TaxonParentEdgeSchema.array(), auth: { type: 'authenticated' } as const },
|
|
146
170
|
list_taxons_by_kind: { method: 'POST', path: '/api/taxonomy/taxons/list-by-kind', req: s.ListTaxonsByKindRequestSchema, resp: s.TaxonSchema.array(), auth: { type: 'authenticated' } as const },
|
|
147
171
|
listening_history: { method: 'POST', path: '/api/analytics/listening-history', req: s.ListeningHistoryRequestSchema, resp: s.ListeningHistoryResponseSchema, auth: { type: 'authenticated' } as const },
|
|
@@ -163,12 +187,12 @@ export const routes = {
|
|
|
163
187
|
query_taxons: { method: 'POST', path: '/api/taxonomy/taxons/query', req: s.QueryTaxonsRequestSchema, resp: s.TaxonsQueryResultSchema, auth: { type: 'authenticated' } as const },
|
|
164
188
|
recent_songs: { method: 'POST', path: '/api/songs/recent', req: s.RecentSongsRequestSchema, resp: s.SongSchema.array(), auth: { type: 'authenticated' } as const },
|
|
165
189
|
recently_added_albums: { method: 'POST', path: '/api/music/relations/recently-added-albums', req: s.RecentlyAddedAlbumsRequestSchema, resp: s.RecentlyAddedAlbumsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
166
|
-
record_play: { method: 'POST', path: '/api/analytics/play', req: s.RecordPlayRequestSchema, resp: s.EmptyResponseSchema, auth: { type: '
|
|
190
|
+
record_play: { method: 'POST', path: '/api/analytics/play', req: s.RecordPlayRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
167
191
|
record_playlist_play: { method: 'POST', path: '/api/playlists/record-play', req: s.GetPlaylistRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
192
|
+
record_video_play: { method: 'POST', path: '/api/analytics/video-play', req: s.RecordVideoPlayRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'authenticated' } as const },
|
|
168
193
|
reject_mb_match: { method: 'POST', path: '/api/albums/mb-reject', req: s.RejectMbMatchRequestSchema, resp: s.MbMatchActionResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
169
194
|
remove_album_taxon: { method: 'POST', path: '/api/taxonomy/album-links/remove', req: s.RemoveAlbumTaxonRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
170
195
|
remove_albums_tags: { method: 'POST', path: '/api/tags/albums/remove', req: s.RemoveAlbumsTagsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
171
|
-
remove_rating: { method: 'POST', path: '/api/ratings/remove', req: s.RemoveRatingRequestSchema, resp: s.RemoveRatingResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
172
196
|
remove_songs_from_playlist: { method: 'POST', path: '/api/playlists/remove-songs', req: s.RemoveSongsFromPlaylistRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
173
197
|
remove_taxon_parent: { method: 'POST', path: '/api/taxonomy/parents/remove', req: s.RemoveTaxonParentRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
174
198
|
reorder_playlist_songs: { method: 'POST', path: '/api/playlists/reorder', req: s.ReorderPlaylistSongsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
@@ -177,10 +201,8 @@ export const routes = {
|
|
|
177
201
|
search: { method: 'POST', path: '/api/music/search', req: s.SearchRequestSchema, resp: s.SearchResponseSchema, auth: { type: 'authenticated' } as const },
|
|
178
202
|
search_musicbrainz_releases: { method: 'POST', path: '/api/musicbrainz/search/releases', req: s.SearchReleasesRequestSchema, resp: s.MbSearchReleasesResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
179
203
|
set_album_taxons: { method: 'POST', path: '/api/taxonomy/album-links/set', req: s.SetAlbumTaxonsRequestSchema, resp: s.AlbumTaxonLinkSchema.array(), auth: { type: 'role', role: 'admin' } as const },
|
|
180
|
-
set_favorite: { method: 'POST', path: '/api/favorites/set', req: s.SetFavoriteRequestSchema, resp: s.SetFavoriteResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
181
204
|
set_mb_lookup_status: { method: 'POST', path: '/api/albums/set-mb-lookup-status', req: s.SetMbLookupStatusRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
182
205
|
set_primary_image: { method: 'POST', path: '/api/music/images/set-primary', req: s.SetPrimaryImageRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
183
|
-
set_rating: { method: 'POST', path: '/api/ratings/set', req: s.SetRatingRequestSchema, resp: s.SetRatingResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
184
206
|
set_related_artist_bandcamp: { method: 'POST', path: '/api/related-artists/set-bandcamp', req: s.SetRelatedArtistBandcampRequestSchema, resp: s.RelatedArtistApiSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
185
207
|
set_scalar_attribute: { method: 'POST', path: '/api/taxonomy/scalars/set', req: s.SetScalarAttributeRequestSchema, resp: s.ScalarAttributeSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
186
208
|
set_taxon_color: { method: 'POST', path: '/api/taxonomy/taxons/set-color', req: s.SetTaxonColorRequestSchema, resp: s.TaxonSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
@@ -201,13 +223,53 @@ export const routes = {
|
|
|
201
223
|
update_album: { method: 'POST', path: '/api/albums/update', req: s.UpdateAlbumRequestSchema, resp: s.AlbumSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
202
224
|
update_artist: { method: 'POST', path: '/api/artists/update', req: s.UpdateArtistRequestSchema, resp: s.ArtistSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
203
225
|
update_artist_metadata: { method: 'POST', path: '/api/artists/update-metadata', req: s.UpdateArtistMetadataRequestSchema, resp: s.UpdateArtistMetadataResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
update_playback_session_items: { method: 'POST', path: '/api/analytics/sessions/items', req: s.UpdatePlaybackSessionItemsRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner' } as const },
|
|
227
|
+
update_playback_session_progress: { method: 'POST', path: '/api/analytics/sessions/progress', req: s.UpdatePlaybackSessionProgressRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner' } as const },
|
|
228
|
+
update_playback_session_status: { method: 'POST', path: '/api/analytics/sessions/status', req: s.UpdatePlaybackSessionStatusRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'owner' } as const },
|
|
207
229
|
update_playlist: { method: 'POST', path: '/api/playlists/update', req: s.UpdatePlaylistRequestSchema, resp: s.PlaylistSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
208
230
|
update_songs: { method: 'POST', path: '/api/songs/update', req: s.UpdateSongsRequestSchema, resp: s.UpdateSongsResultSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
209
231
|
upload_image: { method: 'POST', path: '/api/upload/image', req: null, resp: s.ImageUploadResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
210
232
|
upload_music: { method: 'POST', path: '/api/upload/music', req: null, resp: s.MusicUploadResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
211
233
|
upload_music_by_blake3: { method: 'POST', path: '/api/upload/music-by-blake3', req: s.UploadMusicByBlake3RequestSchema, resp: s.MusicUploadResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
212
234
|
},
|
|
235
|
+
video: {
|
|
236
|
+
bulk_delete_videos: { method: 'POST', path: '/api/video/videos/bulk-delete', req: s.BulkDeleteVideosRequestSchema, resp: s.BulkDeleteVideosResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
237
|
+
create_video: { method: 'POST', path: '/api/video/videos', req: s.CreateVideoRequestSchema, resp: s.VideoSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
238
|
+
create_video_season: { method: 'POST', path: '/api/video/seasons', req: s.CreateVideoSeasonRequestSchema, resp: s.VideoSeasonSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
239
|
+
create_video_series: { method: 'POST', path: '/api/video/series', req: s.CreateVideoSeriesRequestSchema, resp: s.VideoSeriesSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
240
|
+
delete_video: { method: 'POST', path: '/api/video/videos/delete', req: s.DeleteVideoRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
241
|
+
delete_video_rendition: { method: 'POST', path: '/api/video/videos/renditions/delete', req: s.DeleteVideoRenditionRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
242
|
+
delete_video_season: { method: 'POST', path: '/api/video/seasons/delete', req: s.DeleteVideoSeasonRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
243
|
+
delete_video_series: { method: 'POST', path: '/api/video/series/delete', req: s.DeleteVideoSeriesRequestSchema, resp: s.EmptyResponseSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
244
|
+
get_playback_progress: { method: 'POST', path: '/api/video/progress/get', req: s.GetPlaybackProgressRequestSchema, resp: s.PlaybackProgressSchema.array(), auth: { type: 'authenticated' } as const },
|
|
245
|
+
get_video: { method: 'POST', path: '/api/video/videos/get', req: s.GetVideoRequestSchema, resp: s.VideoSchema, auth: { type: 'authenticated' } as const },
|
|
246
|
+
get_video_renditions: { method: 'POST', path: '/api/video/videos/renditions', req: s.GetVideoRenditionsRequestSchema, resp: s.VideoRenditionSchema.array(), auth: { type: 'authenticated' } as const },
|
|
247
|
+
get_video_season: { method: 'POST', path: '/api/video/seasons/get', req: s.GetVideoSeasonRequestSchema, resp: s.VideoSeasonSchema, auth: { type: 'authenticated' } as const },
|
|
248
|
+
get_video_series: { method: 'POST', path: '/api/video/series/get', req: s.GetVideoSeriesRequestSchema, resp: s.VideoSeriesSchema, auth: { type: 'authenticated' } as const },
|
|
249
|
+
get_video_series_detail: { method: 'POST', path: '/api/video/series/detail', req: s.GetVideoSeriesRequestSchema, resp: s.SeriesDetailSchema, auth: { type: 'authenticated' } as const },
|
|
250
|
+
get_video_with_metadata: { method: 'POST', path: '/api/video/videos/get-with-metadata', req: s.GetVideoRequestSchema, resp: s.VideoWithMetadataSchema, auth: { type: 'authenticated' } as const },
|
|
251
|
+
list_pending_video_import_review: { method: 'POST', path: '/api/video/import/pending', req: s.ListPendingVideoReviewRequestSchema, resp: s.PendingVideoReviewSessionSchema.array(), auth: { type: 'authenticated' } as const },
|
|
252
|
+
list_playback_progress: { method: 'POST', path: '/api/video/progress/list', req: s.ListPlaybackProgressRequestSchema, resp: s.PlaybackProgressSchema.array(), auth: { type: 'authenticated' } as const },
|
|
253
|
+
list_video_seasons: { method: 'POST', path: '/api/video/seasons/list', req: s.ListVideoSeasonsRequestSchema, resp: s.VideoSeasonSchema.array(), auth: { type: 'authenticated' } as const },
|
|
254
|
+
list_video_series: { method: 'POST', path: '/api/video/series/list', req: s.ListVideoSeriesRequestSchema, resp: s.VideoSeriesSchema.array(), auth: { type: 'authenticated' } as const },
|
|
255
|
+
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 },
|
|
256
|
+
list_videos_by_series: { method: 'POST', path: '/api/video/videos/list-by-series', req: s.ListVideosBySeriesRequestSchema, resp: s.VideoSchema.array(), auth: { type: 'authenticated' } as const },
|
|
257
|
+
list_videos_unattached: { method: 'POST', path: '/api/video/videos/list-unattached', req: s.ListVideosUnattachedRequestSchema, resp: s.VideoSchema.array(), auth: { type: 'authenticated' } as const },
|
|
258
|
+
mark_video_group_reviewed: { method: 'POST', path: '/api/video/import/mark-reviewed', req: s.MarkVideoGroupReviewedRequestSchema, resp: s.VideoImportReviewOkSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
259
|
+
move_video_review: { method: 'POST', path: '/api/video/import/move-video', req: s.MoveVideoReviewRequestSchema, resp: s.VideoImportReviewOkSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
260
|
+
patch_video_group_review: { method: 'POST', path: '/api/video/import/patch-group', req: s.PatchVideoGroupReviewRequestSchema, resp: s.VideoImportReviewOkSchema, auth: { type: 'owner_or', role: 'admin' } as const },
|
|
261
|
+
query_video_series: { method: 'POST', path: '/api/video/series/query', req: s.QueryParamsSchema, resp: s.SeriesQueryResultSchema, auth: { type: 'authenticated' } as const },
|
|
262
|
+
query_videos: { method: 'POST', path: '/api/video/videos/query', req: s.QueryVideosRequestSchema, resp: s.VideosQueryResultSchema, auth: { type: 'authenticated' } as const },
|
|
263
|
+
recently_added_videos: { method: 'POST', path: '/api/video/relations/recently-added-videos', req: s.RecentlyAddedVideosRequestSchema, resp: s.RecentlyAddedVideosResponseSchema, auth: { type: 'authenticated' } as const },
|
|
264
|
+
sync_video_by_blake3: { method: 'POST', path: '/api/sync/video-by-blake3', req: s.SyncVideoByBlake3RequestSchema, resp: s.SyncVideoByBlake3ResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
265
|
+
unassigned_videos: { method: 'POST', path: '/api/video/relations/unassigned-videos', req: s.UnassignedVideosRequestSchema, resp: s.UnassignedVideosResponseSchema, auth: { type: 'authenticated' } as const },
|
|
266
|
+
update_video_season: { method: 'POST', path: '/api/video/seasons/update', req: s.UpdateVideoSeasonRequestSchema, resp: s.VideoSeasonSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
267
|
+
update_video_series: { method: 'POST', path: '/api/video/series/update', req: s.UpdateVideoSeriesRequestSchema, resp: s.VideoSeriesSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
268
|
+
update_videos: { method: 'POST', path: '/api/video/videos/update', req: s.UpdateVideosRequestSchema, resp: s.UpdateVideosResultSchema, auth: { type: 'role', role: 'admin' } as const },
|
|
269
|
+
upload_video: { method: 'POST', path: '/api/upload/video', req: null, resp: s.VideoUploadResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
270
|
+
upload_video_by_blake3: { method: 'POST', path: '/api/upload/video-by-blake3', req: s.UploadVideoByBlake3RequestSchema, resp: s.VideoUploadResponseSchema, auth: { type: 'role', role: 'member' } as const },
|
|
271
|
+
upsert_playback_progress: { method: 'POST', path: '/api/video/progress/upsert', req: s.UpsertPlaybackProgressRequestSchema, resp: s.PlaybackProgressSchema, auth: { type: 'authenticated' } as const },
|
|
272
|
+
video_pending: { method: 'POST', path: '/api/video/import/video-pending', req: s.VideoPendingRequestSchema, resp: s.VideoPendingResponseSchema, auth: { type: 'authenticated' } as const },
|
|
273
|
+
videos_by_value: { method: 'POST', path: '/api/video/relations/videos-by-value', req: s.VideosByValueRequestSchema, resp: s.VideosByValueResponseSchema, auth: { type: 'authenticated' } as const },
|
|
274
|
+
},
|
|
213
275
|
} as const;
|