soundcloud-api-ts 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Twin Paws
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,9 +2,33 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/soundcloud-api-ts)](https://www.npmjs.com/package/soundcloud-api-ts)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/soundcloud-api-ts)](https://www.npmjs.com/package/soundcloud-api-ts)
5
- [![license](https://img.shields.io/npm/l/soundcloud-api-ts)](https://github.com/twin-paws/soundcloud-api-client/blob/main/LICENSE)
6
-
7
- A TypeScript client for the SoundCloud API. Zero dependencies, uses native `fetch` (Node 18+).
5
+ [![CI](https://github.com/twin-paws/soundcloud-api-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/twin-paws/soundcloud-api-ts/actions/workflows/ci.yml)
6
+ [![license](https://img.shields.io/npm/l/soundcloud-api-ts)](https://github.com/twin-paws/soundcloud-api-ts/blob/main/LICENSE)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/soundcloud-api-ts)](https://bundlephobia.com/package/soundcloud-api-ts)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
9
+ [![coverage](https://img.shields.io/badge/coverage-67%25-yellow.svg)]()
10
+
11
+ A TypeScript client for the SoundCloud API. Zero dependencies, uses native `fetch`.
12
+
13
+ ## Why soundcloud-api-ts?
14
+
15
+ - **Zero dependencies** — uses native `fetch`, nothing to install
16
+ - **Full TypeScript types** for all API responses
17
+ - **Token management built-in** — `setToken()`, auto-refresh on 401
18
+ - **PKCE support** for public clients and SPAs
19
+ - **Clean API** — `sc.tracks.getTrack(id)` not `getTrack(token, id)`
20
+ - **Dual ESM/CJS output** — works everywhere
21
+
22
+ ## Comparison
23
+
24
+ | Feature | soundcloud-api-ts | soundcloud.ts | node-soundcloud |
25
+ | --- | --- | --- | --- |
26
+ | TypeScript | ✅ Native | ✅ | ❌ |
27
+ | Dependencies | 0 | varies | varies |
28
+ | OAuth 2.0 | ✅ Full | Partial | Partial |
29
+ | Auto token refresh | ✅ | ❌ | ❌ |
30
+ | PKCE | ✅ | ❌ | ❌ |
31
+ | Maintained | ✅ 2026 | — | — |
8
32
 
9
33
  ## Install
10
34
 
@@ -234,6 +258,32 @@ interface SoundCloudPaginatedResponse<T> {
234
258
  }
235
259
  ```
236
260
 
261
+ ### Automatic Pagination
262
+
263
+ The client provides three helpers that automatically follow `next_href` across pages:
264
+
265
+ ```ts
266
+ // Stream pages — yields T[] for each page
267
+ for await (const page of sc.paginate(() => sc.users.getTracks(userId))) {
268
+ console.log(`Got ${page.length} tracks`);
269
+ }
270
+
271
+ // Stream individual items — yields one T at a time
272
+ for await (const track of sc.paginateItems(() => sc.search.tracks("lofi"))) {
273
+ console.log(track.title);
274
+ }
275
+
276
+ // Collect all into a flat array (with optional limit)
277
+ const allTracks = await sc.fetchAll(() => sc.users.getTracks(userId));
278
+ const first100 = await sc.fetchAll(() => sc.search.tracks("lofi"), { maxItems: 100 });
279
+ ```
280
+
281
+ The standalone functions are also exported for advanced use:
282
+
283
+ ```ts
284
+ import { paginate, paginateItems, fetchAll, scFetchUrl } from "soundcloud-api-ts";
285
+ ```
286
+
237
287
  ## Utilities
238
288
 
239
289
  ```ts
@@ -243,11 +293,66 @@ import { getSoundCloudWidgetUrl } from "soundcloud-api-ts";
243
293
  const widgetUrl = getSoundCloudWidgetUrl(trackId);
244
294
  ```
245
295
 
296
+ ## Error Handling
297
+
298
+ All API errors throw a `SoundCloudError` with structured properties:
299
+
300
+ ```ts
301
+ import { SoundCloudError } from "soundcloud-api-ts";
302
+
303
+ try {
304
+ await sc.tracks.getTrack(999);
305
+ } catch (err) {
306
+ if (err instanceof SoundCloudError) {
307
+ console.log(err.status); // 404
308
+ console.log(err.statusText); // "Not Found"
309
+ console.log(err.message); // "404 - Not Found" (from SC's response)
310
+ console.log(err.errorCode); // "invalid_client" (on auth errors)
311
+ console.log(err.errors); // ["404 - Not Found"] (individual error messages)
312
+ console.log(err.docsLink); // "https://developers.soundcloud.com/docs/api/explorer/open-api"
313
+ console.log(err.body); // full parsed response body
314
+
315
+ // Convenience getters
316
+ if (err.isNotFound) { /* handle 404 */ }
317
+ if (err.isRateLimited) { /* handle 429 */ }
318
+ if (err.isUnauthorized) { /* handle 401 */ }
319
+ if (err.isForbidden) { /* handle 403 */ }
320
+ if (err.isServerError) { /* handle 5xx */ }
321
+ }
322
+ }
323
+ ```
324
+
325
+ Error messages are parsed directly from SoundCloud's API response format, giving you the most useful message available.
326
+
327
+ ## Rate Limiting & Retries
328
+
329
+ The client automatically retries on **429 Too Many Requests** and **5xx Server Errors** with exponential backoff:
330
+
331
+ ```ts
332
+ const sc = new SoundCloudClient({
333
+ clientId: "...",
334
+ clientSecret: "...",
335
+ maxRetries: 3, // default: 3
336
+ retryBaseDelay: 1000, // default: 1000ms
337
+ onDebug: (msg) => console.log(msg), // optional retry logging
338
+ });
339
+ ```
340
+
341
+ - **429 responses** respect the `Retry-After` header when present
342
+ - **5xx responses** (500, 502, 503, 504) are retried with exponential backoff
343
+ - **4xx errors** (except 429) are NOT retried — they throw immediately
344
+ - **401 errors** trigger `onTokenRefresh` (if configured) instead of retry
345
+ - Backoff formula: `baseDelay × 2^attempt` with jitter
346
+
246
347
  ## Requirements
247
348
 
248
- - Node.js 18+ (uses native `fetch`)
349
+ - Node.js 20+ (uses native `fetch`)
249
350
  - SoundCloud API credentials
250
351
 
352
+ ## Contributing
353
+
354
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
355
+
251
356
  ## License
252
357
 
253
- MIT
358
+ [MIT](LICENSE) © Twin Paws
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var SoundCloudError = class extends Error {
5
+ /** HTTP status code */
6
+ status;
7
+ /** HTTP status text (e.g. "Unauthorized") */
8
+ statusText;
9
+ /** SC error code (e.g. "invalid_client") */
10
+ errorCode;
11
+ /** SC docs link */
12
+ docsLink;
13
+ /** Individual error messages from the errors array */
14
+ errors;
15
+ /** The full parsed response body */
16
+ body;
17
+ constructor(status, statusText, body) {
18
+ const msg = body?.message || body?.error_description || body?.error_code || body?.errors?.[0]?.error_message || body?.error || `${status} ${statusText}`;
19
+ super(msg);
20
+ this.name = "SoundCloudError";
21
+ this.status = status;
22
+ this.statusText = statusText;
23
+ this.errorCode = body?.error_code ?? void 0;
24
+ this.docsLink = body?.link ?? void 0;
25
+ this.errors = body?.errors?.map((e) => e.error_message).filter((m) => !!m) ?? [];
26
+ this.body = body;
27
+ }
28
+ /** True if status is 401 Unauthorized */
29
+ get isUnauthorized() {
30
+ return this.status === 401;
31
+ }
32
+ /** True if status is 403 Forbidden */
33
+ get isForbidden() {
34
+ return this.status === 403;
35
+ }
36
+ /** True if status is 404 Not Found */
37
+ get isNotFound() {
38
+ return this.status === 404;
39
+ }
40
+ /** True if status is 429 Too Many Requests */
41
+ get isRateLimited() {
42
+ return this.status === 429;
43
+ }
44
+ /** True if status is 5xx server error */
45
+ get isServerError() {
46
+ return this.status >= 500 && this.status < 600;
47
+ }
48
+ };
49
+
50
+ exports.SoundCloudError = SoundCloudError;
51
+ //# sourceMappingURL=chunk-34DWTDWF.js.map
52
+ //# sourceMappingURL=chunk-34DWTDWF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";;;AAgCO,IAAM,eAAA,GAAN,cAA8B,KAAA,CAAM;AAAA;AAAA,EAEhC,MAAA;AAAA;AAAA,EAEA,UAAA;AAAA;AAAA,EAEA,SAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA,EAEA,MAAA;AAAA;AAAA,EAEA,IAAA;AAAA,EAET,WAAA,CAAY,MAAA,EAAgB,UAAA,EAAoB,IAAA,EAA4B;AAE1E,IAAA,MAAM,MACJ,IAAA,EAAM,OAAA,IACN,IAAA,EAAM,iBAAA,IACN,MAAM,UAAA,IACN,IAAA,EAAM,MAAA,GAAS,CAAC,GAAG,aAAA,IACnB,IAAA,EAAM,SACN,CAAA,EAAG,MAAM,IAAI,UAAU,CAAA,CAAA;AAEzB,IAAA,KAAA,CAAM,GAAG,CAAA;AACT,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,MAAM,UAAA,IAAc,MAAA;AACrC,IAAA,IAAA,CAAK,QAAA,GAAW,MAAM,IAAA,IAAQ,MAAA;AAC9B,IAAA,IAAA,CAAK,SACH,IAAA,EAAM,MAAA,EACF,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,aAAa,CAAA,CAC3B,MAAA,CAAO,CAAC,CAAA,KAAmB,CAAC,CAAC,CAAC,KAAK,EAAC;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAA,GAA0B;AAC5B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAuB;AACzB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsB;AACxB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAA,IAAU,GAAA,IAAO,IAAA,CAAK,MAAA,GAAS,GAAA;AAAA,EAC7C;AACF","file":"chunk-34DWTDWF.js","sourcesContent":["/**\n * SoundCloud API error response shape.\n * Based on actual SC API responses, e.g.:\n * {\n * \"code\": 401,\n * \"message\": \"invalid_client\",\n * \"link\": \"https://developers.soundcloud.com/docs/api/explorer/open-api\",\n * \"status\": \"401 - Unauthorized\",\n * \"errors\": [{\"error_message\": \"invalid_client\"}],\n * \"error\": null,\n * \"error_code\": \"invalid_client\"\n * }\n */\nexport interface SoundCloudErrorBody {\n /** HTTP status code */\n code?: number;\n /** Error message from SC (e.g. \"invalid_client\", \"404 - Not Found\") */\n message?: string;\n /** SC status string (e.g. \"401 - Unauthorized\") */\n status?: string;\n /** Link to SC API docs */\n link?: string;\n /** Array of individual errors */\n errors?: Array<{ error_message?: string }>;\n /** Error field — typically null in SC responses */\n error?: string | null;\n /** Error code (e.g. \"invalid_client\") */\n error_code?: string;\n /** OAuth error_description (used in /oauth2/token errors) */\n error_description?: string;\n}\n\nexport class SoundCloudError extends Error {\n /** HTTP status code */\n readonly status: number;\n /** HTTP status text (e.g. \"Unauthorized\") */\n readonly statusText: string;\n /** SC error code (e.g. \"invalid_client\") */\n readonly errorCode?: string;\n /** SC docs link */\n readonly docsLink?: string;\n /** Individual error messages from the errors array */\n readonly errors: string[];\n /** The full parsed response body */\n readonly body?: SoundCloudErrorBody;\n\n constructor(status: number, statusText: string, body?: SoundCloudErrorBody) {\n // Build the most useful message we can from SC's response\n const msg =\n body?.message ||\n body?.error_description ||\n body?.error_code ||\n body?.errors?.[0]?.error_message ||\n body?.error ||\n `${status} ${statusText}`;\n\n super(msg);\n this.name = \"SoundCloudError\";\n this.status = status;\n this.statusText = statusText;\n this.errorCode = body?.error_code ?? undefined;\n this.docsLink = body?.link ?? undefined;\n this.errors =\n body?.errors\n ?.map((e) => e.error_message)\n .filter((m): m is string => !!m) ?? [];\n this.body = body;\n }\n\n /** True if status is 401 Unauthorized */\n get isUnauthorized(): boolean {\n return this.status === 401;\n }\n\n /** True if status is 403 Forbidden */\n get isForbidden(): boolean {\n return this.status === 403;\n }\n\n /** True if status is 404 Not Found */\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** True if status is 429 Too Many Requests */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** True if status is 5xx server error */\n get isServerError(): boolean {\n return this.status >= 500 && this.status < 600;\n }\n}\n"]}
@@ -0,0 +1,50 @@
1
+ // src/errors.ts
2
+ var SoundCloudError = class extends Error {
3
+ /** HTTP status code */
4
+ status;
5
+ /** HTTP status text (e.g. "Unauthorized") */
6
+ statusText;
7
+ /** SC error code (e.g. "invalid_client") */
8
+ errorCode;
9
+ /** SC docs link */
10
+ docsLink;
11
+ /** Individual error messages from the errors array */
12
+ errors;
13
+ /** The full parsed response body */
14
+ body;
15
+ constructor(status, statusText, body) {
16
+ const msg = body?.message || body?.error_description || body?.error_code || body?.errors?.[0]?.error_message || body?.error || `${status} ${statusText}`;
17
+ super(msg);
18
+ this.name = "SoundCloudError";
19
+ this.status = status;
20
+ this.statusText = statusText;
21
+ this.errorCode = body?.error_code ?? void 0;
22
+ this.docsLink = body?.link ?? void 0;
23
+ this.errors = body?.errors?.map((e) => e.error_message).filter((m) => !!m) ?? [];
24
+ this.body = body;
25
+ }
26
+ /** True if status is 401 Unauthorized */
27
+ get isUnauthorized() {
28
+ return this.status === 401;
29
+ }
30
+ /** True if status is 403 Forbidden */
31
+ get isForbidden() {
32
+ return this.status === 403;
33
+ }
34
+ /** True if status is 404 Not Found */
35
+ get isNotFound() {
36
+ return this.status === 404;
37
+ }
38
+ /** True if status is 429 Too Many Requests */
39
+ get isRateLimited() {
40
+ return this.status === 429;
41
+ }
42
+ /** True if status is 5xx server error */
43
+ get isServerError() {
44
+ return this.status >= 500 && this.status < 600;
45
+ }
46
+ };
47
+
48
+ export { SoundCloudError };
49
+ //# sourceMappingURL=chunk-GKNBLKPB.mjs.map
50
+ //# sourceMappingURL=chunk-GKNBLKPB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";AAgCO,IAAM,eAAA,GAAN,cAA8B,KAAA,CAAM;AAAA;AAAA,EAEhC,MAAA;AAAA;AAAA,EAEA,UAAA;AAAA;AAAA,EAEA,SAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA,EAEA,MAAA;AAAA;AAAA,EAEA,IAAA;AAAA,EAET,WAAA,CAAY,MAAA,EAAgB,UAAA,EAAoB,IAAA,EAA4B;AAE1E,IAAA,MAAM,MACJ,IAAA,EAAM,OAAA,IACN,IAAA,EAAM,iBAAA,IACN,MAAM,UAAA,IACN,IAAA,EAAM,MAAA,GAAS,CAAC,GAAG,aAAA,IACnB,IAAA,EAAM,SACN,CAAA,EAAG,MAAM,IAAI,UAAU,CAAA,CAAA;AAEzB,IAAA,KAAA,CAAM,GAAG,CAAA;AACT,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,MAAM,UAAA,IAAc,MAAA;AACrC,IAAA,IAAA,CAAK,QAAA,GAAW,MAAM,IAAA,IAAQ,MAAA;AAC9B,IAAA,IAAA,CAAK,SACH,IAAA,EAAM,MAAA,EACF,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,aAAa,CAAA,CAC3B,MAAA,CAAO,CAAC,CAAA,KAAmB,CAAC,CAAC,CAAC,KAAK,EAAC;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAA,GAA0B;AAC5B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAuB;AACzB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsB;AACxB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAA,IAAU,GAAA,IAAO,IAAA,CAAK,MAAA,GAAS,GAAA;AAAA,EAC7C;AACF","file":"chunk-GKNBLKPB.mjs","sourcesContent":["/**\n * SoundCloud API error response shape.\n * Based on actual SC API responses, e.g.:\n * {\n * \"code\": 401,\n * \"message\": \"invalid_client\",\n * \"link\": \"https://developers.soundcloud.com/docs/api/explorer/open-api\",\n * \"status\": \"401 - Unauthorized\",\n * \"errors\": [{\"error_message\": \"invalid_client\"}],\n * \"error\": null,\n * \"error_code\": \"invalid_client\"\n * }\n */\nexport interface SoundCloudErrorBody {\n /** HTTP status code */\n code?: number;\n /** Error message from SC (e.g. \"invalid_client\", \"404 - Not Found\") */\n message?: string;\n /** SC status string (e.g. \"401 - Unauthorized\") */\n status?: string;\n /** Link to SC API docs */\n link?: string;\n /** Array of individual errors */\n errors?: Array<{ error_message?: string }>;\n /** Error field — typically null in SC responses */\n error?: string | null;\n /** Error code (e.g. \"invalid_client\") */\n error_code?: string;\n /** OAuth error_description (used in /oauth2/token errors) */\n error_description?: string;\n}\n\nexport class SoundCloudError extends Error {\n /** HTTP status code */\n readonly status: number;\n /** HTTP status text (e.g. \"Unauthorized\") */\n readonly statusText: string;\n /** SC error code (e.g. \"invalid_client\") */\n readonly errorCode?: string;\n /** SC docs link */\n readonly docsLink?: string;\n /** Individual error messages from the errors array */\n readonly errors: string[];\n /** The full parsed response body */\n readonly body?: SoundCloudErrorBody;\n\n constructor(status: number, statusText: string, body?: SoundCloudErrorBody) {\n // Build the most useful message we can from SC's response\n const msg =\n body?.message ||\n body?.error_description ||\n body?.error_code ||\n body?.errors?.[0]?.error_message ||\n body?.error ||\n `${status} ${statusText}`;\n\n super(msg);\n this.name = \"SoundCloudError\";\n this.status = status;\n this.statusText = statusText;\n this.errorCode = body?.error_code ?? undefined;\n this.docsLink = body?.link ?? undefined;\n this.errors =\n body?.errors\n ?.map((e) => e.error_message)\n .filter((m): m is string => !!m) ?? [];\n this.body = body;\n }\n\n /** True if status is 401 Unauthorized */\n get isUnauthorized(): boolean {\n return this.status === 401;\n }\n\n /** True if status is 403 Forbidden */\n get isForbidden(): boolean {\n return this.status === 403;\n }\n\n /** True if status is 404 Not Found */\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** True if status is 429 Too Many Requests */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** True if status is 5xx server error */\n get isServerError(): boolean {\n return this.status >= 500 && this.status < 600;\n }\n}\n"]}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { SoundCloudTrack, SoundCloudPlaylist, SoundCloudToken, SoundCloudMe, SoundCloudActivitiesResponse, SoundCloudPaginatedResponse, SoundCloudUser, SoundCloudWebProfile, SoundCloudStreams, SoundCloudComment } from './types/index.mjs';
2
- export { SoundCloudActivity, SoundCloudCommentUser, SoundCloudQuota, SoundCloudSubscription, SoundCloudSubscriptionProduct } from './types/index.mjs';
1
+ import { SoundCloudTrack, SoundCloudPlaylist, SoundCloudToken, SoundCloudPaginatedResponse, SoundCloudMe, SoundCloudActivitiesResponse, SoundCloudUser, SoundCloudWebProfile, SoundCloudStreams, SoundCloudComment } from './types/index.mjs';
2
+ export { SoundCloudActivity, SoundCloudCommentUser, SoundCloudError, SoundCloudQuota, SoundCloudSubscription, SoundCloudSubscriptionProduct } from './types/index.mjs';
3
3
 
4
4
  interface RequestOptions {
5
5
  path: string;
@@ -8,6 +8,14 @@ interface RequestOptions {
8
8
  body?: Record<string, unknown> | FormData | URLSearchParams;
9
9
  contentType?: string;
10
10
  }
11
+ interface RetryConfig {
12
+ /** Max retries on 429/5xx (default: 3) */
13
+ maxRetries: number;
14
+ /** Base delay in ms for exponential backoff (default: 1000) */
15
+ retryBaseDelay: number;
16
+ /** Optional debug logger */
17
+ onDebug?: (message: string) => void;
18
+ }
11
19
  interface AutoRefreshContext {
12
20
  getToken: () => string | undefined;
13
21
  onTokenRefresh?: () => Promise<{
@@ -15,12 +23,18 @@ interface AutoRefreshContext {
15
23
  refresh_token?: string;
16
24
  }>;
17
25
  setToken: (accessToken: string, refreshToken?: string) => void;
26
+ retry?: RetryConfig;
18
27
  }
19
28
  /**
20
29
  * Make a request to the SoundCloud API using native fetch.
21
30
  * Returns parsed JSON, or for 302 redirects returns the Location header.
22
31
  */
23
32
  declare function scFetch<T>(options: RequestOptions, refreshCtx?: AutoRefreshContext): Promise<T>;
33
+ /**
34
+ * Fetch an absolute URL (e.g. next_href from paginated responses).
35
+ * Adds OAuth token if provided.
36
+ */
37
+ declare function scFetchUrl<T>(url: string, token?: string, retryConfig?: RetryConfig): Promise<T>;
24
38
 
25
39
  interface UpdateTrackParams {
26
40
  title?: string;
@@ -94,6 +108,12 @@ interface SoundCloudClientConfig {
94
108
  redirectUri?: string;
95
109
  /** Called automatically when a request returns 401. Return new tokens to retry. */
96
110
  onTokenRefresh?: (client: SoundCloudClient) => Promise<SoundCloudToken>;
111
+ /** Max retries on 429/5xx (default: 3) */
112
+ maxRetries?: number;
113
+ /** Base delay in ms for exponential backoff (default: 1000) */
114
+ retryBaseDelay?: number;
115
+ /** Optional debug logger for retry attempts, etc. */
116
+ onDebug?: (message: string) => void;
97
117
  }
98
118
  /** Optional token override, passed as the last parameter to client methods. */
99
119
  interface TokenOption {
@@ -123,6 +143,36 @@ declare class SoundCloudClient {
123
143
  get accessToken(): string | undefined;
124
144
  /** Get the currently stored refresh token, if any. */
125
145
  get refreshToken(): string | undefined;
146
+ /**
147
+ * Async generator that follows `next_href` automatically, yielding each page's `collection`.
148
+ *
149
+ * ```ts
150
+ * for await (const page of sc.paginate(() => sc.search.tracks("lofi"))) {
151
+ * console.log(page); // SoundCloudTrack[]
152
+ * }
153
+ * ```
154
+ */
155
+ paginate<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T[], void, undefined>;
156
+ /**
157
+ * Async generator that yields individual items across all pages.
158
+ *
159
+ * ```ts
160
+ * for await (const track of sc.paginateItems(() => sc.search.tracks("lofi"))) {
161
+ * console.log(track); // single SoundCloudTrack
162
+ * }
163
+ * ```
164
+ */
165
+ paginateItems<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T, void, undefined>;
166
+ /**
167
+ * Collects all pages into a single flat array.
168
+ *
169
+ * ```ts
170
+ * const allTracks = await sc.fetchAll(() => sc.search.tracks("lofi"), { maxItems: 100 });
171
+ * ```
172
+ */
173
+ fetchAll<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, options?: {
174
+ maxItems?: number;
175
+ }): Promise<T[]>;
126
176
  }
127
177
  declare namespace SoundCloudClient {
128
178
  class Auth {
@@ -293,6 +343,21 @@ declare namespace SoundCloudClient {
293
343
  }
294
344
  }
295
345
 
346
+ /**
347
+ * Async generator that follows `next_href` automatically, yielding each page's `collection`.
348
+ */
349
+ declare function paginate<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T[], void, undefined>;
350
+ /**
351
+ * Async generator that yields individual items across all pages.
352
+ */
353
+ declare function paginateItems<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T, void, undefined>;
354
+ /**
355
+ * Collects all pages into a single flat array with an optional max items limit.
356
+ */
357
+ declare function fetchAll<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>, options?: {
358
+ maxItems?: number;
359
+ }): Promise<T[]>;
360
+
296
361
  declare const getClientToken: (clientId: string, clientSecret: string) => Promise<SoundCloudToken>;
297
362
 
298
363
  declare const getUserToken: (clientId: string, clientSecret: string, redirectUri: string, code: string, codeVerifier?: string) => Promise<SoundCloudToken>;
@@ -437,4 +502,4 @@ declare const unrepostPlaylist: (token: string, playlistId: string | number) =>
437
502
  */
438
503
  declare const getSoundCloudWidgetUrl: (trackId: string | number) => string;
439
504
 
440
- export { type CreatePlaylistParams, type RequestOptions, SoundCloudActivitiesResponse, SoundCloudClient, type SoundCloudClientConfig, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudToken, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile, type TokenOption, type UpdatePlaylistParams, type UpdateTrackParams, createPlaylist, createTrackComment, deletePlaylist, deleteTrack, followUser, generateCodeChallenge, generateCodeVerifier, getAuthorizationUrl, getClientToken, getFollowers, getFollowings, getMe, getMeActivities, getMeActivitiesOwn, getMeActivitiesTracks, getMeFollowers, getMeFollowings, getMeFollowingsTracks, getMeLikesPlaylists, getMeLikesTracks, getMePlaylists, getMeTracks, getPlaylist, getPlaylistReposts, getPlaylistTracks, getRelatedTracks, getSoundCloudWidgetUrl, getTrack, getTrackComments, getTrackLikes, getTrackReposts, getTrackStreams, getUser, getUserLikesPlaylists, getUserLikesTracks, getUserPlaylists, getUserToken, getUserTracks, getUserWebProfiles, likePlaylist, likeTrack, refreshUserToken, repostPlaylist, repostTrack, resolveUrl, scFetch, searchPlaylists, searchTracks, searchUsers, signOut, unfollowUser, unlikePlaylist, unlikeTrack, unrepostPlaylist, unrepostTrack, updatePlaylist, updateTrack };
505
+ export { type CreatePlaylistParams, type RequestOptions, type RetryConfig, SoundCloudActivitiesResponse, SoundCloudClient, type SoundCloudClientConfig, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudToken, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile, type TokenOption, type UpdatePlaylistParams, type UpdateTrackParams, createPlaylist, createTrackComment, deletePlaylist, deleteTrack, fetchAll, followUser, generateCodeChallenge, generateCodeVerifier, getAuthorizationUrl, getClientToken, getFollowers, getFollowings, getMe, getMeActivities, getMeActivitiesOwn, getMeActivitiesTracks, getMeFollowers, getMeFollowings, getMeFollowingsTracks, getMeLikesPlaylists, getMeLikesTracks, getMePlaylists, getMeTracks, getPlaylist, getPlaylistReposts, getPlaylistTracks, getRelatedTracks, getSoundCloudWidgetUrl, getTrack, getTrackComments, getTrackLikes, getTrackReposts, getTrackStreams, getUser, getUserLikesPlaylists, getUserLikesTracks, getUserPlaylists, getUserToken, getUserTracks, getUserWebProfiles, likePlaylist, likeTrack, paginate, paginateItems, refreshUserToken, repostPlaylist, repostTrack, resolveUrl, scFetch, scFetchUrl, searchPlaylists, searchTracks, searchUsers, signOut, unfollowUser, unlikePlaylist, unlikeTrack, unrepostPlaylist, unrepostTrack, updatePlaylist, updateTrack };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { SoundCloudTrack, SoundCloudPlaylist, SoundCloudToken, SoundCloudMe, SoundCloudActivitiesResponse, SoundCloudPaginatedResponse, SoundCloudUser, SoundCloudWebProfile, SoundCloudStreams, SoundCloudComment } from './types/index.js';
2
- export { SoundCloudActivity, SoundCloudCommentUser, SoundCloudQuota, SoundCloudSubscription, SoundCloudSubscriptionProduct } from './types/index.js';
1
+ import { SoundCloudTrack, SoundCloudPlaylist, SoundCloudToken, SoundCloudPaginatedResponse, SoundCloudMe, SoundCloudActivitiesResponse, SoundCloudUser, SoundCloudWebProfile, SoundCloudStreams, SoundCloudComment } from './types/index.js';
2
+ export { SoundCloudActivity, SoundCloudCommentUser, SoundCloudError, SoundCloudQuota, SoundCloudSubscription, SoundCloudSubscriptionProduct } from './types/index.js';
3
3
 
4
4
  interface RequestOptions {
5
5
  path: string;
@@ -8,6 +8,14 @@ interface RequestOptions {
8
8
  body?: Record<string, unknown> | FormData | URLSearchParams;
9
9
  contentType?: string;
10
10
  }
11
+ interface RetryConfig {
12
+ /** Max retries on 429/5xx (default: 3) */
13
+ maxRetries: number;
14
+ /** Base delay in ms for exponential backoff (default: 1000) */
15
+ retryBaseDelay: number;
16
+ /** Optional debug logger */
17
+ onDebug?: (message: string) => void;
18
+ }
11
19
  interface AutoRefreshContext {
12
20
  getToken: () => string | undefined;
13
21
  onTokenRefresh?: () => Promise<{
@@ -15,12 +23,18 @@ interface AutoRefreshContext {
15
23
  refresh_token?: string;
16
24
  }>;
17
25
  setToken: (accessToken: string, refreshToken?: string) => void;
26
+ retry?: RetryConfig;
18
27
  }
19
28
  /**
20
29
  * Make a request to the SoundCloud API using native fetch.
21
30
  * Returns parsed JSON, or for 302 redirects returns the Location header.
22
31
  */
23
32
  declare function scFetch<T>(options: RequestOptions, refreshCtx?: AutoRefreshContext): Promise<T>;
33
+ /**
34
+ * Fetch an absolute URL (e.g. next_href from paginated responses).
35
+ * Adds OAuth token if provided.
36
+ */
37
+ declare function scFetchUrl<T>(url: string, token?: string, retryConfig?: RetryConfig): Promise<T>;
24
38
 
25
39
  interface UpdateTrackParams {
26
40
  title?: string;
@@ -94,6 +108,12 @@ interface SoundCloudClientConfig {
94
108
  redirectUri?: string;
95
109
  /** Called automatically when a request returns 401. Return new tokens to retry. */
96
110
  onTokenRefresh?: (client: SoundCloudClient) => Promise<SoundCloudToken>;
111
+ /** Max retries on 429/5xx (default: 3) */
112
+ maxRetries?: number;
113
+ /** Base delay in ms for exponential backoff (default: 1000) */
114
+ retryBaseDelay?: number;
115
+ /** Optional debug logger for retry attempts, etc. */
116
+ onDebug?: (message: string) => void;
97
117
  }
98
118
  /** Optional token override, passed as the last parameter to client methods. */
99
119
  interface TokenOption {
@@ -123,6 +143,36 @@ declare class SoundCloudClient {
123
143
  get accessToken(): string | undefined;
124
144
  /** Get the currently stored refresh token, if any. */
125
145
  get refreshToken(): string | undefined;
146
+ /**
147
+ * Async generator that follows `next_href` automatically, yielding each page's `collection`.
148
+ *
149
+ * ```ts
150
+ * for await (const page of sc.paginate(() => sc.search.tracks("lofi"))) {
151
+ * console.log(page); // SoundCloudTrack[]
152
+ * }
153
+ * ```
154
+ */
155
+ paginate<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T[], void, undefined>;
156
+ /**
157
+ * Async generator that yields individual items across all pages.
158
+ *
159
+ * ```ts
160
+ * for await (const track of sc.paginateItems(() => sc.search.tracks("lofi"))) {
161
+ * console.log(track); // single SoundCloudTrack
162
+ * }
163
+ * ```
164
+ */
165
+ paginateItems<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T, void, undefined>;
166
+ /**
167
+ * Collects all pages into a single flat array.
168
+ *
169
+ * ```ts
170
+ * const allTracks = await sc.fetchAll(() => sc.search.tracks("lofi"), { maxItems: 100 });
171
+ * ```
172
+ */
173
+ fetchAll<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, options?: {
174
+ maxItems?: number;
175
+ }): Promise<T[]>;
126
176
  }
127
177
  declare namespace SoundCloudClient {
128
178
  class Auth {
@@ -293,6 +343,21 @@ declare namespace SoundCloudClient {
293
343
  }
294
344
  }
295
345
 
346
+ /**
347
+ * Async generator that follows `next_href` automatically, yielding each page's `collection`.
348
+ */
349
+ declare function paginate<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T[], void, undefined>;
350
+ /**
351
+ * Async generator that yields individual items across all pages.
352
+ */
353
+ declare function paginateItems<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>): AsyncGenerator<T, void, undefined>;
354
+ /**
355
+ * Collects all pages into a single flat array with an optional max items limit.
356
+ */
357
+ declare function fetchAll<T>(firstPage: () => Promise<SoundCloudPaginatedResponse<T>>, fetchNext: (url: string) => Promise<SoundCloudPaginatedResponse<T>>, options?: {
358
+ maxItems?: number;
359
+ }): Promise<T[]>;
360
+
296
361
  declare const getClientToken: (clientId: string, clientSecret: string) => Promise<SoundCloudToken>;
297
362
 
298
363
  declare const getUserToken: (clientId: string, clientSecret: string, redirectUri: string, code: string, codeVerifier?: string) => Promise<SoundCloudToken>;
@@ -437,4 +502,4 @@ declare const unrepostPlaylist: (token: string, playlistId: string | number) =>
437
502
  */
438
503
  declare const getSoundCloudWidgetUrl: (trackId: string | number) => string;
439
504
 
440
- export { type CreatePlaylistParams, type RequestOptions, SoundCloudActivitiesResponse, SoundCloudClient, type SoundCloudClientConfig, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudToken, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile, type TokenOption, type UpdatePlaylistParams, type UpdateTrackParams, createPlaylist, createTrackComment, deletePlaylist, deleteTrack, followUser, generateCodeChallenge, generateCodeVerifier, getAuthorizationUrl, getClientToken, getFollowers, getFollowings, getMe, getMeActivities, getMeActivitiesOwn, getMeActivitiesTracks, getMeFollowers, getMeFollowings, getMeFollowingsTracks, getMeLikesPlaylists, getMeLikesTracks, getMePlaylists, getMeTracks, getPlaylist, getPlaylistReposts, getPlaylistTracks, getRelatedTracks, getSoundCloudWidgetUrl, getTrack, getTrackComments, getTrackLikes, getTrackReposts, getTrackStreams, getUser, getUserLikesPlaylists, getUserLikesTracks, getUserPlaylists, getUserToken, getUserTracks, getUserWebProfiles, likePlaylist, likeTrack, refreshUserToken, repostPlaylist, repostTrack, resolveUrl, scFetch, searchPlaylists, searchTracks, searchUsers, signOut, unfollowUser, unlikePlaylist, unlikeTrack, unrepostPlaylist, unrepostTrack, updatePlaylist, updateTrack };
505
+ export { type CreatePlaylistParams, type RequestOptions, type RetryConfig, SoundCloudActivitiesResponse, SoundCloudClient, type SoundCloudClientConfig, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudToken, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile, type TokenOption, type UpdatePlaylistParams, type UpdateTrackParams, createPlaylist, createTrackComment, deletePlaylist, deleteTrack, fetchAll, followUser, generateCodeChallenge, generateCodeVerifier, getAuthorizationUrl, getClientToken, getFollowers, getFollowings, getMe, getMeActivities, getMeActivitiesOwn, getMeActivitiesTracks, getMeFollowers, getMeFollowings, getMeFollowingsTracks, getMeLikesPlaylists, getMeLikesTracks, getMePlaylists, getMeTracks, getPlaylist, getPlaylistReposts, getPlaylistTracks, getRelatedTracks, getSoundCloudWidgetUrl, getTrack, getTrackComments, getTrackLikes, getTrackReposts, getTrackStreams, getUser, getUserLikesPlaylists, getUserLikesTracks, getUserPlaylists, getUserToken, getUserTracks, getUserWebProfiles, likePlaylist, likeTrack, paginate, paginateItems, refreshUserToken, repostPlaylist, repostTrack, resolveUrl, scFetch, scFetchUrl, searchPlaylists, searchTracks, searchUsers, signOut, unfollowUser, unlikePlaylist, unlikeTrack, unrepostPlaylist, unrepostTrack, updatePlaylist, updateTrack };