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/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/index.d.ts +2573 -0
- package/dist/index.js +1739 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
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
|