soundcloud-api-ts 1.13.4 → 1.14.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 +8 -8
- package/README.md +15 -10
- package/dist/{chunk-B3OPPWJN.mjs → chunk-4FNI5QIN.mjs} +96 -38
- package/dist/chunk-4FNI5QIN.mjs.map +1 -0
- package/dist/{chunk-MZ7OS7LN.js → chunk-YBNVXQ2E.js} +96 -38
- package/dist/chunk-YBNVXQ2E.js.map +1 -0
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +90 -82
- package/dist/index.d.ts +90 -82
- package/dist/index.js +68 -68
- package/dist/index.mjs +1 -1
- package/llms.txt +7 -7
- package/package.json +7 -1
- package/dist/chunk-B3OPPWJN.mjs.map +0 -1
- package/dist/chunk-MZ7OS7LN.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -125,20 +125,20 @@ try {
|
|
|
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
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
|
-
5. **Request telemetry** — pass `onRequest` in the config to receive `SCRequestTelemetry` after every request (method, path, duration, status, retries, error)
|
|
128
|
+
5. **Request telemetry** — pass `onRequest` in the config to receive `SCRequestTelemetry` after every client-namespace request (method, path, duration, status, retries, error), including pagination and retries. NOT emitted for `sc.raw.*` or `auth.signOut`.
|
|
129
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`
|
|
131
|
-
8. **Deduplication** — concurrent identical GETs share a single in-flight promise (`dedupe: true` by default
|
|
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.
|
|
130
|
+
7. **Fetch injection** — pass `fetch` in the constructor for Bun/Deno/Cloudflare Workers portability. No Node-only APIs used at runtime. There is no `AbortController` option — bake cancellation/timeouts into the `fetch` you inject.
|
|
131
|
+
8. **Deduplication** — concurrent identical GETs through the client namespaces share a single in-flight promise (`dedupe: true` by default; wired up in v1.14.0 — earlier versions accepted but ignored the option). `sc.raw.*` and pagination `next_href` fetches are not deduped.
|
|
132
|
+
9. **Cache** — pass a `SoundCloudCache` implementation in the constructor to cache namespace GET responses (also wired up in v1.14.0). Base package defines the interface only; bring your own backend. `cacheTtlMs` defaults to 60000ms.
|
|
133
133
|
10. **Retry hook** — pass `onRetry` to receive `RetryInfo` on each retry: `{ attempt, delayMs, reason, status?, url }`.
|
|
134
|
-
11. **`sc.tracks.getTracks(ids[])`** — batch fetch multiple tracks by ID array in a single request. Returns `SoundCloudTrack[]` (may be shorter than input if some tracks are unavailable).
|
|
134
|
+
11. **`sc.tracks.getTracks(ids[])`** — batch fetch multiple tracks by ID array in a single request. Max 200 IDs — throws immediately above that, before any network call. Returns `SoundCloudTrack[]` (may be shorter than input if some tracks are unavailable).
|
|
135
135
|
12. **`sc.me.getConnections()`** — list linked social accounts. Requires user token. May require elevated API access.
|
|
136
136
|
13. **TokenProvider / TokenStore interfaces** — in `src/auth/token-provider.ts`. Implement to integrate with NextAuth, Clerk, Redis, or any session framework. See `docs/auth-guide.md`.
|
|
137
137
|
14. **Auth guide** — `docs/auth-guide.md` covers: client creds vs user tokens, full PKCE flow, auto-refresh, NextAuth/Clerk bridge patterns, 401 troubleshooting table (invalid_client / insufficient_scope / invalid_token / unauthorized_client).
|
|
138
138
|
15. **OpenAPI tooling** — `pnpm openapi:sync` fetches the spec (if available), `pnpm openapi:coverage` reports implemented vs total. `src/client/registry.ts` is the source of truth — update it when adding new endpoints.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
16. **No env vars** — the package reads no environment variables. Pass `clientId`, `clientSecret`, and `redirectUri` directly to the constructor.
|
|
140
|
+
17. **IDs can be numbers or strings** — all ID parameters accept `string | number`.
|
|
141
|
+
18. **Search pagination** — search uses zero-based `pageNumber` (10 results per page), not cursor-based pagination.
|
|
142
142
|
|
|
143
143
|
## Project Structure (for contributors)
|
|
144
144
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://bundlephobia.com/package/soundcloud-api-ts)
|
|
8
8
|
[](https://packagephobia.com/result?p=soundcloud-api-ts)
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
|
-
[](https://nodejs.org/)
|
|
11
11
|
[]()
|
|
12
12
|
[](https://twin-paws.github.io/soundcloud-api-ts/)
|
|
13
13
|
[](https://github.com/twin-paws/soundcloud-api-ts)
|
|
@@ -25,11 +25,11 @@ It is built on SoundCloud's **official documented API** with registered app cred
|
|
|
25
25
|
- **TypeScript-first** — full types ship in the package. No `@types/*` installs, no casting to `any`.
|
|
26
26
|
- **Zero dependencies** — native `fetch`, nothing in `node_modules` at runtime. 4.5 KB min+gz.
|
|
27
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`
|
|
28
|
+
- **Runtime portable** — inject your own `fetch` for Cloudflare Workers, Bun, Deno, and Edge runtimes.
|
|
29
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
30
|
- **Full auth support** — client credentials flow for server-to-server, authorization code + PKCE for user-context operations, auto token refresh on 401.
|
|
31
31
|
- **Pagination built-in** — async iterators and `fetchAll` helpers across all paginated endpoints.
|
|
32
|
-
- **Interactive CLI** — `sc-cli
|
|
32
|
+
- **Interactive CLI** — `sc-cli track <id>`, `sc-cli search <query>`, `sc-cli me` from your terminal.
|
|
33
33
|
- **LLM-friendly** — ships `llms.txt`, `llms-full.txt`, and `AGENTS.md` for AI coding agents.
|
|
34
34
|
|
|
35
35
|
## Comparison
|
|
@@ -131,9 +131,11 @@ sc.setToken(token.access_token);
|
|
|
131
131
|
|
|
132
132
|
// Now all calls use the stored token automatically
|
|
133
133
|
const results = await sc.search.tracks("electronic");
|
|
134
|
-
const me = await sc.me.getMe();
|
|
135
134
|
const track = await sc.tracks.getTrack(123456);
|
|
136
135
|
const streams = await sc.tracks.getStreams(123456);
|
|
136
|
+
|
|
137
|
+
// /me endpoints need a user token (authorization code flow), not a client token:
|
|
138
|
+
// const me = await sc.me.getMe();
|
|
137
139
|
```
|
|
138
140
|
|
|
139
141
|
## OAuth 2.1 Flow
|
|
@@ -240,7 +242,7 @@ sc.users.getWebProfiles(userId, options?)
|
|
|
240
242
|
|
|
241
243
|
// Tracks
|
|
242
244
|
sc.tracks.getTrack(trackId, options?)
|
|
243
|
-
sc.tracks.getTracks(ids[], options?) // batch fetch by IDs
|
|
245
|
+
sc.tracks.getTracks(ids[], options?) // batch fetch by IDs (max 200, throws above)
|
|
244
246
|
sc.tracks.getStreams(trackId, options?)
|
|
245
247
|
sc.tracks.getComments(trackId, limit?, options?)
|
|
246
248
|
sc.tracks.createComment(trackId, body, timestamp?, options?)
|
|
@@ -277,12 +279,12 @@ sc.search.playlists(query, pageNumber?, options?)
|
|
|
277
279
|
|
|
278
280
|
// Resolve
|
|
279
281
|
sc.resolve.resolveUrl(url, options?)
|
|
280
|
-
```
|
|
281
282
|
|
|
282
283
|
// Raw escape hatch — call any endpoint
|
|
283
284
|
sc.raw.get('/tracks/{id}', { id: 123456 })
|
|
284
285
|
sc.raw.post('/tracks/{id}/comments', { body: { body: 'great track' } })
|
|
285
286
|
sc.raw.request({ method: 'GET', path: '/me', query: {} })
|
|
287
|
+
```
|
|
286
288
|
|
|
287
289
|
Where `options` is `{ token?: string }` — only needed to override the stored token.
|
|
288
290
|
|
|
@@ -461,6 +463,10 @@ const sc = new SoundCloudClient({
|
|
|
461
463
|
});
|
|
462
464
|
```
|
|
463
465
|
|
|
466
|
+
Dedupe and cache apply to GETs made through the client namespaces (`sc.tracks.*`, `sc.users.*`, …). `sc.raw.*` and pagination `next_href` continuation fetches are not deduped or cached.
|
|
467
|
+
|
|
468
|
+
> **Note:** the `dedupe`, `cache`, and `cacheTtlMs` options were accepted but not actually wired up in v1.12.0–v1.13.4 — they take effect from v1.14.0.
|
|
469
|
+
|
|
464
470
|
### Pluggable Cache
|
|
465
471
|
|
|
466
472
|
Bring your own cache backend — in-memory, Redis, Cloudflare KV, whatever. The base package defines the interface only (no implementation, no deps):
|
|
@@ -490,12 +496,11 @@ Pass a custom `fetch` implementation to work in any runtime — Cloudflare Worke
|
|
|
490
496
|
```ts
|
|
491
497
|
const sc = new SoundCloudClient({
|
|
492
498
|
clientId, clientSecret,
|
|
493
|
-
fetch: myCustomFetch,
|
|
494
|
-
AbortController: myAbortCtrl, // optional: custom AbortController
|
|
499
|
+
fetch: myCustomFetch, // optional: custom fetch (defaults to globalThis.fetch)
|
|
495
500
|
});
|
|
496
501
|
```
|
|
497
502
|
|
|
498
|
-
No Node-only APIs are used at runtime. The client works anywhere `fetch` is available.
|
|
503
|
+
No Node-only APIs are used at runtime. The client works anywhere `fetch` is available. There is no `AbortController` option — if you need cancellation or timeouts, wrap them into the `fetch` you inject.
|
|
499
504
|
|
|
500
505
|
---
|
|
501
506
|
|
|
@@ -551,7 +556,7 @@ The `SCRequestTelemetry` object includes:
|
|
|
551
556
|
| `retryCount` | `number` | Number of retries (0 = first attempt succeeded) |
|
|
552
557
|
| `error` | `string?` | Error message if the request failed |
|
|
553
558
|
|
|
554
|
-
Telemetry fires
|
|
559
|
+
Telemetry fires for all client-namespace requests: direct calls, pagination, retries, and 401 token refresh. It is **not** emitted for `sc.raw.*` or `auth.signOut`. Fully optional — zero overhead when `onRequest` is not set.
|
|
555
560
|
|
|
556
561
|
## API Terms Compliance
|
|
557
562
|
|
|
@@ -3,6 +3,7 @@ import { SoundCloudError } from './chunk-QYYEWUIJ.mjs';
|
|
|
3
3
|
// src/client/http.ts
|
|
4
4
|
var BASE_URL = "https://api.soundcloud.com";
|
|
5
5
|
var AUTH_BASE_URL = "https://secure.soundcloud.com";
|
|
6
|
+
var DEFAULT_CACHE_TTL_MS = 6e4;
|
|
6
7
|
var DEFAULT_RETRY = { maxRetries: 3, retryBaseDelay: 1e3 };
|
|
7
8
|
function delay(ms) {
|
|
8
9
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -29,6 +30,26 @@ async function parseErrorBody(response) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
async function scFetch(options, refreshCtx, onRequest) {
|
|
33
|
+
const deduper = refreshCtx?.deduper;
|
|
34
|
+
const cache = refreshCtx?.cache;
|
|
35
|
+
if (options.method !== "GET" || !deduper && !cache) {
|
|
36
|
+
return scFetchCore(options, refreshCtx, onRequest);
|
|
37
|
+
}
|
|
38
|
+
const key = `GET ${options.path} ${options.token ?? ""}`;
|
|
39
|
+
const run = async () => {
|
|
40
|
+
if (cache) {
|
|
41
|
+
const hit = await cache.get(key);
|
|
42
|
+
if (hit !== void 0) return hit;
|
|
43
|
+
}
|
|
44
|
+
const result = await scFetchCore(options, refreshCtx, onRequest);
|
|
45
|
+
if (cache && result !== void 0) {
|
|
46
|
+
await cache.set(key, result, { ttlMs: refreshCtx?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS });
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
return deduper ? deduper.add(key, run) : run();
|
|
51
|
+
}
|
|
52
|
+
async function scFetchCore(options, refreshCtx, onRequest) {
|
|
32
53
|
const retryConfig = refreshCtx?.retry ?? DEFAULT_RETRY;
|
|
33
54
|
const telemetryCallback = onRequest ?? refreshCtx?.onRequest;
|
|
34
55
|
const startTime = Date.now();
|
|
@@ -71,8 +92,9 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
71
92
|
headers["Content-Type"] = options.contentType;
|
|
72
93
|
}
|
|
73
94
|
let lastResponse;
|
|
95
|
+
const fetchFn = refreshCtx?.fetchImpl ?? fetch;
|
|
74
96
|
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
75
|
-
const response = await
|
|
97
|
+
const response = await fetchFn(url, {
|
|
76
98
|
method: options.method,
|
|
77
99
|
headers,
|
|
78
100
|
body: fetchBody,
|
|
@@ -151,7 +173,7 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
151
173
|
throw err;
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
|
-
async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
176
|
+
async function scFetchUrl(url, token, retryConfig, onRequest, fetchImpl) {
|
|
155
177
|
const config = retryConfig ?? DEFAULT_RETRY;
|
|
156
178
|
const headers = { Accept: "application/json" };
|
|
157
179
|
if (token) headers["Authorization"] = `OAuth ${token}`;
|
|
@@ -170,8 +192,9 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
170
192
|
});
|
|
171
193
|
};
|
|
172
194
|
let lastResponse;
|
|
195
|
+
const fetchFn = fetchImpl ?? fetch;
|
|
173
196
|
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
174
|
-
const response = await
|
|
197
|
+
const response = await fetchFn(url, { method: "GET", headers, redirect: "manual" });
|
|
175
198
|
finalStatus = response.status;
|
|
176
199
|
if (response.status === 302) {
|
|
177
200
|
const location = response.headers.get("location");
|
|
@@ -235,6 +258,14 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
235
258
|
throw err;
|
|
236
259
|
}
|
|
237
260
|
|
|
261
|
+
// src/utils/base64.ts
|
|
262
|
+
var toBase64 = (value) => {
|
|
263
|
+
if (typeof Buffer !== "undefined") {
|
|
264
|
+
return Buffer.from(value).toString("base64");
|
|
265
|
+
}
|
|
266
|
+
return btoa(value);
|
|
267
|
+
};
|
|
268
|
+
|
|
238
269
|
// src/client/paginate.ts
|
|
239
270
|
async function* paginate(firstPage, fetchNext) {
|
|
240
271
|
let page = await firstPage();
|
|
@@ -353,6 +384,28 @@ var RawClient = class {
|
|
|
353
384
|
}
|
|
354
385
|
};
|
|
355
386
|
|
|
387
|
+
// src/client/dedupe.ts
|
|
388
|
+
var InFlightDeduper = class {
|
|
389
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
390
|
+
/**
|
|
391
|
+
* Return an existing in-flight promise for `key`, or start a new one via `factory`.
|
|
392
|
+
* The entry is removed from the map once the promise settles (resolve or reject).
|
|
393
|
+
*/
|
|
394
|
+
add(key, factory) {
|
|
395
|
+
const existing = this.inFlight.get(key);
|
|
396
|
+
if (existing) return existing;
|
|
397
|
+
const promise = factory().finally(() => {
|
|
398
|
+
this.inFlight.delete(key);
|
|
399
|
+
});
|
|
400
|
+
this.inFlight.set(key, promise);
|
|
401
|
+
return promise;
|
|
402
|
+
}
|
|
403
|
+
/** Number of currently in-flight requests */
|
|
404
|
+
get size() {
|
|
405
|
+
return this.inFlight.size;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
356
409
|
// src/client/SoundCloudClient.ts
|
|
357
410
|
function resolveToken(tokenGetter, explicit) {
|
|
358
411
|
const t = explicit ?? tokenGetter();
|
|
@@ -397,6 +450,14 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
397
450
|
onDebug: config.onDebug,
|
|
398
451
|
onRetry: config.onRetry
|
|
399
452
|
};
|
|
453
|
+
const sharedCtx = {
|
|
454
|
+
retry: retryConfig,
|
|
455
|
+
onRequest: config.onRequest,
|
|
456
|
+
fetchImpl: config.fetch,
|
|
457
|
+
deduper: config.dedupe ?? true ? new InFlightDeduper() : void 0,
|
|
458
|
+
cache: config.cache,
|
|
459
|
+
cacheTtlMs: config.cacheTtlMs
|
|
460
|
+
};
|
|
400
461
|
const refreshCtx = config.onTokenRefresh ? {
|
|
401
462
|
getToken,
|
|
402
463
|
onTokenRefresh: async () => {
|
|
@@ -404,16 +465,14 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
404
465
|
return result;
|
|
405
466
|
},
|
|
406
467
|
setToken: (a, r) => this.setToken(a, r),
|
|
407
|
-
|
|
408
|
-
onRequest: config.onRequest
|
|
468
|
+
...sharedCtx
|
|
409
469
|
} : {
|
|
410
470
|
getToken,
|
|
411
471
|
setToken: (
|
|
412
472
|
/* v8 ignore next */
|
|
413
473
|
(a, r) => this.setToken(a, r)
|
|
414
474
|
),
|
|
415
|
-
|
|
416
|
-
onRequest: config.onRequest
|
|
475
|
+
...sharedCtx
|
|
417
476
|
};
|
|
418
477
|
this.auth = new _SoundCloudClient.Auth(this.config);
|
|
419
478
|
this.me = new _SoundCloudClient.Me(getToken, refreshCtx);
|
|
@@ -465,7 +524,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
465
524
|
paginate(firstPage) {
|
|
466
525
|
const token = this._accessToken;
|
|
467
526
|
const onReq = this.config.onRequest;
|
|
468
|
-
return paginate(firstPage, (url) => scFetchUrl(url, token, void 0, onReq));
|
|
527
|
+
return paginate(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch));
|
|
469
528
|
}
|
|
470
529
|
/**
|
|
471
530
|
* Async generator that yields individual items across all pages.
|
|
@@ -483,7 +542,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
483
542
|
paginateItems(firstPage) {
|
|
484
543
|
const token = this._accessToken;
|
|
485
544
|
const onReq = this.config.onRequest;
|
|
486
|
-
return paginateItems(firstPage, (url) => scFetchUrl(url, token, void 0, onReq));
|
|
545
|
+
return paginateItems(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch));
|
|
487
546
|
}
|
|
488
547
|
/**
|
|
489
548
|
* Collects all pages into a single flat array.
|
|
@@ -502,7 +561,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
502
561
|
fetchAll(firstPage, options) {
|
|
503
562
|
const token = this._accessToken;
|
|
504
563
|
const onReq = this.config.onRequest;
|
|
505
|
-
return fetchAll(firstPage, (url) => scFetchUrl(url, token, void 0, onReq), options);
|
|
564
|
+
return fetchAll(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch), options);
|
|
506
565
|
}
|
|
507
566
|
};
|
|
508
567
|
((SoundCloudClient2) => {
|
|
@@ -511,7 +570,25 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
511
570
|
this.config = config;
|
|
512
571
|
}
|
|
513
572
|
fetch(opts) {
|
|
514
|
-
|
|
573
|
+
const ctx = {
|
|
574
|
+
getToken: (
|
|
575
|
+
/* v8 ignore next */
|
|
576
|
+
() => void 0
|
|
577
|
+
),
|
|
578
|
+
setToken: (
|
|
579
|
+
/* v8 ignore next */
|
|
580
|
+
() => {
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
retry: {
|
|
584
|
+
maxRetries: this.config.maxRetries ?? 3,
|
|
585
|
+
retryBaseDelay: this.config.retryBaseDelay ?? 1e3,
|
|
586
|
+
onDebug: this.config.onDebug,
|
|
587
|
+
onRetry: this.config.onRetry
|
|
588
|
+
},
|
|
589
|
+
fetchImpl: this.config.fetch
|
|
590
|
+
};
|
|
591
|
+
return scFetch(opts, ctx, this.config.onRequest);
|
|
515
592
|
}
|
|
516
593
|
/**
|
|
517
594
|
* Build the authorization URL to redirect users to SoundCloud's OAuth login page.
|
|
@@ -559,7 +636,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
559
636
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
560
637
|
*/
|
|
561
638
|
async getClientToken() {
|
|
562
|
-
const credentials =
|
|
639
|
+
const credentials = toBase64(`${this.config.clientId}:${this.config.clientSecret}`);
|
|
563
640
|
return this.fetch({
|
|
564
641
|
path: "/oauth/token",
|
|
565
642
|
method: "POST",
|
|
@@ -586,6 +663,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
586
663
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
587
664
|
*/
|
|
588
665
|
async getUserToken(code, codeVerifier) {
|
|
666
|
+
if (!this.config.redirectUri) throw new Error("redirectUri is required for getUserToken");
|
|
589
667
|
const params = {
|
|
590
668
|
grant_type: "authorization_code",
|
|
591
669
|
client_id: this.config.clientId,
|
|
@@ -616,6 +694,7 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
616
694
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
617
695
|
*/
|
|
618
696
|
async refreshUserToken(refreshToken) {
|
|
697
|
+
if (!this.config.redirectUri) throw new Error("redirectUri is required for refreshUserToken");
|
|
619
698
|
return this.fetch({
|
|
620
699
|
path: "/oauth/token",
|
|
621
700
|
method: "POST",
|
|
@@ -644,7 +723,8 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
644
723
|
* ```
|
|
645
724
|
*/
|
|
646
725
|
async signOut(accessToken) {
|
|
647
|
-
const
|
|
726
|
+
const fetchFn = this.config.fetch ?? fetch;
|
|
727
|
+
const res = await fetchFn("https://secure.soundcloud.com/sign-out", {
|
|
648
728
|
method: "POST",
|
|
649
729
|
headers: { "Content-Type": "application/json" },
|
|
650
730
|
body: JSON.stringify({ access_token: accessToken })
|
|
@@ -1643,28 +1723,6 @@ var SoundCloudClient = class _SoundCloudClient {
|
|
|
1643
1723
|
SoundCloudClient2.Reposts = Reposts;
|
|
1644
1724
|
})(SoundCloudClient || (SoundCloudClient = {}));
|
|
1645
1725
|
|
|
1646
|
-
// src/client/dedupe.ts
|
|
1647
|
-
var InFlightDeduper = class {
|
|
1648
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1649
|
-
/**
|
|
1650
|
-
* Return an existing in-flight promise for `key`, or start a new one via `factory`.
|
|
1651
|
-
* The entry is removed from the map once the promise settles (resolve or reject).
|
|
1652
|
-
*/
|
|
1653
|
-
add(key, factory) {
|
|
1654
|
-
const existing = this.inFlight.get(key);
|
|
1655
|
-
if (existing) return existing;
|
|
1656
|
-
const promise = factory().finally(() => {
|
|
1657
|
-
this.inFlight.delete(key);
|
|
1658
|
-
});
|
|
1659
|
-
this.inFlight.set(key, promise);
|
|
1660
|
-
return promise;
|
|
1661
|
-
}
|
|
1662
|
-
/** Number of currently in-flight requests */
|
|
1663
|
-
get size() {
|
|
1664
|
-
return this.inFlight.size;
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
1726
|
// src/client/registry.ts
|
|
1669
1727
|
var IMPLEMENTED_OPERATIONS = [
|
|
1670
1728
|
// Auth
|
|
@@ -1729,7 +1787,7 @@ var IMPLEMENTED_OPERATIONS = [
|
|
|
1729
1787
|
|
|
1730
1788
|
// src/auth/getClientToken.ts
|
|
1731
1789
|
var getClientToken = (clientId, clientSecret) => {
|
|
1732
|
-
const credentials =
|
|
1790
|
+
const credentials = toBase64(`${clientId}:${clientSecret}`);
|
|
1733
1791
|
return scFetch({
|
|
1734
1792
|
path: "/oauth/token",
|
|
1735
1793
|
method: "POST",
|
|
@@ -2033,5 +2091,5 @@ var unrepostPlaylist = async (token, playlistId) => {
|
|
|
2033
2091
|
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`;
|
|
2034
2092
|
|
|
2035
2093
|
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 };
|
|
2036
|
-
//# sourceMappingURL=chunk-
|
|
2037
|
-
//# sourceMappingURL=chunk-
|
|
2094
|
+
//# sourceMappingURL=chunk-4FNI5QIN.mjs.map
|
|
2095
|
+
//# sourceMappingURL=chunk-4FNI5QIN.mjs.map
|