soundcloud-api-ts 1.11.3 → 1.13.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/AGENTS.md +6 -1
- package/README.md +127 -26
- package/dist/{chunk-HCEUCJMA.mjs → chunk-JH7TLL2C.mjs} +286 -9
- package/dist/chunk-JH7TLL2C.mjs.map +1 -0
- package/dist/{chunk-2747SK6H.js → chunk-VBDIRSOG.js} +290 -8
- package/dist/chunk-VBDIRSOG.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/cli.mjs +1 -1
- package/dist/index-DX6Anc1-.d.mts +582 -0
- package/dist/index-DX6Anc1-.d.ts +582 -0
- package/dist/index.d.mts +310 -3
- package/dist/index.d.ts +310 -3
- package/dist/index.js +83 -63
- package/dist/index.mjs +1 -1
- package/dist/types/index.d.mts +1 -555
- package/dist/types/index.d.ts +1 -555
- package/llms.txt +45 -0
- package/package.json +4 -2
- package/dist/chunk-2747SK6H.js.map +0 -1
- package/dist/chunk-HCEUCJMA.mjs.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -123,9 +123,14 @@ try {
|
|
|
123
123
|
|
|
124
124
|
1. **Token required for ALL requests** — even public endpoints like `getTrack` need at least a client credentials token. Call `setToken()` first.
|
|
125
125
|
2. **User token vs client token** — write operations (like, repost, comment, follow, create/update/delete) require a user token obtained via the authorization code flow. A client credentials token won't work.
|
|
126
|
-
3. **Rate limits exist** — SoundCloud returns 429 when rate limited. The client has built-in retry with exponential backoff (configurable via `maxRetries` and `retryBaseDelay`).
|
|
126
|
+
3. **Rate limits exist** — SoundCloud returns 429 when rate limited. The client has built-in retry with exponential backoff (configurable via `maxRetries` and `retryBaseDelay`). `Retry-After` header is honored (capped 60s).
|
|
127
127
|
4. **Auto token refresh** — pass `onTokenRefresh` in the config to automatically refresh expired tokens on 401.
|
|
128
128
|
5. **Request telemetry** — pass `onRequest` in the config to receive `SCRequestTelemetry` after every request (method, path, duration, status, retries, error). Fires on all paths including pagination and retries.
|
|
129
|
+
6. **sc.raw** — `sc.raw.get('/tracks/{id}', { id: 123456 })` calls any endpoint without a typed wrapper. Returns `RawResponse<T>` with `{ data, status, headers }`. Does NOT throw on non-2xx — check `res.status` yourself. Good for endpoints not yet wrapped.
|
|
130
|
+
7. **Fetch injection** — pass `fetch` and `AbortController` in the constructor for Bun/Deno/Cloudflare Workers portability. No Node-only APIs used at runtime.
|
|
131
|
+
8. **Deduplication** — concurrent identical GETs share a single in-flight promise (`dedupe: true` by default). Prevents redundant fetches in SSR or concurrent component trees.
|
|
132
|
+
9. **Cache** — pass a `SoundCloudCache` implementation in the constructor to cache GET responses. Base package defines the interface only; bring your own backend. `cacheTtlMs` defaults to 60000ms.
|
|
133
|
+
10. **Retry hook** — pass `onRetry` to receive `RetryInfo` on each retry: `{ attempt, delayMs, reason, status?, url }`.
|
|
129
134
|
6. **No env vars** — the package reads no environment variables. Pass `clientId`, `clientSecret`, and `redirectUri` directly to the constructor.
|
|
130
135
|
7. **IDs can be numbers or strings** — all ID parameters accept `string | number`.
|
|
131
136
|
8. **Search pagination** — search uses zero-based `pageNumber` (10 results per page), not cursor-based pagination.
|
package/README.md
CHANGED
|
@@ -11,28 +11,26 @@
|
|
|
11
11
|
[]()
|
|
12
12
|
[](https://twin-paws.github.io/soundcloud-api-ts/)
|
|
13
13
|
[](https://github.com/twin-paws/soundcloud-api-ts)
|
|
14
|
+
[](tools/coverage-baseline.json)
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Zero dependencies, native `fetch`, built-in OAuth 2.1 + PKCE, automatic retry, and an interactive CLI.
|
|
18
|
-
|
|
19
|
-
This package is intended to be the recommended option for developers looking for a TypeScript SoundCloud API client.
|
|
16
|
+
The TypeScript SoundCloud API client built on the official API. Zero runtime dependencies, native `fetch`, OAuth 2.1 + PKCE, production-grade retry and deduplication, pluggable cache, raw escape hatch, and an interactive CLI.
|
|
20
17
|
|
|
21
18
|
## Why This Package?
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
Most TypeScript SoundCloud clients fall into one of two categories: unmaintained wrappers that predate OAuth 2.1, or scrapers that harvest undocumented `api-v2` client IDs from browser dev tools and break whenever SoundCloud ships a frontend update. soundcloud-api-ts is neither.
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
22
|
+
It is built on SoundCloud's **official documented API** with registered app credentials, OAuth 2.1 + PKCE, and a production-grade HTTP layer — all with zero runtime dependencies.
|
|
23
|
+
|
|
24
|
+
- **Official API only** — `api.soundcloud.com` + `secure.soundcloud.com` OAuth. No `api-v2` scraping, no harvested client IDs, no terms violations.
|
|
25
|
+
- **TypeScript-first** — full types ship in the package. No `@types/*` installs, no casting to `any`.
|
|
26
|
+
- **Zero dependencies** — native `fetch`, nothing in `node_modules` at runtime. 4.5 KB min+gz.
|
|
27
|
+
- **Production HTTP layer** — exponential backoff on 429/5xx, `Retry-After` header respected, in-flight GET deduplication, pluggable cache interface, `onRetry` hook.
|
|
28
|
+
- **Runtime portable** — inject your own `fetch` and `AbortController` for Cloudflare Workers, Bun, Deno, and Edge runtimes.
|
|
29
|
+
- **Raw escape hatch** — `sc.raw.get('/any/endpoint/{id}', { id })` calls anything in the spec, not just wrapped endpoints. Never blocked by a missing wrapper.
|
|
30
|
+
- **Full auth support** — client credentials flow for server-to-server, authorization code + PKCE for user-context operations, auto token refresh on 401.
|
|
31
|
+
- **Pagination built-in** — async iterators and `fetchAll` helpers across all paginated endpoints.
|
|
32
|
+
- **Interactive CLI** — `sc-cli tracks <id>`, `sc-cli search <query>`, `sc-cli me` from your terminal.
|
|
33
|
+
- **LLM-friendly** — ships `llms.txt`, `llms-full.txt`, and `AGENTS.md` for AI coding agents.
|
|
36
34
|
|
|
37
35
|
## Comparison
|
|
38
36
|
|
|
@@ -40,22 +38,23 @@ Unlike legacy JavaScript SoundCloud SDKs and community wrappers that require sep
|
|
|
40
38
|
| --- | --- | --- | --- |
|
|
41
39
|
| TypeScript | ✅ Native | ✅ | ✅ |
|
|
42
40
|
| Dependencies | **0** | 1 | 3 (lodash, cookie, undici) |
|
|
43
|
-
| Bundle size (min+gz) | **4.5 KB** | ❌ unbundlable
|
|
44
|
-
| Auth method | **Official OAuth 2.1** | ⚠️
|
|
41
|
+
| Bundle size (min+gz) | **4.5 KB** | ❌ unbundlable | 191 KB |
|
|
42
|
+
| Auth method | **Official OAuth 2.1** | ⚠️ Scrapes client ID | ⚠️ Scrapes client ID |
|
|
45
43
|
| PKCE support | ✅ | ❌ | ❌ |
|
|
46
|
-
| Auto token refresh | ✅
|
|
47
|
-
| Auto retry (429/5xx) | ✅
|
|
44
|
+
| Auto token refresh | ✅ | ❌ | ❌ |
|
|
45
|
+
| Auto retry (429/5xx) | ✅ + Retry-After | ❌ | ❌ |
|
|
46
|
+
| In-flight deduplication | ✅ | ❌ | ❌ |
|
|
47
|
+
| Pluggable cache interface | ✅ | ❌ | ❌ |
|
|
48
|
+
| Fetch injection (Workers/Bun/Deno) | ✅ | ❌ | ❌ |
|
|
49
|
+
| Raw escape hatch | ✅ `sc.raw` | ❌ | ❌ |
|
|
48
50
|
| CLI tool | ✅ `sc-cli` | ❌ | ❌ |
|
|
49
51
|
| Pagination helpers | ✅ async iterators | ❌ | ✅ |
|
|
50
52
|
| Typed errors | ✅ `SoundCloudError` | ❌ | ❌ |
|
|
51
53
|
| Test coverage | **100%** | — | — |
|
|
52
54
|
| API docs site | ✅ [TypeDoc](https://twin-paws.github.io/soundcloud-api-ts/) | ✅ | ❌ |
|
|
53
55
|
| LLM/AI-friendly | ✅ llms.txt + AGENTS.md | ❌ | ❌ |
|
|
54
|
-
| Maintained | ✅ 2026 | ✅ 2025 | ✅ 2026 |
|
|
55
56
|
|
|
56
|
-
> **Why does auth method matter?** `soundcloud.ts` and `soundcloud-fetch`
|
|
57
|
-
>
|
|
58
|
-
> `soundcloud-api-ts` uses the **official documented API** (`api.soundcloud.com`) with registered app credentials, OAuth 2.1 via `secure.soundcloud.com` as specified by SoundCloud, PKCE for public clients, and automatic token refresh.
|
|
57
|
+
> **Why does auth method matter?** `soundcloud.ts` and `soundcloud-fetch` scrape SoundCloud's undocumented `api-v2` and require harvesting a client ID from browser dev tools. This breaks whenever SoundCloud ships a frontend update, and the [API Terms of Use](https://developers.soundcloud.com/docs/api/terms-of-use) explicitly prohibit it: *"any attempt to circumvent this and obtain a new client ID and Security Code is strictly prohibited."*
|
|
59
58
|
|
|
60
59
|
**Coming from `soundcloud.ts`?** See the [Migration Guide](docs/MIGRATING.md) — most changes are find-and-replace.
|
|
61
60
|
|
|
@@ -182,6 +181,17 @@ await sc.auth.signOut(token.access_token);
|
|
|
182
181
|
sc.clearToken();
|
|
183
182
|
```
|
|
184
183
|
|
|
184
|
+
### Auth at a glance
|
|
185
|
+
|
|
186
|
+
| Endpoint category | Client Credentials | User Token |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| tracks, users, search, playlists, resolve | ✅ | ✅ |
|
|
189
|
+
| /me endpoints | ❌ | ✅ |
|
|
190
|
+
| likes, reposts | ❌ | ✅ |
|
|
191
|
+
| create/update/delete | ❌ | ✅ |
|
|
192
|
+
|
|
193
|
+
See [Auth Guide](docs/auth-guide.md) for full details, token provider patterns, and troubleshooting.
|
|
194
|
+
|
|
185
195
|
## Client Class
|
|
186
196
|
|
|
187
197
|
The `SoundCloudClient` class organizes all endpoints into namespaces. Token is resolved automatically when `setToken()` has been called. Override per-call via `{ token: "..." }` options object.
|
|
@@ -216,6 +226,7 @@ sc.me.unfollow(userUrn, options?)
|
|
|
216
226
|
sc.me.getFollowers(limit?, options?)
|
|
217
227
|
sc.me.getPlaylists(limit?, options?)
|
|
218
228
|
sc.me.getTracks(limit?, options?)
|
|
229
|
+
sc.me.getConnections(options?) // connected social accounts; may require app approval
|
|
219
230
|
|
|
220
231
|
// Users
|
|
221
232
|
sc.users.getUser(userId, options?)
|
|
@@ -229,6 +240,7 @@ sc.users.getWebProfiles(userId, options?)
|
|
|
229
240
|
|
|
230
241
|
// Tracks
|
|
231
242
|
sc.tracks.getTrack(trackId, options?)
|
|
243
|
+
sc.tracks.getTracks(ids[], options?) // batch fetch by IDs
|
|
232
244
|
sc.tracks.getStreams(trackId, options?)
|
|
233
245
|
sc.tracks.getComments(trackId, limit?, options?)
|
|
234
246
|
sc.tracks.createComment(trackId, body, timestamp?, options?)
|
|
@@ -267,6 +279,11 @@ sc.search.playlists(query, pageNumber?, options?)
|
|
|
267
279
|
sc.resolve.resolveUrl(url, options?)
|
|
268
280
|
```
|
|
269
281
|
|
|
282
|
+
// Raw escape hatch — call any endpoint
|
|
283
|
+
sc.raw.get('/tracks/{id}', { id: 123456 })
|
|
284
|
+
sc.raw.post('/tracks/{id}/comments', { body: { body: 'great track' } })
|
|
285
|
+
sc.raw.request({ method: 'GET', path: '/me', query: {} })
|
|
286
|
+
|
|
270
287
|
Where `options` is `{ token?: string }` — only needed to override the stored token.
|
|
271
288
|
|
|
272
289
|
## Standalone Functions
|
|
@@ -403,21 +420,105 @@ try {
|
|
|
403
420
|
|
|
404
421
|
Error messages are parsed directly from SoundCloud's API response format, giving you the most useful message available.
|
|
405
422
|
|
|
423
|
+
## Raw API — Call Any Endpoint
|
|
424
|
+
|
|
425
|
+
`sc.raw` is a low-level escape hatch that lets you call any SoundCloud API endpoint — including ones not yet wrapped — while still using your configured auth and fetch.
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
import { SoundCloudClient, type RawResponse } from 'soundcloud-api-ts';
|
|
429
|
+
|
|
430
|
+
const sc = new SoundCloudClient({ clientId, clientSecret });
|
|
431
|
+
const token = await sc.auth.getClientToken();
|
|
432
|
+
sc.setToken(token.access_token);
|
|
433
|
+
|
|
434
|
+
// Path templating: {id} is replaced from the params object
|
|
435
|
+
const res: RawResponse = await sc.raw.get('/tracks/{id}', { id: 123456 });
|
|
436
|
+
console.log(res.data); // parsed JSON body
|
|
437
|
+
console.log(res.status); // 200
|
|
438
|
+
console.log(res.headers); // response headers
|
|
439
|
+
|
|
440
|
+
// POST with body
|
|
441
|
+
await sc.raw.post('/tracks/{id}/comments', { id: 123456, body: { body: 'great track', timestamp: 30000 } });
|
|
442
|
+
|
|
443
|
+
// Fully generic
|
|
444
|
+
await sc.raw.request({ method: 'DELETE', path: '/tracks/{id}', query: { id: 123456 } });
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`sc.raw` returns `RawResponse<T = unknown>` — `{ data: T, status: number, headers: Record<string, string> }`. It does **not** throw on non-2xx status codes; check `res.status` yourself.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## In-Flight Deduplication & Caching
|
|
452
|
+
|
|
453
|
+
### GET Coalescing (default on)
|
|
454
|
+
|
|
455
|
+
When multiple callers fire the same GET request simultaneously (SSR, React StrictMode, concurrent components), only one fetch is made. All callers share the same promise.
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
const sc = new SoundCloudClient({
|
|
459
|
+
clientId, clientSecret,
|
|
460
|
+
dedupe: true, // default — set false to disable
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Pluggable Cache
|
|
465
|
+
|
|
466
|
+
Bring your own cache backend — in-memory, Redis, Cloudflare KV, whatever. The base package defines the interface only (no implementation, no deps):
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import { SoundCloudClient, type SoundCloudCache } from 'soundcloud-api-ts';
|
|
470
|
+
|
|
471
|
+
const myCache: SoundCloudCache = {
|
|
472
|
+
get: (key) => store.get(key),
|
|
473
|
+
set: (key, value, { ttlMs }) => store.set(key, value, ttlMs),
|
|
474
|
+
delete: (key) => store.delete(key),
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const sc = new SoundCloudClient({
|
|
478
|
+
clientId, clientSecret,
|
|
479
|
+
cache: myCache,
|
|
480
|
+
cacheTtlMs: 30_000, // 30s default per GET response
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Runtime Portability
|
|
487
|
+
|
|
488
|
+
Pass a custom `fetch` implementation to work in any runtime — Cloudflare Workers, Deno, Bun, or environments without a global `fetch`:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
const sc = new SoundCloudClient({
|
|
492
|
+
clientId, clientSecret,
|
|
493
|
+
fetch: myCustomFetch, // optional: custom fetch
|
|
494
|
+
AbortController: myAbortCtrl, // optional: custom AbortController
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
No Node-only APIs are used at runtime. The client works anywhere `fetch` is available.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
406
502
|
## Rate Limiting & Retries
|
|
407
503
|
|
|
408
504
|
The client automatically retries on **429 Too Many Requests** and **5xx Server Errors** with exponential backoff:
|
|
409
505
|
|
|
410
506
|
```ts
|
|
507
|
+
import { SoundCloudClient, type RetryInfo } from 'soundcloud-api-ts';
|
|
508
|
+
|
|
411
509
|
const sc = new SoundCloudClient({
|
|
412
510
|
clientId: "...",
|
|
413
511
|
clientSecret: "...",
|
|
414
512
|
maxRetries: 3, // default: 3
|
|
415
513
|
retryBaseDelay: 1000, // default: 1000ms
|
|
416
514
|
onDebug: (msg) => console.log(msg), // optional retry logging
|
|
515
|
+
onRetry: (info: RetryInfo) => {
|
|
516
|
+
console.warn(`[SC] retry #${info.attempt} — ${info.reason} (${info.status}) delay=${info.delayMs}ms url=${info.url}`);
|
|
517
|
+
},
|
|
417
518
|
});
|
|
418
519
|
```
|
|
419
520
|
|
|
420
|
-
- **429 responses**
|
|
521
|
+
- **429 responses** use the `Retry-After` header value as the delay (capped at 60s)
|
|
421
522
|
- **5xx responses** (500, 502, 503, 504) are retried with exponential backoff
|
|
422
523
|
- **4xx errors** (except 429) are NOT retried — they throw immediately
|
|
423
524
|
- **401 errors** trigger `onTokenRefresh` (if configured) instead of retry
|
|
@@ -15,7 +15,7 @@ function getRetryDelay(response, attempt, config) {
|
|
|
15
15
|
const retryAfter = response.headers.get("retry-after");
|
|
16
16
|
if (retryAfter) {
|
|
17
17
|
const seconds = Number(retryAfter);
|
|
18
|
-
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
18
|
+
if (!Number.isNaN(seconds)) return Math.min(seconds * 1e3, 6e4);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
const base = config.retryBaseDelay * Math.pow(2, attempt);
|
|
@@ -91,9 +91,26 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
91
91
|
return void 0;
|
|
92
92
|
}
|
|
93
93
|
if (response.ok) {
|
|
94
|
-
const
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
if (typeof data === "object" && data !== null) {
|
|
96
|
+
const metaHeaders = {};
|
|
97
|
+
if (typeof response.headers.forEach === "function") {
|
|
98
|
+
response.headers.forEach((value, key) => {
|
|
99
|
+
metaHeaders[key] = value;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
Object.defineProperty(data, "_meta", {
|
|
104
|
+
value: { status: response.status, headers: metaHeaders },
|
|
105
|
+
enumerable: false,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
95
112
|
emitTelemetry();
|
|
96
|
-
return
|
|
113
|
+
return data;
|
|
97
114
|
}
|
|
98
115
|
if (!isRetryable(response.status)) {
|
|
99
116
|
const body2 = await parseErrorBody(response);
|
|
@@ -108,6 +125,13 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
108
125
|
retryConfig.onDebug?.(
|
|
109
126
|
`Retry ${attempt + 1}/${retryConfig.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
|
|
110
127
|
);
|
|
128
|
+
retryConfig.onRetry?.({
|
|
129
|
+
attempt: retryCount,
|
|
130
|
+
delayMs,
|
|
131
|
+
reason: `${response.status} ${response.statusText}`,
|
|
132
|
+
status: response.status,
|
|
133
|
+
url
|
|
134
|
+
});
|
|
111
135
|
await delay(delayMs);
|
|
112
136
|
}
|
|
113
137
|
}
|
|
@@ -161,9 +185,26 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
161
185
|
return void 0;
|
|
162
186
|
}
|
|
163
187
|
if (response.ok) {
|
|
164
|
-
const
|
|
188
|
+
const data = await response.json();
|
|
189
|
+
if (typeof data === "object" && data !== null) {
|
|
190
|
+
const metaHeaders = {};
|
|
191
|
+
if (typeof response.headers.forEach === "function") {
|
|
192
|
+
response.headers.forEach((value, key) => {
|
|
193
|
+
metaHeaders[key] = value;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
Object.defineProperty(data, "_meta", {
|
|
198
|
+
value: { status: response.status, headers: metaHeaders },
|
|
199
|
+
enumerable: false,
|
|
200
|
+
configurable: true,
|
|
201
|
+
writable: true
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
165
206
|
emitTelemetry();
|
|
166
|
-
return
|
|
207
|
+
return data;
|
|
167
208
|
}
|
|
168
209
|
if (!isRetryable(response.status)) {
|
|
169
210
|
const body2 = await parseErrorBody(response);
|
|
@@ -178,6 +219,13 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
178
219
|
config.onDebug?.(
|
|
179
220
|
`Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
|
|
180
221
|
);
|
|
222
|
+
config.onRetry?.({
|
|
223
|
+
attempt: retryCount,
|
|
224
|
+
delayMs,
|
|
225
|
+
reason: `${response.status} ${response.statusText}`,
|
|
226
|
+
status: response.status,
|
|
227
|
+
url
|
|
228
|
+
});
|
|
181
229
|
await delay(delayMs);
|
|
182
230
|
}
|
|
183
231
|
}
|
|
@@ -215,6 +263,96 @@ async function fetchAll(firstPage, fetchNext, options) {
|
|
|
215
263
|
return result;
|
|
216
264
|
}
|
|
217
265
|
|
|
266
|
+
// src/client/raw.ts
|
|
267
|
+
var RawClient = class {
|
|
268
|
+
constructor(baseUrl, getToken, fetchFn) {
|
|
269
|
+
this.baseUrl = baseUrl;
|
|
270
|
+
this.getToken = getToken;
|
|
271
|
+
this.fetchFn = fetchFn;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Make a raw HTTP request. Path template placeholders like `{id}` are substituted
|
|
275
|
+
* from matching keys in `query` before the remaining query params are appended to
|
|
276
|
+
* the URL as search parameters.
|
|
277
|
+
*/
|
|
278
|
+
async request({
|
|
279
|
+
method,
|
|
280
|
+
path,
|
|
281
|
+
query,
|
|
282
|
+
body,
|
|
283
|
+
token
|
|
284
|
+
}) {
|
|
285
|
+
let resolvedPath = path;
|
|
286
|
+
const remainingQuery = {};
|
|
287
|
+
if (query) {
|
|
288
|
+
for (const [key, value] of Object.entries(query)) {
|
|
289
|
+
if (value === void 0) continue;
|
|
290
|
+
const placeholder = `{${key}}`;
|
|
291
|
+
if (resolvedPath.includes(placeholder)) {
|
|
292
|
+
resolvedPath = resolvedPath.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
|
|
293
|
+
} else {
|
|
294
|
+
remainingQuery[key] = String(value);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const fullUrl = resolvedPath.startsWith("http") ? new URL(resolvedPath) : new URL(resolvedPath, this.baseUrl);
|
|
299
|
+
for (const [key, value] of Object.entries(remainingQuery)) {
|
|
300
|
+
fullUrl.searchParams.set(key, value);
|
|
301
|
+
}
|
|
302
|
+
const headers = {
|
|
303
|
+
Accept: "application/json"
|
|
304
|
+
};
|
|
305
|
+
const authToken = token ?? this.getToken();
|
|
306
|
+
if (authToken) {
|
|
307
|
+
headers["Authorization"] = `OAuth ${authToken}`;
|
|
308
|
+
}
|
|
309
|
+
let fetchBody;
|
|
310
|
+
if (body !== void 0) {
|
|
311
|
+
headers["Content-Type"] = "application/json";
|
|
312
|
+
fetchBody = JSON.stringify(body);
|
|
313
|
+
}
|
|
314
|
+
const response = await this.fetchFn(fullUrl.toString(), {
|
|
315
|
+
method,
|
|
316
|
+
headers,
|
|
317
|
+
body: fetchBody
|
|
318
|
+
});
|
|
319
|
+
const responseHeaders = {};
|
|
320
|
+
if (typeof response.headers.forEach === "function") {
|
|
321
|
+
response.headers.forEach((value, key) => {
|
|
322
|
+
responseHeaders[key] = value;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
let data;
|
|
326
|
+
const contentLength = response.headers.get("content-length");
|
|
327
|
+
if (response.status === 204 || contentLength === "0") {
|
|
328
|
+
data = void 0;
|
|
329
|
+
} else {
|
|
330
|
+
try {
|
|
331
|
+
data = await response.json();
|
|
332
|
+
} catch {
|
|
333
|
+
data = void 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { data, status: response.status, headers: responseHeaders };
|
|
337
|
+
}
|
|
338
|
+
/** GET shorthand */
|
|
339
|
+
get(path, params) {
|
|
340
|
+
return this.request({ method: "GET", path, query: params });
|
|
341
|
+
}
|
|
342
|
+
/** POST shorthand */
|
|
343
|
+
post(path, body) {
|
|
344
|
+
return this.request({ method: "POST", path, body });
|
|
345
|
+
}
|
|
346
|
+
/** PUT shorthand */
|
|
347
|
+
put(path, body) {
|
|
348
|
+
return this.request({ method: "PUT", path, body });
|
|
349
|
+
}
|
|
350
|
+
/** DELETE shorthand */
|
|
351
|
+
delete(path) {
|
|
352
|
+
return this.request({ method: "DELETE", path });
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
218
356
|
// src/client/SoundCloudClient.ts
|
|
219
357
|
function resolveToken(tokenGetter, explicit) {
|
|
220
358
|
const t = explicit ?? tokenGetter();
|
|
@@ -243,6 +381,8 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
243
381
|
likes;
|
|
244
382
|
/** Repost/unrepost actions (/reposts) */
|
|
245
383
|
reposts;
|
|
384
|
+
/** Low-level raw HTTP client — returns status/headers without throwing on non-2xx */
|
|
385
|
+
raw;
|
|
246
386
|
/**
|
|
247
387
|
* Creates a new SoundCloudClient instance.
|
|
248
388
|
*
|
|
@@ -254,7 +394,8 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
254
394
|
const retryConfig = {
|
|
255
395
|
maxRetries: config.maxRetries ?? 3,
|
|
256
396
|
retryBaseDelay: config.retryBaseDelay ?? 1e3,
|
|
257
|
-
onDebug: config.onDebug
|
|
397
|
+
onDebug: config.onDebug,
|
|
398
|
+
onRetry: config.onRetry
|
|
258
399
|
};
|
|
259
400
|
const refreshCtx = config.onTokenRefresh ? {
|
|
260
401
|
getToken,
|
|
@@ -283,6 +424,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
283
424
|
this.resolve = new _SoundCloudClient.Resolve(getToken, refreshCtx);
|
|
284
425
|
this.likes = new _SoundCloudClient.Likes(getToken, refreshCtx);
|
|
285
426
|
this.reposts = new _SoundCloudClient.Reposts(getToken, refreshCtx);
|
|
427
|
+
this.raw = new RawClient("https://api.soundcloud.com", getToken, config.fetch ?? globalThis.fetch);
|
|
286
428
|
}
|
|
287
429
|
/**
|
|
288
430
|
* Store an access token (and optionally refresh token) on this client instance.
|
|
@@ -730,6 +872,27 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
730
872
|
const t = resolveToken(this.getToken, options?.token);
|
|
731
873
|
return this.fetch({ path: `/me/tracks?${limit ? `limit=${limit}&` : ""}linked_partitioning=true`, method: "GET", token: t });
|
|
732
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* List the authenticated user's connected external social accounts.
|
|
877
|
+
*
|
|
878
|
+
* @param options - Optional token override
|
|
879
|
+
* @returns Array of connection objects for linked social services (Twitter, Facebook, etc.)
|
|
880
|
+
* @throws {SoundCloudError} When the API returns an error
|
|
881
|
+
*
|
|
882
|
+
* @remarks This endpoint may require elevated API access or app approval.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```ts
|
|
886
|
+
* const connections = await sc.me.getConnections();
|
|
887
|
+
* connections.forEach(c => console.log(c.service, c.display_name));
|
|
888
|
+
* ```
|
|
889
|
+
*
|
|
890
|
+
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/me/get_me_connections
|
|
891
|
+
*/
|
|
892
|
+
async getConnections(options) {
|
|
893
|
+
const t = resolveToken(this.getToken, options?.token);
|
|
894
|
+
return this.fetch({ path: "/me/connections", method: "GET", token: t });
|
|
895
|
+
}
|
|
733
896
|
}
|
|
734
897
|
SoundCloudClient2.Me = Me;
|
|
735
898
|
class Users {
|
|
@@ -901,6 +1064,26 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
901
1064
|
const t = resolveToken(this.getToken, options?.token);
|
|
902
1065
|
return this.fetch({ path: `/tracks/${trackId}`, method: "GET", token: t });
|
|
903
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Fetch multiple tracks by their IDs in a single request.
|
|
1069
|
+
*
|
|
1070
|
+
* @param ids - Array of track IDs (numeric or string URNs)
|
|
1071
|
+
* @param options - Optional token override
|
|
1072
|
+
* @returns Array of track objects (may be shorter than `ids` if some tracks are unavailable)
|
|
1073
|
+
* @throws {SoundCloudError} When the API returns an error
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```ts
|
|
1077
|
+
* const tracks = await sc.tracks.getTracks([123456, 234567, 345678]);
|
|
1078
|
+
* tracks.forEach(t => console.log(t.title));
|
|
1079
|
+
* ```
|
|
1080
|
+
*
|
|
1081
|
+
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/tracks/get_tracks
|
|
1082
|
+
*/
|
|
1083
|
+
async getTracks(ids, options) {
|
|
1084
|
+
const t = resolveToken(this.getToken, options?.token);
|
|
1085
|
+
return this.fetch({ path: `/tracks?ids=${ids.join(",")}`, method: "GET", token: t });
|
|
1086
|
+
}
|
|
904
1087
|
/**
|
|
905
1088
|
* Get stream URLs for a track.
|
|
906
1089
|
*
|
|
@@ -1456,6 +1639,90 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
1456
1639
|
SoundCloudClient2.Reposts = Reposts;
|
|
1457
1640
|
})(SoundCloudClient || (SoundCloudClient = {}));
|
|
1458
1641
|
|
|
1642
|
+
// src/client/dedupe.ts
|
|
1643
|
+
var InFlightDeduper = class {
|
|
1644
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
1645
|
+
/**
|
|
1646
|
+
* Return an existing in-flight promise for `key`, or start a new one via `factory`.
|
|
1647
|
+
* The entry is removed from the map once the promise settles (resolve or reject).
|
|
1648
|
+
*/
|
|
1649
|
+
add(key, factory) {
|
|
1650
|
+
const existing = this.inFlight.get(key);
|
|
1651
|
+
if (existing) return existing;
|
|
1652
|
+
const promise = factory().finally(() => {
|
|
1653
|
+
this.inFlight.delete(key);
|
|
1654
|
+
});
|
|
1655
|
+
this.inFlight.set(key, promise);
|
|
1656
|
+
return promise;
|
|
1657
|
+
}
|
|
1658
|
+
/** Number of currently in-flight requests */
|
|
1659
|
+
get size() {
|
|
1660
|
+
return this.inFlight.size;
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// src/client/registry.ts
|
|
1665
|
+
var IMPLEMENTED_OPERATIONS = [
|
|
1666
|
+
// Auth
|
|
1667
|
+
"post_oauth2_token",
|
|
1668
|
+
"delete_oauth2_token",
|
|
1669
|
+
// Me
|
|
1670
|
+
"get_me",
|
|
1671
|
+
"get_me_activities",
|
|
1672
|
+
"get_me_activities_own",
|
|
1673
|
+
"get_me_activities_tracks",
|
|
1674
|
+
"get_me_likes_tracks",
|
|
1675
|
+
"get_me_likes_playlists",
|
|
1676
|
+
"get_me_followings",
|
|
1677
|
+
"get_me_followings_tracks",
|
|
1678
|
+
"post_me_followings_user_id",
|
|
1679
|
+
"delete_me_followings_user_id",
|
|
1680
|
+
"get_me_followers",
|
|
1681
|
+
"get_me_playlists",
|
|
1682
|
+
"get_me_tracks",
|
|
1683
|
+
"get_me_connections",
|
|
1684
|
+
// Users
|
|
1685
|
+
"get_users_user_id",
|
|
1686
|
+
"get_users_user_id_followers",
|
|
1687
|
+
"get_users_user_id_followings",
|
|
1688
|
+
"get_users_user_id_tracks",
|
|
1689
|
+
"get_users_user_id_playlists",
|
|
1690
|
+
"get_users_user_id_likes_tracks",
|
|
1691
|
+
"get_users_user_id_likes_playlists",
|
|
1692
|
+
"get_users_user_id_web_profiles",
|
|
1693
|
+
// Tracks
|
|
1694
|
+
"get_tracks_track_id",
|
|
1695
|
+
"put_tracks_track_id",
|
|
1696
|
+
"delete_tracks_track_id",
|
|
1697
|
+
"get_tracks_track_id_comments",
|
|
1698
|
+
"post_tracks_track_id_comments",
|
|
1699
|
+
"get_tracks_track_id_likes",
|
|
1700
|
+
"get_tracks_track_id_reposts",
|
|
1701
|
+
"get_tracks_track_id_related",
|
|
1702
|
+
"get_tracks_track_id_streams",
|
|
1703
|
+
"post_likes_tracks_track_id",
|
|
1704
|
+
"delete_likes_tracks_track_id",
|
|
1705
|
+
"post_reposts_tracks_track_id",
|
|
1706
|
+
"delete_reposts_tracks_track_id",
|
|
1707
|
+
// Playlists
|
|
1708
|
+
"get_playlists_playlist_id",
|
|
1709
|
+
"post_playlists",
|
|
1710
|
+
"put_playlists_playlist_id",
|
|
1711
|
+
"delete_playlists_playlist_id",
|
|
1712
|
+
"get_playlists_playlist_id_tracks",
|
|
1713
|
+
"get_playlists_playlist_id_reposts",
|
|
1714
|
+
"post_likes_playlists_playlist_id",
|
|
1715
|
+
"delete_likes_playlists_playlist_id",
|
|
1716
|
+
"post_reposts_playlists_playlist_id",
|
|
1717
|
+
"delete_reposts_playlists_playlist_id",
|
|
1718
|
+
// Search
|
|
1719
|
+
"get_tracks",
|
|
1720
|
+
"get_users",
|
|
1721
|
+
"get_playlists",
|
|
1722
|
+
// Resolve
|
|
1723
|
+
"get_resolve"
|
|
1724
|
+
];
|
|
1725
|
+
|
|
1459
1726
|
// src/auth/getClientToken.ts
|
|
1460
1727
|
var getClientToken = (clientId, clientSecret) => {
|
|
1461
1728
|
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
@@ -1575,6 +1842,13 @@ var getUserWebProfiles = (token, userId) => scFetch({ path: `/users/${userId}/we
|
|
|
1575
1842
|
// src/tracks/getTrack.ts
|
|
1576
1843
|
var getTrack = (token, trackId) => scFetch({ path: `/tracks/${trackId}`, method: "GET", token });
|
|
1577
1844
|
|
|
1845
|
+
// src/tracks/getTracks.ts
|
|
1846
|
+
var getTracks = (token, ids) => scFetch({
|
|
1847
|
+
path: `/tracks?ids=${ids.join(",")}`,
|
|
1848
|
+
method: "GET",
|
|
1849
|
+
token
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1578
1852
|
// src/tracks/getComments.ts
|
|
1579
1853
|
var getTrackComments = (token, trackId, limit) => scFetch({ path: `/tracks/${trackId}/comments?threaded=1&filter_replies=0${limit ? `&limit=${limit}` : ""}&linked_partitioning=true`, method: "GET", token });
|
|
1580
1854
|
|
|
@@ -1693,6 +1967,9 @@ var getMePlaylists = (token, limit) => scFetch({ path: `/me/playlists?${limit ?
|
|
|
1693
1967
|
// src/me/tracks.ts
|
|
1694
1968
|
var getMeTracks = (token, limit) => scFetch({ path: `/me/tracks?${limit ? `limit=${limit}&` : ""}linked_partitioning=true`, method: "GET", token });
|
|
1695
1969
|
|
|
1970
|
+
// src/me/connections.ts
|
|
1971
|
+
var getMeConnections = (token) => scFetch({ path: "/me/connections", method: "GET", token });
|
|
1972
|
+
|
|
1696
1973
|
// src/likes/index.ts
|
|
1697
1974
|
var likePlaylist = async (token, playlistId) => {
|
|
1698
1975
|
try {
|
|
@@ -1748,6 +2025,6 @@ var unrepostPlaylist = async (token, playlistId) => {
|
|
|
1748
2025
|
// src/utils/widget.ts
|
|
1749
2026
|
var getSoundCloudWidgetUrl = (trackId) => `https%3A//api.soundcloud.com/tracks/${trackId}&show_teaser=false&color=%2300a99d&inverse=false&show_user=false&sharing=false&buying=false&liking=false&show_artwork=false&show_name=false`;
|
|
1750
2027
|
|
|
1751
|
-
export { SoundCloudClient, 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 };
|
|
1752
|
-
//# sourceMappingURL=chunk-
|
|
1753
|
-
//# sourceMappingURL=chunk-
|
|
2028
|
+
export { IMPLEMENTED_OPERATIONS, InFlightDeduper, RawClient, SoundCloudClient, createPlaylist, createTrackComment, deletePlaylist, deleteTrack, fetchAll, followUser, generateCodeChallenge, generateCodeVerifier, getAuthorizationUrl, getClientToken, getFollowers, getFollowings, getMe, getMeActivities, getMeActivitiesOwn, getMeActivitiesTracks, getMeConnections, getMeFollowers, getMeFollowings, getMeFollowingsTracks, getMeLikesPlaylists, getMeLikesTracks, getMePlaylists, getMeTracks, getPlaylist, getPlaylistReposts, getPlaylistTracks, getRelatedTracks, getSoundCloudWidgetUrl, getTrack, getTrackComments, getTrackLikes, getTrackReposts, getTrackStreams, getTracks, 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 };
|
|
2029
|
+
//# sourceMappingURL=chunk-JH7TLL2C.mjs.map
|
|
2030
|
+
//# sourceMappingURL=chunk-JH7TLL2C.mjs.map
|