sonovault 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rekordcloud B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # sonovault
2
+
3
+ [![CI](https://github.com/rekordcloud/sonovault-js/actions/workflows/ci.yml/badge.svg)](https://github.com/rekordcloud/sonovault-js/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/sonovault)](https://www.npmjs.com/package/sonovault)
5
+
6
+ TypeScript/Node client for the **[SonoVault](https://sonovault.now)** music metadata API — 90M+ tracks with ISRC, ISWC, genre, record label, canonical release dates, and cross-platform IDs for Spotify, Apple Music, Tidal, Beatport, Discogs, and MusicBrainz, resolved in a single call.
7
+
8
+ - **One key, no OAuth** — a single `x-api-key` header, no approval queue.
9
+ - **Free tier** — 1,000 requests/month, no credit card: [get an API key](https://sonovault.now).
10
+ - **Docs** — full API reference at [sonovault.now/docs](https://sonovault.now/docs).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install sonovault
16
+ ```
17
+
18
+ Node 18+ (uses the built-in `fetch`). ESM and CommonJS both supported.
19
+
20
+ ## Quickstart
21
+
22
+ ```ts
23
+ import { SonoVault } from "sonovault";
24
+
25
+ const sv = new SonoVault({ apiKey: process.env.SONOVAULT_API_KEY! });
26
+
27
+ // Find a track's ISRC from artist + title
28
+ const { results } = await sv.tracks.search({ artist: "Daft Punk", title: "One More Time" });
29
+ console.log(results[0].isrc); // "GBDUW0000053"
30
+ console.log(results[0].genre, results[0].releases[0]?.label?.name);
31
+
32
+ // Resolve that ISRC to its ID on every platform
33
+ const { links } = await sv.tracks.links({ isrc: "GBDUW0000053" });
34
+ for (const link of links) {
35
+ console.log(link.source, link.url); // spotify https://open.spotify.com/track/…
36
+ }
37
+
38
+ // Recording → composition (ISWC), for royalty/publishing workflows
39
+ const work = await sv.tracks.iswc({ isrc: "GBDUW0000053" });
40
+ ```
41
+
42
+ ## Bulk resolve
43
+
44
+ Resolve up to 100 lines — track names, ISRCs, or platform IDs — in one request (great for enriching play logs and library exports):
45
+
46
+ ```ts
47
+ const batch = await sv.tracks.resolve({
48
+ input_type: "track_name",
49
+ items: [
50
+ { artist: "Daft Punk", title: "Harder, Better, Faster, Stronger" },
51
+ { artist: "Daft Punk", title: "Around the World" },
52
+ ],
53
+ });
54
+
55
+ for (const row of batch.results) {
56
+ console.log(row.status, row.track?.isrc, row.track?.releases[0]?.label?.name);
57
+ }
58
+ ```
59
+
60
+ ## Pagination
61
+
62
+ List endpoints return `{ results, next_cursor }` — pass the cursor back to get the next page (`next_cursor` is `null` on the last page):
63
+
64
+ ```ts
65
+ let cursor: string | undefined;
66
+ do {
67
+ const page = await sv.artists.releases(42, { cursor });
68
+ // …use page.results
69
+ cursor = page.next_cursor ?? undefined;
70
+ } while (cursor);
71
+ ```
72
+
73
+ ## Error handling
74
+
75
+ Non-2xx responses throw a typed `SonoVaultError`:
76
+
77
+ ```ts
78
+ import { SonoVaultError } from "sonovault";
79
+
80
+ try {
81
+ await sv.tracks.browse({ genre: "House" }); // paid-tier endpoint
82
+ } catch (err) {
83
+ if (err instanceof SonoVaultError) {
84
+ console.log(err.status, err.isForbidden, err.message);
85
+ }
86
+ }
87
+ ```
88
+
89
+ Rate-limited responses that carry a `Retry-After` header are retried automatically (twice by default; configure with `maxRetries`).
90
+
91
+ ## API coverage
92
+
93
+ | Namespace | Methods |
94
+ |---|---|
95
+ | `sv.tracks` | `search`, `get`, `byIsrc`, `iswc`, `byIswc`, `links`, `resolve`, `identify`, `identifyAudio`, `browse` |
96
+ | `sv.artists` | `search`, `get`, `releases` |
97
+ | `sv.labels` | `search`, `get`, `releases`, `artists` |
98
+ | `sv.releases` | `search`, `get`, `latest` |
99
+ | `sv.genres` | `list` |
100
+ | `sv.suggestions` | `submit`, `list` |
101
+ | `sv.streams` | `create`, `list`, `get`, `update`, `history`, `report`, `live`, `stop` |
102
+ | `sv.webhooks` | `create`, `list`, `update`, `delete`, `test`, `deliveries` |
103
+
104
+ Some endpoints (audio identify, browse, charts, stream monitoring) need a paid tier — see [pricing](https://sonovault.now/pricing). Everything else works on the free tier.
105
+
106
+ ## Related
107
+
108
+ - [SonoVault API docs](https://sonovault.now/docs) — full endpoint reference with examples in 8 languages
109
+ - [sonovault-python](https://github.com/rekordcloud/sonovault-python) — the Python client
110
+ - [Free ISRC lookup](https://sonovault.now/isrc-lookup) · [ISWC lookup](https://sonovault.now/iswc-lookup) — browser tools built on the same API
111
+
112
+ ## License
113
+
114
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SonoVault: () => SonoVault,
24
+ SonoVaultError: () => SonoVaultError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/error.ts
29
+ var SonoVaultError = class extends Error {
30
+ constructor(message, status, body = void 0) {
31
+ super(message);
32
+ this.name = "SonoVaultError";
33
+ this.status = status;
34
+ this.body = body;
35
+ }
36
+ /** Missing or invalid API key. */
37
+ get isAuthError() {
38
+ return this.status === 401;
39
+ }
40
+ /** The endpoint needs a paid tier (or an admin key). */
41
+ get isForbidden() {
42
+ return this.status === 403;
43
+ }
44
+ /** Rate limit or monthly credit quota hit. */
45
+ get isRateLimited() {
46
+ return this.status === 429;
47
+ }
48
+ };
49
+
50
+ // src/client.ts
51
+ var SonoVault = class {
52
+ constructor(options) {
53
+ this.tracks = {
54
+ /** Search by artist + title (both required — there is no free-text query). */
55
+ search: (params) => this.request("/v1/tracks/search", { query: params }),
56
+ /** Fetch a track by its SonoVault ID. */
57
+ get: (id) => this.request(`/v1/tracks/${id}`),
58
+ /** Look up a track by any of its ISRCs. */
59
+ byIsrc: (isrc) => this.request(`/v1/tracks/isrc/${encodeURIComponent(isrc)}`),
60
+ /** Recording → composition: the ISWC(s) behind a recording, by ISRC or track ID. */
61
+ iswc: (params) => this.request("/v1/tracks/iswc", { query: params }),
62
+ /** Composition → recordings: every recording of a work, by ISWC. */
63
+ byIswc: (iswc, params = {}) => this.request(
64
+ `/v1/tracks/iswc/${encodeURIComponent(iswc)}`,
65
+ { query: params }
66
+ ),
67
+ /** Cross-platform IDs + deep links for a track, resolved from any platform's ID or an ISRC. */
68
+ links: (params) => this.request("/v1/tracks/links", { query: params }),
69
+ /** Resolve up to 100 track names, ISRCs, or platform IDs in one request. */
70
+ resolve: (body) => this.request("/v1/tracks/resolve", { method: "POST", json: body }),
71
+ /** Identify a track from a Chromaprint fingerprint (`fpcalc -raw`). Paid tiers. */
72
+ identify: (body) => this.request("/v1/tracks/identify", { method: "POST", json: body }),
73
+ /**
74
+ * Identify a track from raw audio bytes (any ffmpeg-decodable format).
75
+ * Send the whole track when you can — the matching section is often mid-track.
76
+ * Paid tiers; costs 10 + ceil(MB) credits.
77
+ */
78
+ identifyAudio: (audio, params = {}) => this.request("/v1/tracks/identify", {
79
+ method: "POST",
80
+ query: params,
81
+ raw: { body: audio, contentType: "application/octet-stream" }
82
+ }),
83
+ /** Browse the catalog by label, artist, genre, or year. Paid tiers. */
84
+ browse: (params) => this.request("/v1/tracks/browse", { query: params })
85
+ };
86
+ this.artists = {
87
+ search: (params) => this.request("/v1/artists/search", { query: params }),
88
+ get: (id) => this.request(`/v1/artists/${id}`),
89
+ releases: (id, params = {}) => this.request(`/v1/artists/${id}/releases`, { query: params })
90
+ };
91
+ this.labels = {
92
+ search: (params) => this.request("/v1/labels/search", { query: params }),
93
+ get: (id) => this.request(`/v1/labels/${id}`),
94
+ releases: (id, params = {}) => this.request(`/v1/labels/${id}/releases`, { query: params }),
95
+ artists: (id, params = {}) => this.request(`/v1/labels/${id}/artists`, { query: params })
96
+ };
97
+ this.releases = {
98
+ search: (params) => this.request("/v1/releases/search", { query: params }),
99
+ get: (id) => this.request(`/v1/releases/${id}`),
100
+ /** Newly released albums (GET /v1/releases/new). Paid tiers. */
101
+ latest: (params = {}) => this.request("/v1/releases/new", { query: params })
102
+ };
103
+ this.genres = {
104
+ /** The canonical genre/subgenre hierarchy. */
105
+ list: () => this.request("/v1/genres")
106
+ };
107
+ this.suggestions = {
108
+ /** Suggest a metadata correction for a track. Paid tiers. */
109
+ submit: (trackId, body) => this.request(`/v1/tracks/${trackId}/suggestions`, {
110
+ method: "POST",
111
+ json: body
112
+ }),
113
+ list: (params = {}) => this.request("/v1/suggestions", { query: params })
114
+ };
115
+ this.streams = {
116
+ /** Start monitoring an Icecast/Shoutcast stream. Paid tiers. */
117
+ create: (body) => this.request("/v1/streams", { method: "POST", json: body }),
118
+ list: () => this.request("/v1/streams"),
119
+ get: (id) => this.request(`/v1/streams/${id}`),
120
+ update: (id, body) => this.request(`/v1/streams/${id}`, { method: "PATCH", json: body }),
121
+ history: (id, params = {}) => this.request(`/v1/streams/${id}/history`, { query: params }),
122
+ report: (params) => this.request("/v1/streams/report", { query: params }),
123
+ /** What's playing right now across your monitored streams. */
124
+ live: () => this.request("/v1/streams/live"),
125
+ /** Stop monitoring a stream. */
126
+ stop: (id) => this.request(`/v1/streams/${id}`, { method: "DELETE" })
127
+ };
128
+ this.webhooks = {
129
+ /** Register an endpoint for stream events. The response includes `secret` once — store it. */
130
+ create: (body) => this.request("/v1/webhooks", { method: "POST", json: body }),
131
+ list: () => this.request("/v1/webhooks"),
132
+ update: (id, body) => this.request(`/v1/webhooks/${id}`, { method: "PATCH", json: body }),
133
+ delete: (id) => this.request(`/v1/webhooks/${id}`, { method: "DELETE" }),
134
+ test: (id) => this.request(`/v1/webhooks/${id}/test`, { method: "POST" }),
135
+ deliveries: (id) => this.request(`/v1/webhooks/${id}/deliveries`)
136
+ };
137
+ if (!options?.apiKey) throw new Error("SonoVault: apiKey is required");
138
+ this.apiKey = options.apiKey;
139
+ this.baseUrl = (options.baseUrl ?? "https://api.sonovault.now").replace(/\/$/, "");
140
+ this.maxRetries = options.maxRetries ?? 2;
141
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
142
+ }
143
+ async request(path, opts = {}) {
144
+ const url = new URL(this.baseUrl + path);
145
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
146
+ if (value !== void 0) url.searchParams.set(key, String(value));
147
+ }
148
+ const headers = { "x-api-key": this.apiKey };
149
+ let body;
150
+ if (opts.json !== void 0) {
151
+ headers["Content-Type"] = "application/json";
152
+ body = JSON.stringify(opts.json);
153
+ } else if (opts.raw) {
154
+ headers["Content-Type"] = opts.raw.contentType;
155
+ body = opts.raw.body;
156
+ }
157
+ let lastError;
158
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
159
+ let res;
160
+ try {
161
+ res = await this.fetchImpl(url, { method: opts.method ?? "GET", headers, body });
162
+ } catch (err) {
163
+ lastError = new SonoVaultError(`Network error: ${err.message}`, 0);
164
+ continue;
165
+ }
166
+ if (res.ok) {
167
+ if (res.status === 204) return void 0;
168
+ return await res.json();
169
+ }
170
+ const errBody = await res.json().catch(() => void 0);
171
+ const message = errBody?.error ?? errBody?.message ?? `HTTP ${res.status}`;
172
+ lastError = new SonoVaultError(message, res.status, errBody);
173
+ const retryAfter = res.headers.get("retry-after");
174
+ const retryable = res.status >= 500 || res.status === 429 && retryAfter !== null;
175
+ if (!retryable || attempt === this.maxRetries) throw lastError;
176
+ const delayMs = retryAfter ? Number(retryAfter) * 1e3 : 500 * 2 ** attempt;
177
+ await new Promise((r) => setTimeout(r, delayMs));
178
+ }
179
+ throw lastError ?? new SonoVaultError("Request failed", 0);
180
+ }
181
+ };
182
+ // Annotate the CommonJS export names for ESM import in node:
183
+ 0 && (module.exports = {
184
+ SonoVault,
185
+ SonoVaultError
186
+ });
@@ -0,0 +1,347 @@
1
+ /** An artist credit on a track. */
2
+ interface TrackArtist {
3
+ id: number;
4
+ name: string;
5
+ is_primary?: boolean;
6
+ is_remixer?: boolean;
7
+ }
8
+ /** A release a track appears on, with its artist, label, and release date. */
9
+ interface TrackRelease {
10
+ id: number;
11
+ title: string;
12
+ artist: {
13
+ id: number;
14
+ name: string;
15
+ };
16
+ label: {
17
+ id: number;
18
+ name: string;
19
+ } | null;
20
+ release_date: string | null;
21
+ }
22
+ /**
23
+ * The public track shape returned by search, lookups, and resolve.
24
+ * Note: audio features (BPM, key, energy, …) are not exposed on the public API.
25
+ */
26
+ interface Track {
27
+ id: number;
28
+ title: string;
29
+ releases: TrackRelease[];
30
+ artists: TrackArtist[];
31
+ isrc: string | null;
32
+ duration: number | null;
33
+ genre: string | null;
34
+ subgenre: string | null;
35
+ }
36
+ /** A cursor-paginated page. `next_cursor` is null on the last page. */
37
+ interface Page<T> {
38
+ results: T[];
39
+ next_cursor: string | null;
40
+ }
41
+ interface Artist {
42
+ id: number;
43
+ name: string;
44
+ [key: string]: unknown;
45
+ }
46
+ interface Label {
47
+ id: number;
48
+ name: string;
49
+ [key: string]: unknown;
50
+ }
51
+ interface Release {
52
+ id: number;
53
+ title: string;
54
+ artist?: {
55
+ id: number;
56
+ name: string;
57
+ };
58
+ label?: {
59
+ id: number;
60
+ name: string;
61
+ } | null;
62
+ release_date?: string | null;
63
+ tracks?: Track[];
64
+ [key: string]: unknown;
65
+ }
66
+ interface Genre {
67
+ id: number;
68
+ name: string;
69
+ subgenres?: {
70
+ id: number;
71
+ name: string;
72
+ }[];
73
+ [key: string]: unknown;
74
+ }
75
+ /** A track's ID on an external platform, with a deep link. */
76
+ interface PlatformLink {
77
+ source: string;
78
+ external_id: string;
79
+ url: string | null;
80
+ }
81
+ interface PlatformLinksResponse {
82
+ track_id: number;
83
+ title: string;
84
+ links: PlatformLink[];
85
+ [key: string]: unknown;
86
+ }
87
+ /** ISWC entries for a recording (one recording can carry several work codes). */
88
+ interface IswcLookupResponse {
89
+ sonovault_id?: number;
90
+ isrc?: string;
91
+ iswcs?: {
92
+ iswc: string;
93
+ title: string | null;
94
+ }[];
95
+ [key: string]: unknown;
96
+ }
97
+ type ResolveInputType = "track_name" | "isrc" | "sonovault_id" | "spotify_id" | "applemusic_id" | "tidal_id" | "beatport_id" | "discogs_id" | "musicbrainz_id";
98
+ interface ResolveRequest {
99
+ input_type: ResolveInputType;
100
+ /** 1–100 entries. `{ artist, title }` objects for `track_name`, strings otherwise. */
101
+ items: (string | {
102
+ artist: string;
103
+ title: string;
104
+ })[];
105
+ }
106
+ interface ResolveResult {
107
+ input: string | {
108
+ artist: string;
109
+ title: string;
110
+ };
111
+ status: "matched" | "not_found" | "skipped_no_credits";
112
+ track: Track | null;
113
+ links: PlatformLink[];
114
+ }
115
+ interface ResolveResponse {
116
+ results: ResolveResult[];
117
+ partial: boolean;
118
+ processed: number;
119
+ credits_used: number;
120
+ credits_remaining: number;
121
+ message: string | null;
122
+ }
123
+ interface IdentifyRequest {
124
+ /** 50–50,000 integers from `fpcalc -raw` (Chromaprint). */
125
+ fingerprint: number[];
126
+ /** Clip duration in seconds. */
127
+ fingerprint_duration?: number;
128
+ /** Max results to return, 1–25. */
129
+ top_n?: number;
130
+ }
131
+ interface IdentifyResult {
132
+ id: number;
133
+ title: string;
134
+ artists: TrackArtist[];
135
+ /** 0–1; higher means a more certain match. */
136
+ confidence: number;
137
+ }
138
+ interface IdentifyResponse {
139
+ matched: boolean;
140
+ results: IdentifyResult[];
141
+ credits_charged: number;
142
+ [key: string]: unknown;
143
+ }
144
+ interface Stream {
145
+ id: string;
146
+ url?: string;
147
+ name?: string;
148
+ status?: string;
149
+ [key: string]: unknown;
150
+ }
151
+ interface Webhook {
152
+ id: string;
153
+ url: string;
154
+ event_types?: string[];
155
+ /** Returned once, on creation — store it to verify delivery signatures. */
156
+ secret?: string;
157
+ [key: string]: unknown;
158
+ }
159
+
160
+ interface SonoVaultOptions {
161
+ /** Your API key — get a free one at https://sonovault.now (1,000 requests/month). */
162
+ apiKey: string;
163
+ /** Override the API base URL. Defaults to https://api.sonovault.now. */
164
+ baseUrl?: string;
165
+ /** Retries on 429/5xx responses. Default 2; set 0 to disable. */
166
+ maxRetries?: number;
167
+ /** Custom fetch implementation (for testing or polyfills). */
168
+ fetch?: typeof globalThis.fetch;
169
+ }
170
+ declare class SonoVault {
171
+ private readonly apiKey;
172
+ private readonly baseUrl;
173
+ private readonly maxRetries;
174
+ private readonly fetchImpl;
175
+ constructor(options: SonoVaultOptions);
176
+ private request;
177
+ readonly tracks: {
178
+ /** Search by artist + title (both required — there is no free-text query). */
179
+ search: (params: {
180
+ artist: string;
181
+ title: string;
182
+ limit?: number;
183
+ cursor?: string;
184
+ }) => Promise<Page<Track>>;
185
+ /** Fetch a track by its SonoVault ID. */
186
+ get: (id: number) => Promise<Track>;
187
+ /** Look up a track by any of its ISRCs. */
188
+ byIsrc: (isrc: string) => Promise<Track>;
189
+ /** Recording → composition: the ISWC(s) behind a recording, by ISRC or track ID. */
190
+ iswc: (params: {
191
+ isrc?: string;
192
+ id?: number;
193
+ }) => Promise<IswcLookupResponse>;
194
+ /** Composition → recordings: every recording of a work, by ISWC. */
195
+ byIswc: (iswc: string, params?: {
196
+ limit?: number;
197
+ }) => Promise<{
198
+ iswc: string;
199
+ results: Track[];
200
+ }>;
201
+ /** Cross-platform IDs + deep links for a track, resolved from any platform's ID or an ISRC. */
202
+ links: (params: {
203
+ id?: number;
204
+ isrc?: string;
205
+ spotify_id?: string;
206
+ beatport_id?: string;
207
+ discogs_id?: string;
208
+ musicbrainz_id?: string;
209
+ applemusic_id?: string;
210
+ tidal_id?: string;
211
+ youtube_id?: string;
212
+ }) => Promise<PlatformLinksResponse>;
213
+ /** Resolve up to 100 track names, ISRCs, or platform IDs in one request. */
214
+ resolve: (body: ResolveRequest) => Promise<ResolveResponse>;
215
+ /** Identify a track from a Chromaprint fingerprint (`fpcalc -raw`). Paid tiers. */
216
+ identify: (body: IdentifyRequest) => Promise<IdentifyResponse>;
217
+ /**
218
+ * Identify a track from raw audio bytes (any ffmpeg-decodable format).
219
+ * Send the whole track when you can — the matching section is often mid-track.
220
+ * Paid tiers; costs 10 + ceil(MB) credits.
221
+ */
222
+ identifyAudio: (audio: ArrayBuffer | Uint8Array | Blob, params?: {
223
+ length?: number;
224
+ top_n?: number;
225
+ }) => Promise<IdentifyResponse>;
226
+ /** Browse the catalog by label, artist, genre, or year. Paid tiers. */
227
+ browse: (params: {
228
+ labelId?: number;
229
+ artistId?: number;
230
+ genre?: string;
231
+ genreId?: number;
232
+ year?: number;
233
+ randomize?: boolean;
234
+ limit?: number;
235
+ cursor?: string;
236
+ }) => Promise<Page<Track>>;
237
+ };
238
+ readonly artists: {
239
+ search: (params: {
240
+ name: string;
241
+ limit?: number;
242
+ cursor?: string;
243
+ }) => Promise<Page<Artist>>;
244
+ get: (id: number) => Promise<Artist>;
245
+ releases: (id: number, params?: {
246
+ limit?: number;
247
+ cursor?: string;
248
+ }) => Promise<Page<Release>>;
249
+ };
250
+ readonly labels: {
251
+ search: (params: {
252
+ name: string;
253
+ limit?: number;
254
+ cursor?: string;
255
+ }) => Promise<Page<Label>>;
256
+ get: (id: number) => Promise<Label>;
257
+ releases: (id: number, params?: {
258
+ limit?: number;
259
+ cursor?: string;
260
+ }) => Promise<Page<Release>>;
261
+ artists: (id: number, params?: {
262
+ limit?: number;
263
+ cursor?: string;
264
+ }) => Promise<Page<Artist>>;
265
+ };
266
+ readonly releases: {
267
+ search: (params: {
268
+ title: string;
269
+ artist?: string;
270
+ limit?: number;
271
+ cursor?: string;
272
+ }) => Promise<Page<Release>>;
273
+ get: (id: number) => Promise<Release>;
274
+ /** Newly released albums (GET /v1/releases/new). Paid tiers. */
275
+ latest: (params?: {
276
+ limit?: number;
277
+ cursor?: string;
278
+ }) => Promise<Page<Release>>;
279
+ };
280
+ readonly genres: {
281
+ /** The canonical genre/subgenre hierarchy. */
282
+ list: () => Promise<{
283
+ genres: Genre[];
284
+ }>;
285
+ };
286
+ readonly suggestions: {
287
+ /** Suggest a metadata correction for a track. Paid tiers. */
288
+ submit: (trackId: number, body: Record<string, unknown>) => Promise<Record<string, unknown>>;
289
+ list: (params?: {
290
+ limit?: number;
291
+ cursor?: string;
292
+ }) => Promise<Page<Record<string, unknown>>>;
293
+ };
294
+ readonly streams: {
295
+ /** Start monitoring an Icecast/Shoutcast stream. Paid tiers. */
296
+ create: (body: Record<string, unknown>) => Promise<Stream>;
297
+ list: () => Promise<{
298
+ streams: Stream[];
299
+ }>;
300
+ get: (id: string) => Promise<Stream>;
301
+ update: (id: string, body: Record<string, unknown>) => Promise<Stream>;
302
+ history: (id: string, params?: {
303
+ since?: string;
304
+ }) => Promise<Record<string, unknown>>;
305
+ report: (params: {
306
+ from: string;
307
+ until: string;
308
+ stream_id?: string;
309
+ }) => Promise<Record<string, unknown>>;
310
+ /** What's playing right now across your monitored streams. */
311
+ live: () => Promise<Record<string, unknown>>;
312
+ /** Stop monitoring a stream. */
313
+ stop: (id: string) => Promise<void>;
314
+ };
315
+ readonly webhooks: {
316
+ /** Register an endpoint for stream events. The response includes `secret` once — store it. */
317
+ create: (body: {
318
+ url: string;
319
+ event_types?: string[];
320
+ description?: string;
321
+ }) => Promise<Webhook>;
322
+ list: () => Promise<{
323
+ webhooks: Webhook[];
324
+ }>;
325
+ update: (id: string, body: Record<string, unknown>) => Promise<Webhook>;
326
+ delete: (id: string) => Promise<void>;
327
+ test: (id: string) => Promise<Record<string, unknown>>;
328
+ deliveries: (id: string) => Promise<Record<string, unknown>>;
329
+ };
330
+ }
331
+
332
+ /** Error thrown for any non-2xx API response. */
333
+ declare class SonoVaultError extends Error {
334
+ /** HTTP status code, or 0 for network errors. */
335
+ readonly status: number;
336
+ /** Parsed JSON error body, when the API returned one. */
337
+ readonly body: unknown;
338
+ constructor(message: string, status: number, body?: unknown);
339
+ /** Missing or invalid API key. */
340
+ get isAuthError(): boolean;
341
+ /** The endpoint needs a paid tier (or an admin key). */
342
+ get isForbidden(): boolean;
343
+ /** Rate limit or monthly credit quota hit. */
344
+ get isRateLimited(): boolean;
345
+ }
346
+
347
+ export { type Artist, type Genre, type IdentifyRequest, type IdentifyResponse, type IdentifyResult, type IswcLookupResponse, type Label, type Page, type PlatformLink, type PlatformLinksResponse, type Release, type ResolveInputType, type ResolveRequest, type ResolveResponse, type ResolveResult, SonoVault, SonoVaultError, type SonoVaultOptions, type Stream, type Track, type TrackArtist, type TrackRelease, type Webhook };
@@ -0,0 +1,347 @@
1
+ /** An artist credit on a track. */
2
+ interface TrackArtist {
3
+ id: number;
4
+ name: string;
5
+ is_primary?: boolean;
6
+ is_remixer?: boolean;
7
+ }
8
+ /** A release a track appears on, with its artist, label, and release date. */
9
+ interface TrackRelease {
10
+ id: number;
11
+ title: string;
12
+ artist: {
13
+ id: number;
14
+ name: string;
15
+ };
16
+ label: {
17
+ id: number;
18
+ name: string;
19
+ } | null;
20
+ release_date: string | null;
21
+ }
22
+ /**
23
+ * The public track shape returned by search, lookups, and resolve.
24
+ * Note: audio features (BPM, key, energy, …) are not exposed on the public API.
25
+ */
26
+ interface Track {
27
+ id: number;
28
+ title: string;
29
+ releases: TrackRelease[];
30
+ artists: TrackArtist[];
31
+ isrc: string | null;
32
+ duration: number | null;
33
+ genre: string | null;
34
+ subgenre: string | null;
35
+ }
36
+ /** A cursor-paginated page. `next_cursor` is null on the last page. */
37
+ interface Page<T> {
38
+ results: T[];
39
+ next_cursor: string | null;
40
+ }
41
+ interface Artist {
42
+ id: number;
43
+ name: string;
44
+ [key: string]: unknown;
45
+ }
46
+ interface Label {
47
+ id: number;
48
+ name: string;
49
+ [key: string]: unknown;
50
+ }
51
+ interface Release {
52
+ id: number;
53
+ title: string;
54
+ artist?: {
55
+ id: number;
56
+ name: string;
57
+ };
58
+ label?: {
59
+ id: number;
60
+ name: string;
61
+ } | null;
62
+ release_date?: string | null;
63
+ tracks?: Track[];
64
+ [key: string]: unknown;
65
+ }
66
+ interface Genre {
67
+ id: number;
68
+ name: string;
69
+ subgenres?: {
70
+ id: number;
71
+ name: string;
72
+ }[];
73
+ [key: string]: unknown;
74
+ }
75
+ /** A track's ID on an external platform, with a deep link. */
76
+ interface PlatformLink {
77
+ source: string;
78
+ external_id: string;
79
+ url: string | null;
80
+ }
81
+ interface PlatformLinksResponse {
82
+ track_id: number;
83
+ title: string;
84
+ links: PlatformLink[];
85
+ [key: string]: unknown;
86
+ }
87
+ /** ISWC entries for a recording (one recording can carry several work codes). */
88
+ interface IswcLookupResponse {
89
+ sonovault_id?: number;
90
+ isrc?: string;
91
+ iswcs?: {
92
+ iswc: string;
93
+ title: string | null;
94
+ }[];
95
+ [key: string]: unknown;
96
+ }
97
+ type ResolveInputType = "track_name" | "isrc" | "sonovault_id" | "spotify_id" | "applemusic_id" | "tidal_id" | "beatport_id" | "discogs_id" | "musicbrainz_id";
98
+ interface ResolveRequest {
99
+ input_type: ResolveInputType;
100
+ /** 1–100 entries. `{ artist, title }` objects for `track_name`, strings otherwise. */
101
+ items: (string | {
102
+ artist: string;
103
+ title: string;
104
+ })[];
105
+ }
106
+ interface ResolveResult {
107
+ input: string | {
108
+ artist: string;
109
+ title: string;
110
+ };
111
+ status: "matched" | "not_found" | "skipped_no_credits";
112
+ track: Track | null;
113
+ links: PlatformLink[];
114
+ }
115
+ interface ResolveResponse {
116
+ results: ResolveResult[];
117
+ partial: boolean;
118
+ processed: number;
119
+ credits_used: number;
120
+ credits_remaining: number;
121
+ message: string | null;
122
+ }
123
+ interface IdentifyRequest {
124
+ /** 50–50,000 integers from `fpcalc -raw` (Chromaprint). */
125
+ fingerprint: number[];
126
+ /** Clip duration in seconds. */
127
+ fingerprint_duration?: number;
128
+ /** Max results to return, 1–25. */
129
+ top_n?: number;
130
+ }
131
+ interface IdentifyResult {
132
+ id: number;
133
+ title: string;
134
+ artists: TrackArtist[];
135
+ /** 0–1; higher means a more certain match. */
136
+ confidence: number;
137
+ }
138
+ interface IdentifyResponse {
139
+ matched: boolean;
140
+ results: IdentifyResult[];
141
+ credits_charged: number;
142
+ [key: string]: unknown;
143
+ }
144
+ interface Stream {
145
+ id: string;
146
+ url?: string;
147
+ name?: string;
148
+ status?: string;
149
+ [key: string]: unknown;
150
+ }
151
+ interface Webhook {
152
+ id: string;
153
+ url: string;
154
+ event_types?: string[];
155
+ /** Returned once, on creation — store it to verify delivery signatures. */
156
+ secret?: string;
157
+ [key: string]: unknown;
158
+ }
159
+
160
+ interface SonoVaultOptions {
161
+ /** Your API key — get a free one at https://sonovault.now (1,000 requests/month). */
162
+ apiKey: string;
163
+ /** Override the API base URL. Defaults to https://api.sonovault.now. */
164
+ baseUrl?: string;
165
+ /** Retries on 429/5xx responses. Default 2; set 0 to disable. */
166
+ maxRetries?: number;
167
+ /** Custom fetch implementation (for testing or polyfills). */
168
+ fetch?: typeof globalThis.fetch;
169
+ }
170
+ declare class SonoVault {
171
+ private readonly apiKey;
172
+ private readonly baseUrl;
173
+ private readonly maxRetries;
174
+ private readonly fetchImpl;
175
+ constructor(options: SonoVaultOptions);
176
+ private request;
177
+ readonly tracks: {
178
+ /** Search by artist + title (both required — there is no free-text query). */
179
+ search: (params: {
180
+ artist: string;
181
+ title: string;
182
+ limit?: number;
183
+ cursor?: string;
184
+ }) => Promise<Page<Track>>;
185
+ /** Fetch a track by its SonoVault ID. */
186
+ get: (id: number) => Promise<Track>;
187
+ /** Look up a track by any of its ISRCs. */
188
+ byIsrc: (isrc: string) => Promise<Track>;
189
+ /** Recording → composition: the ISWC(s) behind a recording, by ISRC or track ID. */
190
+ iswc: (params: {
191
+ isrc?: string;
192
+ id?: number;
193
+ }) => Promise<IswcLookupResponse>;
194
+ /** Composition → recordings: every recording of a work, by ISWC. */
195
+ byIswc: (iswc: string, params?: {
196
+ limit?: number;
197
+ }) => Promise<{
198
+ iswc: string;
199
+ results: Track[];
200
+ }>;
201
+ /** Cross-platform IDs + deep links for a track, resolved from any platform's ID or an ISRC. */
202
+ links: (params: {
203
+ id?: number;
204
+ isrc?: string;
205
+ spotify_id?: string;
206
+ beatport_id?: string;
207
+ discogs_id?: string;
208
+ musicbrainz_id?: string;
209
+ applemusic_id?: string;
210
+ tidal_id?: string;
211
+ youtube_id?: string;
212
+ }) => Promise<PlatformLinksResponse>;
213
+ /** Resolve up to 100 track names, ISRCs, or platform IDs in one request. */
214
+ resolve: (body: ResolveRequest) => Promise<ResolveResponse>;
215
+ /** Identify a track from a Chromaprint fingerprint (`fpcalc -raw`). Paid tiers. */
216
+ identify: (body: IdentifyRequest) => Promise<IdentifyResponse>;
217
+ /**
218
+ * Identify a track from raw audio bytes (any ffmpeg-decodable format).
219
+ * Send the whole track when you can — the matching section is often mid-track.
220
+ * Paid tiers; costs 10 + ceil(MB) credits.
221
+ */
222
+ identifyAudio: (audio: ArrayBuffer | Uint8Array | Blob, params?: {
223
+ length?: number;
224
+ top_n?: number;
225
+ }) => Promise<IdentifyResponse>;
226
+ /** Browse the catalog by label, artist, genre, or year. Paid tiers. */
227
+ browse: (params: {
228
+ labelId?: number;
229
+ artistId?: number;
230
+ genre?: string;
231
+ genreId?: number;
232
+ year?: number;
233
+ randomize?: boolean;
234
+ limit?: number;
235
+ cursor?: string;
236
+ }) => Promise<Page<Track>>;
237
+ };
238
+ readonly artists: {
239
+ search: (params: {
240
+ name: string;
241
+ limit?: number;
242
+ cursor?: string;
243
+ }) => Promise<Page<Artist>>;
244
+ get: (id: number) => Promise<Artist>;
245
+ releases: (id: number, params?: {
246
+ limit?: number;
247
+ cursor?: string;
248
+ }) => Promise<Page<Release>>;
249
+ };
250
+ readonly labels: {
251
+ search: (params: {
252
+ name: string;
253
+ limit?: number;
254
+ cursor?: string;
255
+ }) => Promise<Page<Label>>;
256
+ get: (id: number) => Promise<Label>;
257
+ releases: (id: number, params?: {
258
+ limit?: number;
259
+ cursor?: string;
260
+ }) => Promise<Page<Release>>;
261
+ artists: (id: number, params?: {
262
+ limit?: number;
263
+ cursor?: string;
264
+ }) => Promise<Page<Artist>>;
265
+ };
266
+ readonly releases: {
267
+ search: (params: {
268
+ title: string;
269
+ artist?: string;
270
+ limit?: number;
271
+ cursor?: string;
272
+ }) => Promise<Page<Release>>;
273
+ get: (id: number) => Promise<Release>;
274
+ /** Newly released albums (GET /v1/releases/new). Paid tiers. */
275
+ latest: (params?: {
276
+ limit?: number;
277
+ cursor?: string;
278
+ }) => Promise<Page<Release>>;
279
+ };
280
+ readonly genres: {
281
+ /** The canonical genre/subgenre hierarchy. */
282
+ list: () => Promise<{
283
+ genres: Genre[];
284
+ }>;
285
+ };
286
+ readonly suggestions: {
287
+ /** Suggest a metadata correction for a track. Paid tiers. */
288
+ submit: (trackId: number, body: Record<string, unknown>) => Promise<Record<string, unknown>>;
289
+ list: (params?: {
290
+ limit?: number;
291
+ cursor?: string;
292
+ }) => Promise<Page<Record<string, unknown>>>;
293
+ };
294
+ readonly streams: {
295
+ /** Start monitoring an Icecast/Shoutcast stream. Paid tiers. */
296
+ create: (body: Record<string, unknown>) => Promise<Stream>;
297
+ list: () => Promise<{
298
+ streams: Stream[];
299
+ }>;
300
+ get: (id: string) => Promise<Stream>;
301
+ update: (id: string, body: Record<string, unknown>) => Promise<Stream>;
302
+ history: (id: string, params?: {
303
+ since?: string;
304
+ }) => Promise<Record<string, unknown>>;
305
+ report: (params: {
306
+ from: string;
307
+ until: string;
308
+ stream_id?: string;
309
+ }) => Promise<Record<string, unknown>>;
310
+ /** What's playing right now across your monitored streams. */
311
+ live: () => Promise<Record<string, unknown>>;
312
+ /** Stop monitoring a stream. */
313
+ stop: (id: string) => Promise<void>;
314
+ };
315
+ readonly webhooks: {
316
+ /** Register an endpoint for stream events. The response includes `secret` once — store it. */
317
+ create: (body: {
318
+ url: string;
319
+ event_types?: string[];
320
+ description?: string;
321
+ }) => Promise<Webhook>;
322
+ list: () => Promise<{
323
+ webhooks: Webhook[];
324
+ }>;
325
+ update: (id: string, body: Record<string, unknown>) => Promise<Webhook>;
326
+ delete: (id: string) => Promise<void>;
327
+ test: (id: string) => Promise<Record<string, unknown>>;
328
+ deliveries: (id: string) => Promise<Record<string, unknown>>;
329
+ };
330
+ }
331
+
332
+ /** Error thrown for any non-2xx API response. */
333
+ declare class SonoVaultError extends Error {
334
+ /** HTTP status code, or 0 for network errors. */
335
+ readonly status: number;
336
+ /** Parsed JSON error body, when the API returned one. */
337
+ readonly body: unknown;
338
+ constructor(message: string, status: number, body?: unknown);
339
+ /** Missing or invalid API key. */
340
+ get isAuthError(): boolean;
341
+ /** The endpoint needs a paid tier (or an admin key). */
342
+ get isForbidden(): boolean;
343
+ /** Rate limit or monthly credit quota hit. */
344
+ get isRateLimited(): boolean;
345
+ }
346
+
347
+ export { type Artist, type Genre, type IdentifyRequest, type IdentifyResponse, type IdentifyResult, type IswcLookupResponse, type Label, type Page, type PlatformLink, type PlatformLinksResponse, type Release, type ResolveInputType, type ResolveRequest, type ResolveResponse, type ResolveResult, SonoVault, SonoVaultError, type SonoVaultOptions, type Stream, type Track, type TrackArtist, type TrackRelease, type Webhook };
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ // src/error.ts
2
+ var SonoVaultError = class extends Error {
3
+ constructor(message, status, body = void 0) {
4
+ super(message);
5
+ this.name = "SonoVaultError";
6
+ this.status = status;
7
+ this.body = body;
8
+ }
9
+ /** Missing or invalid API key. */
10
+ get isAuthError() {
11
+ return this.status === 401;
12
+ }
13
+ /** The endpoint needs a paid tier (or an admin key). */
14
+ get isForbidden() {
15
+ return this.status === 403;
16
+ }
17
+ /** Rate limit or monthly credit quota hit. */
18
+ get isRateLimited() {
19
+ return this.status === 429;
20
+ }
21
+ };
22
+
23
+ // src/client.ts
24
+ var SonoVault = class {
25
+ constructor(options) {
26
+ this.tracks = {
27
+ /** Search by artist + title (both required — there is no free-text query). */
28
+ search: (params) => this.request("/v1/tracks/search", { query: params }),
29
+ /** Fetch a track by its SonoVault ID. */
30
+ get: (id) => this.request(`/v1/tracks/${id}`),
31
+ /** Look up a track by any of its ISRCs. */
32
+ byIsrc: (isrc) => this.request(`/v1/tracks/isrc/${encodeURIComponent(isrc)}`),
33
+ /** Recording → composition: the ISWC(s) behind a recording, by ISRC or track ID. */
34
+ iswc: (params) => this.request("/v1/tracks/iswc", { query: params }),
35
+ /** Composition → recordings: every recording of a work, by ISWC. */
36
+ byIswc: (iswc, params = {}) => this.request(
37
+ `/v1/tracks/iswc/${encodeURIComponent(iswc)}`,
38
+ { query: params }
39
+ ),
40
+ /** Cross-platform IDs + deep links for a track, resolved from any platform's ID or an ISRC. */
41
+ links: (params) => this.request("/v1/tracks/links", { query: params }),
42
+ /** Resolve up to 100 track names, ISRCs, or platform IDs in one request. */
43
+ resolve: (body) => this.request("/v1/tracks/resolve", { method: "POST", json: body }),
44
+ /** Identify a track from a Chromaprint fingerprint (`fpcalc -raw`). Paid tiers. */
45
+ identify: (body) => this.request("/v1/tracks/identify", { method: "POST", json: body }),
46
+ /**
47
+ * Identify a track from raw audio bytes (any ffmpeg-decodable format).
48
+ * Send the whole track when you can — the matching section is often mid-track.
49
+ * Paid tiers; costs 10 + ceil(MB) credits.
50
+ */
51
+ identifyAudio: (audio, params = {}) => this.request("/v1/tracks/identify", {
52
+ method: "POST",
53
+ query: params,
54
+ raw: { body: audio, contentType: "application/octet-stream" }
55
+ }),
56
+ /** Browse the catalog by label, artist, genre, or year. Paid tiers. */
57
+ browse: (params) => this.request("/v1/tracks/browse", { query: params })
58
+ };
59
+ this.artists = {
60
+ search: (params) => this.request("/v1/artists/search", { query: params }),
61
+ get: (id) => this.request(`/v1/artists/${id}`),
62
+ releases: (id, params = {}) => this.request(`/v1/artists/${id}/releases`, { query: params })
63
+ };
64
+ this.labels = {
65
+ search: (params) => this.request("/v1/labels/search", { query: params }),
66
+ get: (id) => this.request(`/v1/labels/${id}`),
67
+ releases: (id, params = {}) => this.request(`/v1/labels/${id}/releases`, { query: params }),
68
+ artists: (id, params = {}) => this.request(`/v1/labels/${id}/artists`, { query: params })
69
+ };
70
+ this.releases = {
71
+ search: (params) => this.request("/v1/releases/search", { query: params }),
72
+ get: (id) => this.request(`/v1/releases/${id}`),
73
+ /** Newly released albums (GET /v1/releases/new). Paid tiers. */
74
+ latest: (params = {}) => this.request("/v1/releases/new", { query: params })
75
+ };
76
+ this.genres = {
77
+ /** The canonical genre/subgenre hierarchy. */
78
+ list: () => this.request("/v1/genres")
79
+ };
80
+ this.suggestions = {
81
+ /** Suggest a metadata correction for a track. Paid tiers. */
82
+ submit: (trackId, body) => this.request(`/v1/tracks/${trackId}/suggestions`, {
83
+ method: "POST",
84
+ json: body
85
+ }),
86
+ list: (params = {}) => this.request("/v1/suggestions", { query: params })
87
+ };
88
+ this.streams = {
89
+ /** Start monitoring an Icecast/Shoutcast stream. Paid tiers. */
90
+ create: (body) => this.request("/v1/streams", { method: "POST", json: body }),
91
+ list: () => this.request("/v1/streams"),
92
+ get: (id) => this.request(`/v1/streams/${id}`),
93
+ update: (id, body) => this.request(`/v1/streams/${id}`, { method: "PATCH", json: body }),
94
+ history: (id, params = {}) => this.request(`/v1/streams/${id}/history`, { query: params }),
95
+ report: (params) => this.request("/v1/streams/report", { query: params }),
96
+ /** What's playing right now across your monitored streams. */
97
+ live: () => this.request("/v1/streams/live"),
98
+ /** Stop monitoring a stream. */
99
+ stop: (id) => this.request(`/v1/streams/${id}`, { method: "DELETE" })
100
+ };
101
+ this.webhooks = {
102
+ /** Register an endpoint for stream events. The response includes `secret` once — store it. */
103
+ create: (body) => this.request("/v1/webhooks", { method: "POST", json: body }),
104
+ list: () => this.request("/v1/webhooks"),
105
+ update: (id, body) => this.request(`/v1/webhooks/${id}`, { method: "PATCH", json: body }),
106
+ delete: (id) => this.request(`/v1/webhooks/${id}`, { method: "DELETE" }),
107
+ test: (id) => this.request(`/v1/webhooks/${id}/test`, { method: "POST" }),
108
+ deliveries: (id) => this.request(`/v1/webhooks/${id}/deliveries`)
109
+ };
110
+ if (!options?.apiKey) throw new Error("SonoVault: apiKey is required");
111
+ this.apiKey = options.apiKey;
112
+ this.baseUrl = (options.baseUrl ?? "https://api.sonovault.now").replace(/\/$/, "");
113
+ this.maxRetries = options.maxRetries ?? 2;
114
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
115
+ }
116
+ async request(path, opts = {}) {
117
+ const url = new URL(this.baseUrl + path);
118
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
119
+ if (value !== void 0) url.searchParams.set(key, String(value));
120
+ }
121
+ const headers = { "x-api-key": this.apiKey };
122
+ let body;
123
+ if (opts.json !== void 0) {
124
+ headers["Content-Type"] = "application/json";
125
+ body = JSON.stringify(opts.json);
126
+ } else if (opts.raw) {
127
+ headers["Content-Type"] = opts.raw.contentType;
128
+ body = opts.raw.body;
129
+ }
130
+ let lastError;
131
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
132
+ let res;
133
+ try {
134
+ res = await this.fetchImpl(url, { method: opts.method ?? "GET", headers, body });
135
+ } catch (err) {
136
+ lastError = new SonoVaultError(`Network error: ${err.message}`, 0);
137
+ continue;
138
+ }
139
+ if (res.ok) {
140
+ if (res.status === 204) return void 0;
141
+ return await res.json();
142
+ }
143
+ const errBody = await res.json().catch(() => void 0);
144
+ const message = errBody?.error ?? errBody?.message ?? `HTTP ${res.status}`;
145
+ lastError = new SonoVaultError(message, res.status, errBody);
146
+ const retryAfter = res.headers.get("retry-after");
147
+ const retryable = res.status >= 500 || res.status === 429 && retryAfter !== null;
148
+ if (!retryable || attempt === this.maxRetries) throw lastError;
149
+ const delayMs = retryAfter ? Number(retryAfter) * 1e3 : 500 * 2 ** attempt;
150
+ await new Promise((r) => setTimeout(r, delayMs));
151
+ }
152
+ throw lastError ?? new SonoVaultError("Request failed", 0);
153
+ }
154
+ };
155
+ export {
156
+ SonoVault,
157
+ SonoVaultError
158
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "sonovault",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript/Node client for the SonoVault music metadata API — ISRC, ISWC, genre, labels, release dates, and cross-platform IDs for 90M+ tracks.",
5
+ "keywords": [
6
+ "music",
7
+ "metadata",
8
+ "isrc",
9
+ "iswc",
10
+ "music-api",
11
+ "spotify",
12
+ "apple-music",
13
+ "tidal",
14
+ "beatport",
15
+ "discogs",
16
+ "musicbrainz",
17
+ "audio-recognition"
18
+ ],
19
+ "homepage": "https://sonovault.now",
20
+ "bugs": "https://github.com/rekordcloud/sonovault-js/issues",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/rekordcloud/sonovault-js.git"
24
+ },
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
45
+ "test": "vitest run",
46
+ "typecheck": "tsc --noEmit",
47
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^26.1.1",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.5.0",
53
+ "vitest": "^3.0.0"
54
+ }
55
+ }