discogs-typescript 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1739 @@
1
+ //#region src/auth/token.ts
2
+ /**
3
+ * Authenticates with a personal access token.
4
+ *
5
+ * Sends `Authorization: Discogs token=<token>`. This authenticates as the token holder and
6
+ * only as the token holder — use {@link OAuth1Auth} to act on behalf of other users.
7
+ *
8
+ * @see https://www.discogs.com/developers/#page:authentication,header:authentication-discogs-auth-flow
9
+ */
10
+ var TokenAuth = class {
11
+ #token;
12
+ constructor(token) {
13
+ if (!token) throw new TypeError("A personal access token is required.");
14
+ this.#token = token;
15
+ }
16
+ authorize(request) {
17
+ request.headers.set("Authorization", `Discogs token=${this.#token}`);
18
+ }
19
+ };
20
+ //#endregion
21
+ //#region src/auth/key-secret.ts
22
+ /**
23
+ * Authenticates with a consumer key and secret.
24
+ *
25
+ * Sends `Authorization: Discogs key=<key>, secret=<secret>`. This raises your rate limit to
26
+ * the authenticated tier and unlocks image URLs, but does not authenticate you as any
27
+ * particular user — endpoints that act on a user's data still require OAuth or a personal
28
+ * access token.
29
+ *
30
+ * @see https://www.discogs.com/developers/#page:authentication,header:authentication-discogs-auth-flow
31
+ */
32
+ var KeySecretAuth = class {
33
+ #key;
34
+ #secret;
35
+ constructor(consumerKey, consumerSecret) {
36
+ if (!consumerKey || !consumerSecret) throw new TypeError("Both a consumer key and a consumer secret are required.");
37
+ this.#key = consumerKey;
38
+ this.#secret = consumerSecret;
39
+ }
40
+ authorize(request) {
41
+ request.headers.set("Authorization", `Discogs key=${this.#key}, secret=${this.#secret}`);
42
+ }
43
+ };
44
+ //#endregion
45
+ //#region src/auth/oauth.ts
46
+ /**
47
+ * Percent-encodes a value per RFC 3986, which is stricter than `encodeURIComponent`:
48
+ * `!`, `'`, `(`, `)` and `*` must be escaped too.
49
+ *
50
+ * @internal
51
+ */
52
+ function percentEncode(value) {
53
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
54
+ }
55
+ /**
56
+ * Generates a random nonce.
57
+ *
58
+ * @internal
59
+ */
60
+ function generateNonce() {
61
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
62
+ crypto.getRandomValues(bytes);
63
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
64
+ }
65
+ /**
66
+ * Current Unix timestamp in seconds, as a string.
67
+ *
68
+ * @internal
69
+ */
70
+ function currentTimestamp() {
71
+ return Math.floor(Date.now() / 1e3).toString();
72
+ }
73
+ /**
74
+ * Builds the signature base string defined by RFC 5849 §3.4.1.
75
+ *
76
+ * Query-string parameters participate in the signature; JSON and multipart request bodies do
77
+ * not, which covers every Discogs endpoint this client talks to.
78
+ *
79
+ * @internal
80
+ */
81
+ function buildSignatureBaseString(method, url, oauthParams) {
82
+ const base = `${url.origin}${url.pathname}`;
83
+ const pairs = [];
84
+ for (const [key, value] of url.searchParams) pairs.push([key, value]);
85
+ for (const [key, value] of Object.entries(oauthParams)) pairs.push([key, value]);
86
+ const byteCompare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
87
+ const normalized = pairs.map(([key, value]) => [percentEncode(key), percentEncode(value)]).sort(([keyA, valueA], [keyB, valueB]) => keyA === keyB ? byteCompare(valueA, valueB) : byteCompare(keyA, keyB)).map(([key, value]) => `${key}=${value}`).join("&");
88
+ return [
89
+ method.toUpperCase(),
90
+ percentEncode(base),
91
+ percentEncode(normalized)
92
+ ].join("&");
93
+ }
94
+ /**
95
+ * The signing key: the percent-encoded consumer secret and token secret, joined by `&`.
96
+ *
97
+ * @internal
98
+ */
99
+ function buildSigningKey(consumerSecret, tokenSecret = "") {
100
+ return `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
101
+ }
102
+ /**
103
+ * Computes an HMAC-SHA1 signature and returns it base64-encoded.
104
+ *
105
+ * @internal
106
+ */
107
+ async function hmacSha1(key, message) {
108
+ const encoder = new TextEncoder();
109
+ const cryptoKey = await crypto.subtle.importKey("raw", encoder.encode(key), {
110
+ name: "HMAC",
111
+ hash: "SHA-1"
112
+ }, false, ["sign"]);
113
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message));
114
+ let binary = "";
115
+ for (const byte of new Uint8Array(signature)) binary += String.fromCharCode(byte);
116
+ return btoa(binary);
117
+ }
118
+ /**
119
+ * Computes the `oauth_signature` value for a request.
120
+ *
121
+ * @internal
122
+ */
123
+ async function signRequest(options) {
124
+ const key = buildSigningKey(options.consumerSecret, options.tokenSecret);
125
+ if (options.signatureMethod === "PLAINTEXT") return key;
126
+ return hmacSha1(key, buildSignatureBaseString(options.method, options.url, options.oauthParams));
127
+ }
128
+ /**
129
+ * Assembles an `Authorization: OAuth …` header value from a set of parameters.
130
+ *
131
+ * @internal
132
+ */
133
+ function buildAuthorizationHeader(params) {
134
+ return `OAuth ${Object.entries(params).map(([key, value]) => `${percentEncode(key)}="${percentEncode(value)}"`).join(", ")}`;
135
+ }
136
+ /**
137
+ * Signs requests with a full OAuth 1.0a access token, authenticating as the user who granted
138
+ * access.
139
+ *
140
+ * Obtain the access token and secret with {@link DiscogsOAuth}; they do not expire unless the
141
+ * user revokes them.
142
+ */
143
+ var OAuth1Auth = class {
144
+ #consumerKey;
145
+ #consumerSecret;
146
+ #accessToken;
147
+ #accessTokenSecret;
148
+ #signatureMethod;
149
+ #nonce;
150
+ #timestamp;
151
+ constructor(credentials, options = {}) {
152
+ const { consumerKey, consumerSecret, accessToken, accessTokenSecret } = credentials;
153
+ if (!consumerKey || !consumerSecret || !accessToken || !accessTokenSecret) throw new TypeError("OAuth authentication requires consumerKey, consumerSecret, accessToken and accessTokenSecret.");
154
+ this.#consumerKey = consumerKey;
155
+ this.#consumerSecret = consumerSecret;
156
+ this.#accessToken = accessToken;
157
+ this.#accessTokenSecret = accessTokenSecret;
158
+ this.#signatureMethod = credentials.signatureMethod ?? "PLAINTEXT";
159
+ this.#nonce = options.nonce ?? generateNonce;
160
+ this.#timestamp = options.timestamp ?? currentTimestamp;
161
+ }
162
+ async authorize(request) {
163
+ const params = {
164
+ oauth_consumer_key: this.#consumerKey,
165
+ oauth_token: this.#accessToken,
166
+ oauth_signature_method: this.#signatureMethod,
167
+ oauth_timestamp: this.#timestamp(),
168
+ oauth_nonce: this.#nonce(),
169
+ oauth_version: "1.0"
170
+ };
171
+ const signature = await signRequest({
172
+ method: request.method,
173
+ url: request.url,
174
+ oauthParams: params,
175
+ consumerSecret: this.#consumerSecret,
176
+ tokenSecret: this.#accessTokenSecret,
177
+ signatureMethod: this.#signatureMethod
178
+ });
179
+ request.headers.set("Authorization", buildAuthorizationHeader({
180
+ ...params,
181
+ oauth_signature: signature
182
+ }));
183
+ }
184
+ };
185
+ //#endregion
186
+ //#region src/errors.ts
187
+ /**
188
+ * Base class for every error the client throws for a failed API response.
189
+ *
190
+ * Use `instanceof DiscogsError` to catch all of them, or one of the subclasses below to
191
+ * handle a specific status.
192
+ */
193
+ var DiscogsError = class extends Error {
194
+ /** HTTP status code of the failing response. */
195
+ status;
196
+ /** The raw response object. */
197
+ response;
198
+ /** Parsed response body, when it could be read. */
199
+ body;
200
+ constructor(message, options) {
201
+ super(message);
202
+ this.name = new.target.name;
203
+ this.status = options.status;
204
+ this.response = options.response;
205
+ this.body = options.body;
206
+ }
207
+ };
208
+ /** 401 — the resource requires authentication, or the supplied credentials were rejected. */
209
+ var DiscogsAuthenticationError = class extends DiscogsError {};
210
+ /** 403 — authenticated, but not allowed to access or modify this resource. */
211
+ var DiscogsPermissionError = class extends DiscogsError {};
212
+ /** 404 — the resource does not exist. */
213
+ var DiscogsNotFoundError = class extends DiscogsError {};
214
+ /** 405 — the HTTP verb is not supported for this resource (e.g. `PUT /artists/1`). */
215
+ var DiscogsMethodNotAllowedError = class extends DiscogsError {};
216
+ /**
217
+ * 422 — the request was well-formed but semantically wrong: a missing or mistyped parameter,
218
+ * an invalid enum value, or a nonsensical action.
219
+ */
220
+ var DiscogsValidationError = class extends DiscogsError {};
221
+ /**
222
+ * 429 — the rate limit was exceeded.
223
+ *
224
+ * Discogs allows 60 requests per minute when authenticated and 25 when not, measured as a
225
+ * moving average over a 60-second window per source IP. Inspect
226
+ * {@link DiscogsRateLimitError.rateLimit} to see where you stand.
227
+ */
228
+ var DiscogsRateLimitError = class extends DiscogsError {
229
+ /** Rate-limit headers from the rejected response, when present. */
230
+ rateLimit;
231
+ constructor(message, options) {
232
+ super(message, options);
233
+ this.rateLimit = options.rateLimit ?? null;
234
+ }
235
+ };
236
+ /**
237
+ * 5xx — Discogs failed to handle the request.
238
+ *
239
+ * For a 500 the `message` in the body is an error code you can quote to Discogs Support.
240
+ */
241
+ var DiscogsServerError = class extends DiscogsError {};
242
+ /**
243
+ * Extracts the human-readable message from a Discogs error body.
244
+ *
245
+ * @internal
246
+ */
247
+ function extractMessage(body, response) {
248
+ if (typeof body === "object" && body !== null && "message" in body) {
249
+ const { message } = body;
250
+ if (typeof message === "string" && message.length > 0) return message;
251
+ }
252
+ if (typeof body === "string" && body.trim().length > 0) return body.trim();
253
+ return response.statusText || `Request failed with status ${String(response.status)}`;
254
+ }
255
+ /**
256
+ * Builds the appropriate {@link DiscogsError} subclass for a failed response.
257
+ *
258
+ * @internal
259
+ */
260
+ function createDiscogsError(response, body, rateLimit) {
261
+ const message = extractMessage(body, response);
262
+ const options = {
263
+ status: response.status,
264
+ response,
265
+ body
266
+ };
267
+ switch (response.status) {
268
+ case 401: return new DiscogsAuthenticationError(message, options);
269
+ case 403: return new DiscogsPermissionError(message, options);
270
+ case 404: return new DiscogsNotFoundError(message, options);
271
+ case 405: return new DiscogsMethodNotAllowedError(message, options);
272
+ case 422: return new DiscogsValidationError(message, options);
273
+ case 429: return new DiscogsRateLimitError(message, {
274
+ ...options,
275
+ rateLimit
276
+ });
277
+ default:
278
+ if (response.status >= 500) return new DiscogsServerError(message, options);
279
+ return new DiscogsError(message, options);
280
+ }
281
+ }
282
+ //#endregion
283
+ //#region src/rate-limit.ts
284
+ /** Header carrying the total request allowance for the current window. */
285
+ var RATE_LIMIT_HEADER = "X-Discogs-Ratelimit";
286
+ /** Header carrying the number of requests already used in the current window. */
287
+ var RATE_LIMIT_USED_HEADER = "X-Discogs-Ratelimit-Used";
288
+ /** Header carrying the number of requests still available in the current window. */
289
+ var RATE_LIMIT_REMAINING_HEADER = "X-Discogs-Ratelimit-Remaining";
290
+ function readInt(headers, name) {
291
+ const raw = headers.get(name);
292
+ if (raw === null) return null;
293
+ const value = Number.parseInt(raw, 10);
294
+ return Number.isNaN(value) ? null : value;
295
+ }
296
+ /**
297
+ * Reads the rate-limit headers off a response.
298
+ *
299
+ * @returns The parsed rate-limit state, or `null` when the headers are absent — which happens
300
+ * on endpoints Discogs does not throttle, and on responses served from a cache.
301
+ */
302
+ function parseRateLimit(headers) {
303
+ const limit = readInt(headers, RATE_LIMIT_HEADER);
304
+ const used = readInt(headers, RATE_LIMIT_USED_HEADER);
305
+ const remaining = readInt(headers, RATE_LIMIT_REMAINING_HEADER);
306
+ if (limit === null && used === null && remaining === null) return null;
307
+ return {
308
+ limit: limit ?? 0,
309
+ used: used ?? 0,
310
+ remaining: remaining ?? 0
311
+ };
312
+ }
313
+ /** Default base URL of the Discogs website, which hosts the authorize page. */
314
+ var DEFAULT_WEBSITE_URL = "https://www.discogs.com";
315
+ /**
316
+ * Drives the three-legged OAuth 1.0a flow that yields an access token for a Discogs user.
317
+ *
318
+ * Once you have the access token, hand it to {@link DiscogsClient} as the `auth` option.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * const oauth = new DiscogsOAuth({
323
+ * consumerKey: process.env.DISCOGS_CONSUMER_KEY!,
324
+ * consumerSecret: process.env.DISCOGS_CONSUMER_SECRET!,
325
+ * userAgent: 'MyApp/1.0 +https://example.com',
326
+ * });
327
+ *
328
+ * // 1. Get a temporary request token and send the user to Discogs.
329
+ * const request = await oauth.getRequestToken('https://example.com/callback');
330
+ * console.log(oauth.getAuthorizeUrl(request.oauthToken));
331
+ *
332
+ * // 2. Discogs redirects back with ?oauth_verifier=… — exchange it for an access token.
333
+ * const access = await oauth.getAccessToken({ ...request, verifier });
334
+ *
335
+ * // 3. Use it.
336
+ * const client = new DiscogsClient({
337
+ * userAgent: 'MyApp/1.0 +https://example.com',
338
+ * auth: {
339
+ * consumerKey, consumerSecret,
340
+ * accessToken: access.oauthToken,
341
+ * accessTokenSecret: access.oauthTokenSecret,
342
+ * },
343
+ * });
344
+ * ```
345
+ */
346
+ var DiscogsOAuth = class {
347
+ #consumerKey;
348
+ #consumerSecret;
349
+ #userAgent;
350
+ #signatureMethod;
351
+ #baseUrl;
352
+ #websiteUrl;
353
+ #fetch;
354
+ #nonce;
355
+ #timestamp;
356
+ constructor(config) {
357
+ if (!config.consumerKey || !config.consumerSecret) throw new TypeError("DiscogsOAuth requires a consumerKey and a consumerSecret.");
358
+ if (!config.userAgent) throw new TypeError("DiscogsOAuth requires a userAgent. Discogs returns an empty response without one.");
359
+ this.#consumerKey = config.consumerKey;
360
+ this.#consumerSecret = config.consumerSecret;
361
+ this.#userAgent = config.userAgent;
362
+ this.#signatureMethod = config.signatureMethod ?? "PLAINTEXT";
363
+ this.#baseUrl = (config.baseUrl ?? "https://api.discogs.com").replace(/\/+$/, "");
364
+ this.#websiteUrl = (config.websiteUrl ?? "https://www.discogs.com").replace(/\/+$/, "");
365
+ this.#fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
366
+ this.#nonce = config.nonce ?? generateNonce;
367
+ this.#timestamp = config.timestamp ?? currentTimestamp;
368
+ }
369
+ /**
370
+ * Step 1 — requests a temporary token from `GET /oauth/request_token`.
371
+ *
372
+ * @param callbackUrl - Where Discogs should send the user after they approve access. Pass
373
+ * `'oob'` (out of band) when you have no callback URL and want the user to type the
374
+ * verifier in manually.
375
+ */
376
+ async getRequestToken(callbackUrl) {
377
+ const url = new URL("/oauth/request_token", `${this.#baseUrl}/`);
378
+ const body = await this.#send("GET", url, { oauth_callback: callbackUrl });
379
+ const token = body.get("oauth_token");
380
+ const secret = body.get("oauth_token_secret");
381
+ if (token === null || secret === null) throw new Error(`Discogs did not return an oauth_token pair from ${url.pathname}: "${body.toString()}"`);
382
+ return {
383
+ oauthToken: token,
384
+ oauthTokenSecret: secret,
385
+ callbackConfirmed: body.get("oauth_callback_confirmed") === "true"
386
+ };
387
+ }
388
+ /**
389
+ * Step 2 — the URL to send the user to so they can approve your application.
390
+ *
391
+ * @param requestToken - The `oauthToken` from {@link DiscogsOAuth.getRequestToken}.
392
+ */
393
+ getAuthorizeUrl(requestToken) {
394
+ const url = new URL("/oauth/authorize", `${this.#websiteUrl}/`);
395
+ url.searchParams.set("oauth_token", requestToken);
396
+ return url.toString();
397
+ }
398
+ /**
399
+ * Step 3 — exchanges the approved request token for a long-lived access token via
400
+ * `POST /oauth/access_token`.
401
+ *
402
+ * Request tokens and verifiers expire 15 minutes after they are issued; an expired or
403
+ * malformed exchange fails with a 400.
404
+ */
405
+ async getAccessToken(params) {
406
+ const url = new URL("/oauth/access_token", `${this.#baseUrl}/`);
407
+ const body = await this.#send("POST", url, {
408
+ oauth_token: params.oauthToken,
409
+ oauth_verifier: params.verifier
410
+ }, params.oauthTokenSecret);
411
+ const token = body.get("oauth_token");
412
+ const secret = body.get("oauth_token_secret");
413
+ if (token === null || secret === null) throw new Error(`Discogs did not return an oauth_token pair from ${url.pathname}: "${body.toString()}"`);
414
+ return {
415
+ oauthToken: token,
416
+ oauthTokenSecret: secret
417
+ };
418
+ }
419
+ /**
420
+ * Signs and sends a token request, returning the form-encoded response body.
421
+ *
422
+ * Both token endpoints answer with `application/x-www-form-urlencoded`, not JSON.
423
+ */
424
+ async #send(method, url, extraParams, tokenSecret = "") {
425
+ const params = {
426
+ oauth_consumer_key: this.#consumerKey,
427
+ oauth_signature_method: this.#signatureMethod,
428
+ oauth_timestamp: this.#timestamp(),
429
+ oauth_nonce: this.#nonce(),
430
+ oauth_version: "1.0",
431
+ ...extraParams
432
+ };
433
+ const signature = await signRequest({
434
+ method,
435
+ url,
436
+ oauthParams: params,
437
+ consumerSecret: this.#consumerSecret,
438
+ tokenSecret,
439
+ signatureMethod: this.#signatureMethod
440
+ });
441
+ const response = await this.#fetch(url.toString(), {
442
+ method,
443
+ headers: {
444
+ Authorization: buildAuthorizationHeader({
445
+ ...params,
446
+ oauth_signature: signature
447
+ }),
448
+ "Content-Type": "application/x-www-form-urlencoded",
449
+ "User-Agent": this.#userAgent
450
+ }
451
+ });
452
+ const text = await response.text();
453
+ if (!response.ok) throw createDiscogsError(response, text, parseRateLimit(response.headers));
454
+ return new URLSearchParams(text);
455
+ }
456
+ };
457
+ //#endregion
458
+ //#region src/auth/index.ts
459
+ function isAuthStrategy(value) {
460
+ return typeof value.authorize === "function";
461
+ }
462
+ /**
463
+ * Turns the client's `auth` option into a concrete {@link AuthStrategy}.
464
+ *
465
+ * Accepts a personal token, a consumer key/secret pair, a full set of OAuth credentials, or a
466
+ * strategy object you built yourself.
467
+ *
468
+ * @internal
469
+ */
470
+ function resolveAuth(auth) {
471
+ if (isAuthStrategy(auth)) return auth;
472
+ if ("token" in auth) return new TokenAuth(auth.token);
473
+ if ("accessToken" in auth) return new OAuth1Auth(auth);
474
+ if ("consumerKey" in auth) return new KeySecretAuth(auth.consumerKey, auth.consumerSecret);
475
+ throw new TypeError("Unrecognised auth option. Supply { token }, { consumerKey, consumerSecret }, { consumerKey, consumerSecret, accessToken, accessTokenSecret }, or an AuthStrategy.");
476
+ }
477
+ //#endregion
478
+ //#region src/http.ts
479
+ /**
480
+ * Appends parameters to a URL's query string, skipping `null` and `undefined` and repeating
481
+ * the key for array values.
482
+ *
483
+ * @internal
484
+ */
485
+ function appendQuery(url, query) {
486
+ if (!query) return;
487
+ for (const [key, value] of Object.entries(query)) {
488
+ if (value === null || value === void 0) continue;
489
+ if (Array.isArray(value)) for (const item of value) url.searchParams.append(key, String(item));
490
+ else url.searchParams.append(key, String(value));
491
+ }
492
+ }
493
+ /**
494
+ * Percent-encodes a value for use as a single path segment.
495
+ *
496
+ * Usernames may contain characters such as `.` and `+` that must survive the round trip.
497
+ *
498
+ * @internal
499
+ */
500
+ function encodePathSegment(value) {
501
+ return encodeURIComponent(String(value));
502
+ }
503
+ /**
504
+ * Sends a request to the Discogs API and returns the parsed body plus its metadata.
505
+ *
506
+ * Non-2xx responses are thrown as a {@link DiscogsError}. `304 Not Modified` is treated as a
507
+ * success with a `null` body, so conditional requests against the inventory export and upload
508
+ * status endpoints work as intended.
509
+ *
510
+ * @internal
511
+ */
512
+ async function sendRequest(config, options) {
513
+ const method = options.method ?? "GET";
514
+ const url = new URL(options.path.replace(/^\//, ""), `${config.baseUrl}/`);
515
+ appendQuery(url, options.query);
516
+ const headers = new Headers(options.headers);
517
+ headers.set("User-Agent", config.userAgent);
518
+ if (!headers.has("Accept")) headers.set("Accept", `application/vnd.discogs.v2.${config.mediaType}+json`);
519
+ let body;
520
+ if (options.formData) body = options.formData;
521
+ else if (options.body !== void 0) {
522
+ body = JSON.stringify(options.body);
523
+ headers.set("Content-Type", "application/json");
524
+ }
525
+ await config.auth?.authorize({
526
+ method,
527
+ url,
528
+ headers
529
+ });
530
+ const init = {
531
+ method,
532
+ headers
533
+ };
534
+ if (body !== void 0) init.body = body;
535
+ if (options.signal) init.signal = options.signal;
536
+ const response = await config.fetch(url.toString(), init);
537
+ const rateLimit = parseRateLimit(response.headers);
538
+ config.onResponse?.({
539
+ response,
540
+ rateLimit
541
+ });
542
+ const responseType = options.responseType ?? "json";
543
+ if (response.status === 304) return {
544
+ data: null,
545
+ response,
546
+ rateLimit
547
+ };
548
+ if (!response.ok) throw createDiscogsError(response, await readErrorBody(response), rateLimit);
549
+ if (responseType === "none" || response.status === 204) return {
550
+ data: null,
551
+ response,
552
+ rateLimit
553
+ };
554
+ if (responseType === "text") return {
555
+ data: await response.text(),
556
+ response,
557
+ rateLimit
558
+ };
559
+ const text = await response.text();
560
+ if (text.length === 0) return {
561
+ data: null,
562
+ response,
563
+ rateLimit
564
+ };
565
+ return {
566
+ data: JSON.parse(text),
567
+ response,
568
+ rateLimit
569
+ };
570
+ }
571
+ /**
572
+ * Reads a failed response's body as JSON, falling back to text, and to `undefined` when the
573
+ * body cannot be read at all.
574
+ *
575
+ * @internal
576
+ */
577
+ async function readErrorBody(response) {
578
+ let text;
579
+ try {
580
+ text = await response.text();
581
+ } catch {
582
+ return;
583
+ }
584
+ if (text.length === 0) return void 0;
585
+ try {
586
+ return JSON.parse(text);
587
+ } catch {
588
+ return text;
589
+ }
590
+ }
591
+ //#endregion
592
+ //#region src/resources/collection.ts
593
+ /**
594
+ * User collection endpoints.
595
+ *
596
+ * A collection is arranged into folders. Folder `0` is the permanent "All" folder (releases
597
+ * cannot be added to it) and folder `1` is "Uncategorized". Since a user may own several
598
+ * copies of the same release, each copy in a folder is an *instance* with its own
599
+ * `instance_id`.
600
+ *
601
+ * Reachable as `client.collection`.
602
+ */
603
+ var CollectionResource = class {
604
+ #client;
605
+ constructor(client) {
606
+ this.#client = client;
607
+ }
608
+ /**
609
+ * Lists a user's collection folders.
610
+ *
611
+ * Without authentication as the owner, only folder `0` ("All") is visible, and only if the
612
+ * collection is public.
613
+ *
614
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection
615
+ */
616
+ getFolders(username) {
617
+ return this.#client.requestData({ path: `/users/${encodePathSegment(username)}/collection/folders` });
618
+ }
619
+ /**
620
+ * Creates a new folder. Requires authentication as the collection owner.
621
+ *
622
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-post
623
+ */
624
+ createFolder(username, name) {
625
+ return this.#client.requestData({
626
+ method: "POST",
627
+ path: `/users/${encodePathSegment(username)}/collection/folders`,
628
+ body: { name }
629
+ });
630
+ }
631
+ /**
632
+ * Gets a single folder. Requires authentication as the owner unless `folderId` is `0`.
633
+ *
634
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder
635
+ */
636
+ getFolder(username, folderId) {
637
+ return this.#client.requestData({ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}` });
638
+ }
639
+ /**
640
+ * Renames a folder. Requires authentication as the owner.
641
+ *
642
+ * Folders `0` ("All") and `1` ("Uncategorized") cannot be renamed.
643
+ *
644
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder-post
645
+ */
646
+ editFolder(username, folderId, name) {
647
+ return this.#client.requestData({
648
+ method: "POST",
649
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}`,
650
+ body: { name }
651
+ });
652
+ }
653
+ /**
654
+ * Deletes a folder. Requires authentication as the owner, and the folder must be empty.
655
+ *
656
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder-delete
657
+ */
658
+ deleteFolder(username, folderId) {
659
+ return this.#client.requestData({
660
+ method: "DELETE",
661
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}`,
662
+ responseType: "none"
663
+ });
664
+ }
665
+ /**
666
+ * Finds every instance of a given release across a user's collection folders.
667
+ *
668
+ * @param releaseId - Must be non-zero.
669
+ *
670
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-items-by-release
671
+ */
672
+ getItemsByRelease(username, releaseId, params = {}) {
673
+ return this.#client.requestData({
674
+ path: `/users/${encodePathSegment(username)}/collection/releases/${encodePathSegment(releaseId)}`,
675
+ query: params
676
+ });
677
+ }
678
+ /**
679
+ * Lists the releases in a collection folder.
680
+ *
681
+ * Requires authentication as the owner when `folderId` is not `0` or the collection is
682
+ * private. Without it, only public notes fields are returned.
683
+ *
684
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-items-by-folder
685
+ */
686
+ getItemsByFolder(username, folderId, params = {}) {
687
+ return this.#client.requestData({
688
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}/releases`,
689
+ query: params
690
+ });
691
+ }
692
+ /**
693
+ * Adds a release to a folder. Requires authentication as the owner.
694
+ *
695
+ * @param folderId - Must be non-zero; pass `1` for "Uncategorized".
696
+ *
697
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-add-to-collection-folder
698
+ */
699
+ addReleaseToFolder(username, folderId, releaseId) {
700
+ return this.#client.requestData({
701
+ method: "POST",
702
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}/releases/${encodePathSegment(releaseId)}`
703
+ });
704
+ }
705
+ /**
706
+ * Changes an instance's rating and/or moves it to a different folder. Requires
707
+ * authentication as the owner.
708
+ *
709
+ * Note the two folder ids: `folderId` identifies the folder the instance currently lives in,
710
+ * while `params.folder_id` is the folder to move it to.
711
+ *
712
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-change-rating-of-release
713
+ */
714
+ changeInstance(username, folderId, releaseId, instanceId, params) {
715
+ return this.#client.requestData({
716
+ method: "POST",
717
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}/releases/${encodePathSegment(releaseId)}/instances/${encodePathSegment(instanceId)}`,
718
+ body: params,
719
+ responseType: "none"
720
+ });
721
+ }
722
+ /**
723
+ * Removes an instance from a collection folder. Requires authentication as the owner.
724
+ *
725
+ * To move it to "Uncategorized" instead of deleting it, use
726
+ * {@link CollectionResource.changeInstance}.
727
+ *
728
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-delete-instance-from-folder
729
+ */
730
+ deleteInstance(username, folderId, releaseId, instanceId) {
731
+ return this.#client.requestData({
732
+ method: "DELETE",
733
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}/releases/${encodePathSegment(releaseId)}/instances/${encodePathSegment(instanceId)}`,
734
+ responseType: "none"
735
+ });
736
+ }
737
+ /**
738
+ * Lists a user's custom collection notes fields.
739
+ *
740
+ * These can only be created and deleted through the Discogs website. Without authentication
741
+ * as the owner, only fields with `public: true` are returned.
742
+ *
743
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-list-custom-fields
744
+ */
745
+ getFields(username) {
746
+ return this.#client.requestData({ path: `/users/${encodePathSegment(username)}/collection/fields` });
747
+ }
748
+ /**
749
+ * Sets the value of a custom notes field on a collection instance.
750
+ *
751
+ * @param value - For a `dropdown` field this must be one of the field's `options`. Sent as a
752
+ * query-string parameter, which is what this endpoint expects.
753
+ *
754
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-edit-fields-instance
755
+ */
756
+ editFieldInstance(username, folderId, releaseId, instanceId, fieldId, value) {
757
+ return this.#client.requestData({
758
+ method: "POST",
759
+ path: `/users/${encodePathSegment(username)}/collection/folders/${encodePathSegment(folderId)}/releases/${encodePathSegment(releaseId)}/instances/${encodePathSegment(instanceId)}/fields/${encodePathSegment(fieldId)}`,
760
+ query: { value },
761
+ responseType: "none"
762
+ });
763
+ }
764
+ /**
765
+ * Gets the minimum, median and maximum value of a collection, as currency-formatted strings.
766
+ * Requires authentication as the collection owner.
767
+ *
768
+ * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-value
769
+ */
770
+ getValue(username) {
771
+ return this.#client.requestData({ path: `/users/${encodePathSegment(username)}/collection/value` });
772
+ }
773
+ };
774
+ //#endregion
775
+ //#region src/resources/database.ts
776
+ /**
777
+ * Database endpoints.
778
+ *
779
+ * Reachable as `client.database`.
780
+ */
781
+ var DatabaseResource = class {
782
+ #client;
783
+ constructor(client) {
784
+ this.#client = client;
785
+ }
786
+ /**
787
+ * Gets a release.
788
+ *
789
+ * @param releaseId - The release id.
790
+ * @param params - Optional currency for the embedded marketplace data.
791
+ *
792
+ * @see https://www.discogs.com/developers/#page:database,header:database-release
793
+ */
794
+ getRelease(releaseId, params = {}) {
795
+ return this.#client.requestData({
796
+ path: `/releases/${encodePathSegment(releaseId)}`,
797
+ query: params
798
+ });
799
+ }
800
+ /**
801
+ * Gets a particular user's rating of a release.
802
+ *
803
+ * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user
804
+ */
805
+ getReleaseRating(releaseId, username) {
806
+ return this.#client.requestData({ path: `/releases/${encodePathSegment(releaseId)}/rating/${encodePathSegment(username)}` });
807
+ }
808
+ /**
809
+ * Sets a user's rating of a release. Requires authentication as that user.
810
+ *
811
+ * @param rating - The new rating, between 1 and 5.
812
+ *
813
+ * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user
814
+ */
815
+ updateReleaseRating(releaseId, username, rating) {
816
+ return this.#client.requestData({
817
+ method: "PUT",
818
+ path: `/releases/${encodePathSegment(releaseId)}/rating/${encodePathSegment(username)}`,
819
+ body: { rating }
820
+ });
821
+ }
822
+ /**
823
+ * Deletes a user's rating of a release. Requires authentication as that user.
824
+ *
825
+ * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user
826
+ */
827
+ deleteReleaseRating(releaseId, username) {
828
+ return this.#client.requestData({
829
+ method: "DELETE",
830
+ path: `/releases/${encodePathSegment(releaseId)}/rating/${encodePathSegment(username)}`,
831
+ responseType: "none"
832
+ });
833
+ }
834
+ /**
835
+ * Gets the community's average rating and rating count for a release.
836
+ *
837
+ * @see https://www.discogs.com/developers/#page:database,header:database-community-release-rating
838
+ */
839
+ getCommunityReleaseRating(releaseId) {
840
+ return this.#client.requestData({ path: `/releases/${encodePathSegment(releaseId)}/rating` });
841
+ }
842
+ /**
843
+ * Gets the "have" and "want" counts for a release.
844
+ *
845
+ * @see https://www.discogs.com/developers/#page:database,header:database-release-stats
846
+ */
847
+ getReleaseStats(releaseId) {
848
+ return this.#client.requestData({ path: `/releases/${encodePathSegment(releaseId)}/stats` });
849
+ }
850
+ /**
851
+ * Gets a master release.
852
+ *
853
+ * @see https://www.discogs.com/developers/#page:database,header:database-master-release
854
+ */
855
+ getMaster(masterId) {
856
+ return this.#client.requestData({ path: `/masters/${encodePathSegment(masterId)}` });
857
+ }
858
+ /**
859
+ * Lists all releases that are versions of a master release.
860
+ *
861
+ * @see https://www.discogs.com/developers/#page:database,header:database-master-release-versions
862
+ */
863
+ getMasterVersions(masterId, params = {}) {
864
+ return this.#client.requestData({
865
+ path: `/masters/${encodePathSegment(masterId)}/versions`,
866
+ query: params
867
+ });
868
+ }
869
+ /**
870
+ * Gets an artist.
871
+ *
872
+ * @see https://www.discogs.com/developers/#page:database,header:database-artist
873
+ */
874
+ getArtist(artistId) {
875
+ return this.#client.requestData({ path: `/artists/${encodePathSegment(artistId)}` });
876
+ }
877
+ /**
878
+ * Lists the releases and masters associated with an artist.
879
+ *
880
+ * Entries are discriminated by their `type` field: `"master"` or `"release"`.
881
+ *
882
+ * @see https://www.discogs.com/developers/#page:database,header:database-artist-releases
883
+ */
884
+ getArtistReleases(artistId, params = {}) {
885
+ return this.#client.requestData({
886
+ path: `/artists/${encodePathSegment(artistId)}/releases`,
887
+ query: params
888
+ });
889
+ }
890
+ /**
891
+ * Gets a label.
892
+ *
893
+ * @see https://www.discogs.com/developers/#page:database,header:database-label
894
+ */
895
+ getLabel(labelId) {
896
+ return this.#client.requestData({ path: `/labels/${encodePathSegment(labelId)}` });
897
+ }
898
+ /**
899
+ * Lists the releases associated with a label.
900
+ *
901
+ * @see https://www.discogs.com/developers/#page:database,header:database-all-label-releases
902
+ */
903
+ getLabelReleases(labelId, params = {}) {
904
+ return this.#client.requestData({
905
+ path: `/labels/${encodePathSegment(labelId)}/releases`,
906
+ query: params
907
+ });
908
+ }
909
+ /**
910
+ * Searches the Discogs database.
911
+ *
912
+ * **Authentication (as any user) is required.** Unauthenticated searches fail with a 401.
913
+ *
914
+ * @example
915
+ * ```ts
916
+ * await client.database.search({ artist: 'nirvana', release_title: 'nevermind', per_page: 3 });
917
+ * ```
918
+ *
919
+ * @see https://www.discogs.com/developers/#page:database,header:database-search
920
+ */
921
+ search(params = {}) {
922
+ return this.#client.requestData({
923
+ path: "/database/search",
924
+ query: params
925
+ });
926
+ }
927
+ };
928
+ //#endregion
929
+ //#region src/resources/inventory-export.ts
930
+ /**
931
+ * Inventory export endpoints.
932
+ *
933
+ * Reachable as `client.inventoryExport`.
934
+ */
935
+ var InventoryExportResource = class {
936
+ #client;
937
+ constructor(client) {
938
+ this.#client = client;
939
+ }
940
+ /**
941
+ * Requests a CSV export of your inventory.
942
+ *
943
+ * Exports are generated asynchronously — poll {@link InventoryExportResource.get} until the
944
+ * status reports success, then call {@link InventoryExportResource.downloadCsv}.
945
+ *
946
+ * @throws A `DiscogsError` with status 409 when an export is already in progress.
947
+ *
948
+ * @see https://www.discogs.com/developers/#page:inventory-export,header:inventory-export-export-your-inventory
949
+ */
950
+ async create() {
951
+ const { response } = await this.#client.request({
952
+ method: "POST",
953
+ path: "/inventory/export",
954
+ responseType: "none"
955
+ });
956
+ const location = response.headers.get("Location");
957
+ const match = location === null ? null : /\/inventory\/export\/(\d+)/.exec(location);
958
+ return {
959
+ id: match?.[1] === void 0 ? null : Number.parseInt(match[1], 10),
960
+ location
961
+ };
962
+ }
963
+ /**
964
+ * Lists your recent inventory exports, newest first.
965
+ *
966
+ * @remarks Discogs names the collection key `items` on this endpoint, not `exports`.
967
+ *
968
+ * @see https://www.discogs.com/developers/#page:inventory-export,header:inventory-export-get-recent-exports
969
+ */
970
+ list(params = {}) {
971
+ return this.#client.requestData({
972
+ path: "/inventory/export",
973
+ query: params
974
+ });
975
+ }
976
+ /**
977
+ * Gets the status of an export.
978
+ *
979
+ * @returns The export, or `null` when `ifModifiedSince` was supplied and Discogs answered
980
+ * `304 Not Modified`.
981
+ *
982
+ * @see https://www.discogs.com/developers/#page:inventory-export,header:inventory-export-get-an-export
983
+ */
984
+ get(exportId, options = {}) {
985
+ return this.#client.requestData({
986
+ path: `/inventory/export/${encodePathSegment(exportId)}`,
987
+ headers: buildConditionalHeaders(options)
988
+ });
989
+ }
990
+ /**
991
+ * Downloads a finished export as CSV text.
992
+ *
993
+ * @see https://www.discogs.com/developers/#page:inventory-export,header:inventory-export-download-an-export
994
+ */
995
+ downloadCsv(exportId) {
996
+ return this.#client.requestData({
997
+ path: `/inventory/export/${encodePathSegment(exportId)}/download`,
998
+ headers: { Accept: "text/csv" },
999
+ responseType: "text"
1000
+ });
1001
+ }
1002
+ /**
1003
+ * Downloads a finished export as a raw {@link Response}, so you can stream it to disk or
1004
+ * read the `Content-Disposition` filename.
1005
+ *
1006
+ * @see https://www.discogs.com/developers/#page:inventory-export,header:inventory-export-download-an-export
1007
+ */
1008
+ async downloadRaw(exportId) {
1009
+ const { response } = await this.#client.request({
1010
+ path: `/inventory/export/${encodePathSegment(exportId)}/download`,
1011
+ headers: { Accept: "text/csv" },
1012
+ responseType: "none"
1013
+ });
1014
+ return response;
1015
+ }
1016
+ };
1017
+ /**
1018
+ * Builds the `If-Modified-Since` header for a conditional request.
1019
+ *
1020
+ * @internal
1021
+ */
1022
+ function buildConditionalHeaders(options) {
1023
+ if (options.ifModifiedSince === void 0) return {};
1024
+ return { "If-Modified-Since": options.ifModifiedSince instanceof Date ? options.ifModifiedSince.toUTCString() : options.ifModifiedSince };
1025
+ }
1026
+ //#endregion
1027
+ //#region src/resources/inventory-upload.ts
1028
+ /**
1029
+ * Wraps a CSV payload in the `multipart/form-data` body Discogs expects, under the field name
1030
+ * `upload`.
1031
+ *
1032
+ * @internal
1033
+ */
1034
+ function buildUploadFormData(csv, filename = "inventory.csv") {
1035
+ const form = new FormData();
1036
+ const blob = typeof csv === "string" ? new Blob([csv], { type: "text/csv" }) : csv;
1037
+ form.append("upload", blob, filename);
1038
+ return form;
1039
+ }
1040
+ /**
1041
+ * Inventory upload endpoints.
1042
+ *
1043
+ * Every upload takes a comma-separated CSV whose first row is a header of **lower case**
1044
+ * field names. Uploads are processed asynchronously — poll
1045
+ * {@link InventoryUploadResource.get} for the outcome.
1046
+ *
1047
+ * Reachable as `client.inventoryUpload`.
1048
+ */
1049
+ var InventoryUploadResource = class {
1050
+ #client;
1051
+ constructor(client) {
1052
+ this.#client = client;
1053
+ }
1054
+ /**
1055
+ * Uploads a CSV of listings to add to your inventory. Added listings go on sale immediately,
1056
+ * priced in the currency from your Marketplace settings.
1057
+ *
1058
+ * Required columns: `release_id`, `price`, `media_condition`.
1059
+ * Optional columns: `sleeve_condition`, `comments`, `accept_offer` (`Y` or `N`), `location`,
1060
+ * `external_id`, `weight` (grams, non-negative integer), `format_quantity`.
1061
+ * Any other column is ignored.
1062
+ *
1063
+ * @param csv - CSV text, or a `Blob`/`File` if you want to control the filename.
1064
+ *
1065
+ * @example
1066
+ * ```ts
1067
+ * await client.inventoryUpload.add(
1068
+ * 'release_id,price,media_condition\n249504,12.50,Near Mint (NM or M-)\n',
1069
+ * );
1070
+ * ```
1071
+ *
1072
+ * @see https://www.discogs.com/developers/#page:inventory-upload,header:inventory-upload-add-inventory
1073
+ */
1074
+ add(csv, filename) {
1075
+ return this.#upload("add", csv, filename);
1076
+ }
1077
+ /**
1078
+ * Uploads a CSV of changes to existing listings.
1079
+ *
1080
+ * Required column: `release_id`.
1081
+ * At least one of: `price`, `media_condition`, `sleeve_condition`, `comments`,
1082
+ * `accept_offer` (`Y` or `N`), `external_id`, `location`, `weight`, `format_quantity`.
1083
+ *
1084
+ * @see https://www.discogs.com/developers/#page:inventory-upload,header:inventory-upload-change-inventory
1085
+ */
1086
+ change(csv, filename) {
1087
+ return this.#upload("change", csv, filename);
1088
+ }
1089
+ /**
1090
+ * Uploads a CSV of listings to delete. The only column is `listing_id`.
1091
+ *
1092
+ * @example
1093
+ * ```ts
1094
+ * await client.inventoryUpload.delete('listing_id\n12345678\n98765432\n');
1095
+ * ```
1096
+ *
1097
+ * @see https://www.discogs.com/developers/#page:inventory-upload,header:inventory-upload-delete-inventory
1098
+ */
1099
+ delete(csv, filename) {
1100
+ return this.#upload("delete", csv, filename);
1101
+ }
1102
+ /**
1103
+ * Lists your recent inventory uploads.
1104
+ *
1105
+ * @remarks Discogs names the collection key `items` on this endpoint, not `uploads`.
1106
+ *
1107
+ * @see https://www.discogs.com/developers/#page:inventory-upload,header:inventory-upload-get-recent-uploads
1108
+ */
1109
+ list(params = {}) {
1110
+ return this.#client.requestData({
1111
+ path: "/inventory/upload",
1112
+ query: params
1113
+ });
1114
+ }
1115
+ /**
1116
+ * Gets the status of an upload, including how many records were processed.
1117
+ *
1118
+ * @returns The upload, or `null` when `ifModifiedSince` was supplied and Discogs answered
1119
+ * `304 Not Modified`.
1120
+ *
1121
+ * @see https://www.discogs.com/developers/#page:inventory-upload,header:inventory-upload-get-an-upload
1122
+ */
1123
+ get(uploadId, options = {}) {
1124
+ return this.#client.requestData({
1125
+ path: `/inventory/upload/${encodePathSegment(uploadId)}`,
1126
+ headers: buildConditionalHeaders(options)
1127
+ });
1128
+ }
1129
+ async #upload(kind, csv, filename) {
1130
+ const { response } = await this.#client.request({
1131
+ method: "POST",
1132
+ path: `/inventory/upload/${kind}`,
1133
+ formData: buildUploadFormData(csv, filename),
1134
+ responseType: "none"
1135
+ });
1136
+ const location = response.headers.get("Location");
1137
+ const match = location === null ? null : /\/inventory\/upload\/(\d+)/.exec(location);
1138
+ return {
1139
+ id: match?.[1] === void 0 ? null : Number.parseInt(match[1], 10),
1140
+ location
1141
+ };
1142
+ }
1143
+ };
1144
+ //#endregion
1145
+ //#region src/resources/lists.ts
1146
+ /**
1147
+ * User list endpoints.
1148
+ *
1149
+ * Reachable as `client.lists`.
1150
+ */
1151
+ var ListsResource = class {
1152
+ #client;
1153
+ constructor(client) {
1154
+ this.#client = client;
1155
+ }
1156
+ /**
1157
+ * Lists a user's lists. Private lists are only returned when authenticated as the owner.
1158
+ *
1159
+ * @see https://www.discogs.com/developers/#page:user-lists,header:user-lists-user-lists
1160
+ */
1161
+ getUserLists(username, params = {}) {
1162
+ return this.#client.requestData({
1163
+ path: `/users/${encodePathSegment(username)}/lists`,
1164
+ query: params
1165
+ });
1166
+ }
1167
+ /**
1168
+ * Gets a list and its items. Private lists are only returned when authenticated as the
1169
+ * owner.
1170
+ *
1171
+ * @remarks This endpoint names its fields differently from the index endpoint —
1172
+ * `created_ts` / `modified_ts` / `list_id` / `url` rather than
1173
+ * `date_added` / `date_changed` / `id` / `uri`.
1174
+ *
1175
+ * @see https://www.discogs.com/developers/#page:user-lists,header:user-lists-list
1176
+ */
1177
+ getList(listId) {
1178
+ return this.#client.requestData({ path: `/lists/${encodePathSegment(listId)}` });
1179
+ }
1180
+ };
1181
+ //#endregion
1182
+ //#region src/resources/marketplace.ts
1183
+ /**
1184
+ * Marketplace endpoints.
1185
+ *
1186
+ * Reachable as `client.marketplace`.
1187
+ */
1188
+ var MarketplaceResource = class {
1189
+ #client;
1190
+ constructor(client) {
1191
+ this.#client = client;
1192
+ }
1193
+ /**
1194
+ * Lists the listings in a user's inventory.
1195
+ *
1196
+ * Unless authenticated as the inventory's owner, only `For Sale` items are returned and the
1197
+ * seller-private fields (`weight`, `format_quantity`, `external_id`, `location`,
1198
+ * `quantity`) are omitted.
1199
+ *
1200
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-inventory
1201
+ */
1202
+ getInventory(username, params = {}) {
1203
+ return this.#client.requestData({
1204
+ path: `/users/${encodePathSegment(username)}/inventory`,
1205
+ query: params
1206
+ });
1207
+ }
1208
+ /**
1209
+ * Gets a listing.
1210
+ *
1211
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing
1212
+ */
1213
+ getListing(listingId, params = {}) {
1214
+ return this.#client.requestData({
1215
+ path: `/marketplace/listings/${encodePathSegment(listingId)}`,
1216
+ query: params
1217
+ });
1218
+ }
1219
+ /**
1220
+ * Creates a listing in the authenticated user's inventory.
1221
+ *
1222
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-new-listing
1223
+ */
1224
+ createListing(params) {
1225
+ return this.#client.requestData({
1226
+ method: "POST",
1227
+ path: "/marketplace/listings",
1228
+ body: params
1229
+ });
1230
+ }
1231
+ /**
1232
+ * Edits a listing. Requires authentication as the listing's owner.
1233
+ *
1234
+ * Listings whose status is not `For Sale`, `Draft` or `Expired` cannot be edited, only
1235
+ * deleted; a `Sold` listing has to be replaced with a new one.
1236
+ *
1237
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-post
1238
+ */
1239
+ editListing(listingId, params) {
1240
+ return this.#client.requestData({
1241
+ method: "POST",
1242
+ path: `/marketplace/listings/${encodePathSegment(listingId)}`,
1243
+ body: params,
1244
+ responseType: "none"
1245
+ });
1246
+ }
1247
+ /**
1248
+ * Permanently removes a listing. Requires authentication as the listing's owner.
1249
+ *
1250
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-delete
1251
+ */
1252
+ deleteListing(listingId) {
1253
+ return this.#client.requestData({
1254
+ method: "DELETE",
1255
+ path: `/marketplace/listings/${encodePathSegment(listingId)}`,
1256
+ responseType: "none"
1257
+ });
1258
+ }
1259
+ /**
1260
+ * Gets an order. Requires authentication as the seller.
1261
+ *
1262
+ * @param orderId - Order ids are strings of the form `"1-1"`.
1263
+ *
1264
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-order
1265
+ */
1266
+ getOrder(orderId) {
1267
+ return this.#client.requestData({ path: `/marketplace/orders/${encodePathSegment(orderId)}` });
1268
+ }
1269
+ /**
1270
+ * Edits an order. Requires authentication as the seller.
1271
+ *
1272
+ * The new `status` must appear in the order's current `next_status` array. Setting
1273
+ * `shipping` invoices the buyer and forces the status to `Invoice Sent`, so `shipping` and
1274
+ * `status` cannot be sent together. Changing the status through this endpoint always
1275
+ * messages the buyer with a fixed "Seller changed status from … to …" note — use
1276
+ * {@link MarketplaceResource.addOrderMessage} to combine a status change with your own text.
1277
+ *
1278
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-order-post
1279
+ */
1280
+ editOrder(orderId, params) {
1281
+ return this.#client.requestData({
1282
+ method: "POST",
1283
+ path: `/marketplace/orders/${encodePathSegment(orderId)}`,
1284
+ body: params
1285
+ });
1286
+ }
1287
+ /**
1288
+ * Lists the authenticated user's orders.
1289
+ *
1290
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-orders
1291
+ */
1292
+ listOrders(params = {}) {
1293
+ return this.#client.requestData({
1294
+ path: "/marketplace/orders",
1295
+ query: params
1296
+ });
1297
+ }
1298
+ /**
1299
+ * Lists an order's messages, most recent first. Requires authentication as the seller.
1300
+ *
1301
+ * Entries are discriminated by their `type` field.
1302
+ *
1303
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-orders-get
1304
+ */
1305
+ getOrderMessages(orderId, params = {}) {
1306
+ return this.#client.requestData({
1307
+ path: `/marketplace/orders/${encodePathSegment(orderId)}/messages`,
1308
+ query: params
1309
+ });
1310
+ }
1311
+ /**
1312
+ * Adds a message to an order's message log, optionally changing the order status at the
1313
+ * same time. At least one of `message` or `status` must be supplied.
1314
+ *
1315
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-orders-post
1316
+ */
1317
+ async addOrderMessage(orderId, params) {
1318
+ if (params.message === void 0 && params.status === void 0) throw new TypeError("addOrderMessage requires at least one of \"message\" or \"status\".");
1319
+ return this.#client.requestData({
1320
+ method: "POST",
1321
+ path: `/marketplace/orders/${encodePathSegment(orderId)}/messages`,
1322
+ body: params
1323
+ });
1324
+ }
1325
+ /**
1326
+ * Calculates the Discogs commission on a sale price, in the given currency (USD by default).
1327
+ *
1328
+ * @remarks The price is formatted to exactly two decimal places, because the endpoint
1329
+ * requires it: `/marketplace/fee/20` returns a 404 while `/marketplace/fee/20.00` succeeds.
1330
+ * The Discogs docs only ever show `10.00` and never state this, so passing a bare integer
1331
+ * is an easy mistake to make.
1332
+ *
1333
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-fee
1334
+ */
1335
+ getFee(price, currency) {
1336
+ const amount = price.toFixed(2);
1337
+ const path = currency === void 0 ? `/marketplace/fee/${amount}` : `/marketplace/fee/${amount}/${encodePathSegment(currency)}`;
1338
+ return this.#client.requestData({ path });
1339
+ }
1340
+ /**
1341
+ * Gets suggested prices per media condition for a release, in the user's selling currency.
1342
+ *
1343
+ * Requires authentication, and the user must have completed their seller settings. Returns
1344
+ * an empty object when Discogs has no suggestions for the release.
1345
+ *
1346
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-price-suggestions
1347
+ */
1348
+ getPriceSuggestions(releaseId) {
1349
+ return this.#client.requestData({ path: `/marketplace/price_suggestions/${encodePathSegment(releaseId)}` });
1350
+ }
1351
+ /**
1352
+ * Gets marketplace statistics for a release: how many copies are for sale and the lowest
1353
+ * listed price.
1354
+ *
1355
+ * `lowest_price` and `num_for_sale` are `null` when nothing is for sale or the release is
1356
+ * blocked from sale.
1357
+ *
1358
+ * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-release-statistics
1359
+ */
1360
+ getReleaseStats(releaseId, params = {}) {
1361
+ return this.#client.requestData({
1362
+ path: `/marketplace/stats/${encodePathSegment(releaseId)}`,
1363
+ query: params
1364
+ });
1365
+ }
1366
+ };
1367
+ //#endregion
1368
+ //#region src/resources/user.ts
1369
+ /**
1370
+ * User identity endpoints.
1371
+ *
1372
+ * Reachable as `client.user`.
1373
+ */
1374
+ var UserResource = class {
1375
+ #client;
1376
+ constructor(client) {
1377
+ this.#client = client;
1378
+ }
1379
+ /**
1380
+ * Gets basic information about the authenticated user — useful as a credentials check at
1381
+ * the end of the OAuth flow.
1382
+ *
1383
+ * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-identity
1384
+ */
1385
+ getIdentity() {
1386
+ return this.#client.requestData({ path: "/oauth/identity" });
1387
+ }
1388
+ /**
1389
+ * Gets a user's profile.
1390
+ *
1391
+ * `email` is only returned when authenticated as this user; `num_collection` and
1392
+ * `num_wantlist` only when authenticated as this user or when the list in question is
1393
+ * public.
1394
+ *
1395
+ * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-profile
1396
+ */
1397
+ getProfile(username) {
1398
+ return this.#client.requestData({ path: `/users/${encodePathSegment(username)}` });
1399
+ }
1400
+ /**
1401
+ * Edits a user's profile. Requires authentication as that user.
1402
+ *
1403
+ * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-profile-post
1404
+ */
1405
+ editProfile(username, params) {
1406
+ return this.#client.requestData({
1407
+ method: "POST",
1408
+ path: `/users/${encodePathSegment(username)}`,
1409
+ body: params
1410
+ });
1411
+ }
1412
+ /**
1413
+ * Lists the database entries a user has submitted, grouped into artists, labels and
1414
+ * releases.
1415
+ *
1416
+ * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-user-submissions
1417
+ */
1418
+ getSubmissions(username, params = {}) {
1419
+ return this.#client.requestData({
1420
+ path: `/users/${encodePathSegment(username)}/submissions`,
1421
+ query: params
1422
+ });
1423
+ }
1424
+ /**
1425
+ * Lists a user's contributions — the releases they have edited or added to.
1426
+ *
1427
+ * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-user-contributions
1428
+ */
1429
+ getContributions(username, params = {}) {
1430
+ return this.#client.requestData({
1431
+ path: `/users/${encodePathSegment(username)}/contributions`,
1432
+ query: params
1433
+ });
1434
+ }
1435
+ };
1436
+ //#endregion
1437
+ //#region src/resources/wantlist.ts
1438
+ /**
1439
+ * User wantlist endpoints.
1440
+ *
1441
+ * Reachable as `client.wantlist`.
1442
+ */
1443
+ var WantlistResource = class {
1444
+ #client;
1445
+ constructor(client) {
1446
+ this.#client = client;
1447
+ }
1448
+ /**
1449
+ * Lists the releases on a user's wantlist.
1450
+ *
1451
+ * A private wantlist requires authentication as its owner, and the `notes` field is only
1452
+ * returned to the owner.
1453
+ *
1454
+ * @see https://www.discogs.com/developers/#page:user-wantlist,header:user-wantlist-wantlist
1455
+ */
1456
+ getWants(username, params = {}) {
1457
+ return this.#client.requestData({
1458
+ path: `/users/${encodePathSegment(username)}/wants`,
1459
+ query: params
1460
+ });
1461
+ }
1462
+ /**
1463
+ * Adds a release to a user's wantlist. Requires authentication as the wantlist owner.
1464
+ *
1465
+ * @see https://www.discogs.com/developers/#page:user-wantlist,header:user-wantlist-add-to-wantlist
1466
+ */
1467
+ addToWantlist(username, releaseId, params = {}) {
1468
+ return this.#client.requestData({
1469
+ method: "PUT",
1470
+ path: `/users/${encodePathSegment(username)}/wants/${encodePathSegment(releaseId)}`,
1471
+ query: params
1472
+ });
1473
+ }
1474
+ /**
1475
+ * Edits the notes or rating on a wantlist entry. Requires authentication as the owner.
1476
+ *
1477
+ * @see https://www.discogs.com/developers/#page:user-wantlist,header:user-wantlist-add-to-wantlist-post
1478
+ */
1479
+ editWantlistItem(username, releaseId, params = {}) {
1480
+ return this.#client.requestData({
1481
+ method: "POST",
1482
+ path: `/users/${encodePathSegment(username)}/wants/${encodePathSegment(releaseId)}`,
1483
+ query: params
1484
+ });
1485
+ }
1486
+ /**
1487
+ * Removes a release from a user's wantlist. Requires authentication as the owner.
1488
+ *
1489
+ * @see https://www.discogs.com/developers/#page:user-wantlist,header:user-wantlist-add-to-wantlist-delete
1490
+ */
1491
+ removeFromWantlist(username, releaseId) {
1492
+ return this.#client.requestData({
1493
+ method: "DELETE",
1494
+ path: `/users/${encodePathSegment(username)}/wants/${encodePathSegment(releaseId)}`,
1495
+ responseType: "none"
1496
+ });
1497
+ }
1498
+ };
1499
+ //#endregion
1500
+ //#region src/client.ts
1501
+ /**
1502
+ * The Discogs API client.
1503
+ *
1504
+ * @module
1505
+ */
1506
+ /** Default base URL of the Discogs API. */
1507
+ var DEFAULT_BASE_URL = "https://api.discogs.com";
1508
+ /**
1509
+ * A client for the Discogs API v2.
1510
+ *
1511
+ * Endpoints are grouped into resources that mirror the sections of the Discogs documentation.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * const client = new DiscogsClient({
1516
+ * userAgent: 'MyApp/1.0 +https://example.com',
1517
+ * auth: { token: process.env.DISCOGS_TOKEN! },
1518
+ * });
1519
+ *
1520
+ * const release = await client.database.getRelease(249504);
1521
+ * const results = await client.database.search({ artist: 'nirvana', type: 'release' });
1522
+ * ```
1523
+ *
1524
+ * @see https://www.discogs.com/developers/
1525
+ */
1526
+ var DiscogsClient = class {
1527
+ /** Database: releases, masters, artists, labels and search. */
1528
+ database;
1529
+ /** Marketplace: inventory, listings, orders, fees, price suggestions and stats. */
1530
+ marketplace;
1531
+ /** Inventory export: request and download CSV exports of your inventory. */
1532
+ inventoryExport;
1533
+ /** Inventory upload: bulk add, change and delete listings from a CSV. */
1534
+ inventoryUpload;
1535
+ /** User identity: the authenticated user, profiles, submissions and contributions. */
1536
+ user;
1537
+ /** User collection: folders, items, custom fields and collection value. */
1538
+ collection;
1539
+ /** User wantlist. */
1540
+ wantlist;
1541
+ /** User lists. */
1542
+ lists;
1543
+ #config;
1544
+ #rateLimit = null;
1545
+ constructor(config) {
1546
+ if (!config.userAgent) throw new TypeError("DiscogsClient requires a userAgent identifying your application. Discogs returns an empty response to requests without one.");
1547
+ const auth = config.auth ? resolveAuth(config.auth) : null;
1548
+ this.#config = {
1549
+ baseUrl: (config.baseUrl ?? "https://api.discogs.com").replace(/\/+$/, ""),
1550
+ userAgent: config.userAgent,
1551
+ mediaType: config.mediaType ?? "discogs",
1552
+ auth,
1553
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1554
+ onResponse: (info) => {
1555
+ this.#rateLimit = info.rateLimit ?? this.#rateLimit;
1556
+ config.onResponse?.(info);
1557
+ }
1558
+ };
1559
+ this.database = new DatabaseResource(this);
1560
+ this.marketplace = new MarketplaceResource(this);
1561
+ this.inventoryExport = new InventoryExportResource(this);
1562
+ this.inventoryUpload = new InventoryUploadResource(this);
1563
+ this.user = new UserResource(this);
1564
+ this.collection = new CollectionResource(this);
1565
+ this.wantlist = new WantlistResource(this);
1566
+ this.lists = new ListsResource(this);
1567
+ }
1568
+ /**
1569
+ * Rate-limit state from the most recent response, or `null` if no response has carried the
1570
+ * headers yet.
1571
+ *
1572
+ * Because this reflects only the latest response it is unreliable while requests overlap —
1573
+ * use the `onResponse` config option when you need per-request accuracy.
1574
+ */
1575
+ get rateLimit() {
1576
+ return this.#rateLimit;
1577
+ }
1578
+ /**
1579
+ * Sends an arbitrary request to the API, returning the parsed body together with the raw
1580
+ * response and its rate-limit headers.
1581
+ *
1582
+ * Use this to reach anything the typed resources do not cover, or when you need response
1583
+ * headers such as `Location` or `Last-Modified`.
1584
+ *
1585
+ * @example
1586
+ * ```ts
1587
+ * const { data, rateLimit } = await client.request<Release>({ path: '/releases/249504' });
1588
+ * ```
1589
+ */
1590
+ request(options) {
1591
+ return sendRequest(this.#config, options);
1592
+ }
1593
+ /**
1594
+ * Sends a request and returns just the parsed body — what every resource method uses.
1595
+ *
1596
+ * @internal
1597
+ */
1598
+ async requestData(options) {
1599
+ const { data } = await sendRequest(this.#config, options);
1600
+ return data;
1601
+ }
1602
+ };
1603
+ //#endregion
1604
+ //#region src/pagination.ts
1605
+ /** Default number of items Discogs returns per page. */
1606
+ var DEFAULT_PER_PAGE = 50;
1607
+ /** Maximum number of items Discogs will return per page. */
1608
+ var MAX_PER_PAGE = 100;
1609
+ /**
1610
+ * Parses an RFC 5988 `Link` header into its `rel` relations.
1611
+ *
1612
+ * The same information is available in the body's `pagination.urls`, so this is mainly useful
1613
+ * when you are working with a raw {@link Response} from {@link DiscogsClient.request}.
1614
+ *
1615
+ * @param header - Raw `Link` header value, or `null` when absent.
1616
+ * @returns A map of relation name to URL. Empty when the header is absent or unparseable.
1617
+ *
1618
+ * @example
1619
+ * ```ts
1620
+ * parseLinkHeader('<https://api.discogs.com/artists/1/releases?page=2>; rel=next')
1621
+ * // → { next: 'https://api.discogs.com/artists/1/releases?page=2' }
1622
+ * ```
1623
+ */
1624
+ function parseLinkHeader(header) {
1625
+ const urls = {};
1626
+ if (!header) return urls;
1627
+ for (const part of header.split(",")) {
1628
+ const match = /<([^>]*)>\s*;\s*rel\s*=\s*"?([^";]+)"?/.exec(part.trim());
1629
+ if (!match) continue;
1630
+ const [, url, rel] = match;
1631
+ if (url === void 0 || rel === void 0) continue;
1632
+ switch (rel.trim()) {
1633
+ case "first":
1634
+ urls.first = url;
1635
+ break;
1636
+ case "prev":
1637
+ urls.prev = url;
1638
+ break;
1639
+ case "next":
1640
+ urls.next = url;
1641
+ break;
1642
+ case "last": urls.last = url;
1643
+ }
1644
+ }
1645
+ return urls;
1646
+ }
1647
+ //#endregion
1648
+ //#region src/types/common.ts
1649
+ /** Every {@link Currency} value, in the order the Discogs docs list them. */
1650
+ var CURRENCIES = [
1651
+ "USD",
1652
+ "GBP",
1653
+ "EUR",
1654
+ "CAD",
1655
+ "AUD",
1656
+ "JPY",
1657
+ "CHF",
1658
+ "MXN",
1659
+ "BRL",
1660
+ "NZD",
1661
+ "SEK",
1662
+ "ZAR"
1663
+ ];
1664
+ //#endregion
1665
+ //#region src/types/marketplace.ts
1666
+ /** Every {@link MediaCondition}, best to worst. */
1667
+ var MEDIA_CONDITIONS = [
1668
+ "Mint (M)",
1669
+ "Near Mint (NM or M-)",
1670
+ "Very Good Plus (VG+)",
1671
+ "Very Good (VG)",
1672
+ "Good Plus (G+)",
1673
+ "Good (G)",
1674
+ "Fair (F)",
1675
+ "Poor (P)"
1676
+ ];
1677
+ /** Every {@link SleeveCondition}. */
1678
+ var SLEEVE_CONDITIONS = [
1679
+ ...MEDIA_CONDITIONS,
1680
+ "Generic",
1681
+ "Not Graded",
1682
+ "No Cover"
1683
+ ];
1684
+ /** Every {@link ListingStatusFilter}, as enumerated by the API's own 422 error message. */
1685
+ var LISTING_STATUS_FILTERS = [
1686
+ "All",
1687
+ "Deleted",
1688
+ "Draft",
1689
+ "Expired",
1690
+ "For Sale",
1691
+ "Sold",
1692
+ "Suspended",
1693
+ "Violation"
1694
+ ];
1695
+ /** Every {@link OrderStatus} a seller may set. */
1696
+ var ORDER_STATUSES = [
1697
+ "New Order",
1698
+ "Buyer Contacted",
1699
+ "Invoice Sent",
1700
+ "Payment Pending",
1701
+ "Payment Received",
1702
+ "In Progress",
1703
+ "Shipped",
1704
+ "Refund Sent",
1705
+ "Cancelled (Non-Paying Buyer)",
1706
+ "Cancelled (Item Unavailable)",
1707
+ "Cancelled (Per Buyer's Request)"
1708
+ ];
1709
+ /** Every {@link OrderStatusFilter}. */
1710
+ var ORDER_STATUS_FILTERS = [
1711
+ "All",
1712
+ ...ORDER_STATUSES,
1713
+ "Merged",
1714
+ "Order Changed",
1715
+ "Cancelled",
1716
+ "Cancelled (Refund Received)"
1717
+ ];
1718
+ /** Every {@link TrackingCarrier}. */
1719
+ var TRACKING_CARRIERS = [
1720
+ "UPS",
1721
+ "USPS",
1722
+ "DHL",
1723
+ "Deutsche Post",
1724
+ "La Poste",
1725
+ "Royal Mail",
1726
+ "PostNL",
1727
+ "DHL Germany",
1728
+ "Other"
1729
+ ];
1730
+ //#endregion
1731
+ //#region src/types/collection.ts
1732
+ /** The permanent "All" folder, which lists every release in the collection. */
1733
+ var FOLDER_ALL = 0;
1734
+ /** The permanent "Uncategorized" folder, the default destination for new additions. */
1735
+ var FOLDER_UNCATEGORIZED = 1;
1736
+ //#endregion
1737
+ export { CURRENCIES, CollectionResource, DEFAULT_BASE_URL, DEFAULT_PER_PAGE, DEFAULT_WEBSITE_URL, DatabaseResource, DiscogsAuthenticationError, DiscogsClient, DiscogsError, DiscogsMethodNotAllowedError, DiscogsNotFoundError, DiscogsOAuth, DiscogsPermissionError, DiscogsRateLimitError, DiscogsServerError, DiscogsValidationError, FOLDER_ALL, FOLDER_UNCATEGORIZED, InventoryExportResource, InventoryUploadResource, KeySecretAuth, LISTING_STATUS_FILTERS, ListsResource, MAX_PER_PAGE, MEDIA_CONDITIONS, MarketplaceResource, OAuth1Auth, ORDER_STATUSES, ORDER_STATUS_FILTERS, RATE_LIMIT_HEADER, RATE_LIMIT_REMAINING_HEADER, RATE_LIMIT_USED_HEADER, SLEEVE_CONDITIONS, TRACKING_CARRIERS, TokenAuth, UserResource, WantlistResource, parseLinkHeader, parseRateLimit };
1738
+
1739
+ //# sourceMappingURL=index.js.map