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
|
@@ -5,6 +5,7 @@ var chunkMLVA534Z_js = require('./chunk-MLVA534Z.js');
|
|
|
5
5
|
// src/client/http.ts
|
|
6
6
|
var BASE_URL = "https://api.soundcloud.com";
|
|
7
7
|
var AUTH_BASE_URL = "https://secure.soundcloud.com";
|
|
8
|
+
var DEFAULT_CACHE_TTL_MS = 6e4;
|
|
8
9
|
var DEFAULT_RETRY = { maxRetries: 3, retryBaseDelay: 1e3 };
|
|
9
10
|
function delay(ms) {
|
|
10
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -31,6 +32,26 @@ async function parseErrorBody(response) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
async function scFetch(options, refreshCtx, onRequest) {
|
|
35
|
+
const deduper = refreshCtx?.deduper;
|
|
36
|
+
const cache = refreshCtx?.cache;
|
|
37
|
+
if (options.method !== "GET" || !deduper && !cache) {
|
|
38
|
+
return scFetchCore(options, refreshCtx, onRequest);
|
|
39
|
+
}
|
|
40
|
+
const key = `GET ${options.path} ${options.token ?? ""}`;
|
|
41
|
+
const run = async () => {
|
|
42
|
+
if (cache) {
|
|
43
|
+
const hit = await cache.get(key);
|
|
44
|
+
if (hit !== void 0) return hit;
|
|
45
|
+
}
|
|
46
|
+
const result = await scFetchCore(options, refreshCtx, onRequest);
|
|
47
|
+
if (cache && result !== void 0) {
|
|
48
|
+
await cache.set(key, result, { ttlMs: refreshCtx?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS });
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
return deduper ? deduper.add(key, run) : run();
|
|
53
|
+
}
|
|
54
|
+
async function scFetchCore(options, refreshCtx, onRequest) {
|
|
34
55
|
const retryConfig = refreshCtx?.retry ?? DEFAULT_RETRY;
|
|
35
56
|
const telemetryCallback = onRequest ?? refreshCtx?.onRequest;
|
|
36
57
|
const startTime = Date.now();
|
|
@@ -73,8 +94,9 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
73
94
|
headers["Content-Type"] = options.contentType;
|
|
74
95
|
}
|
|
75
96
|
let lastResponse;
|
|
97
|
+
const fetchFn = refreshCtx?.fetchImpl ?? fetch;
|
|
76
98
|
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
77
|
-
const response = await
|
|
99
|
+
const response = await fetchFn(url, {
|
|
78
100
|
method: options.method,
|
|
79
101
|
headers,
|
|
80
102
|
body: fetchBody,
|
|
@@ -153,7 +175,7 @@ async function scFetch(options, refreshCtx, onRequest) {
|
|
|
153
175
|
throw err;
|
|
154
176
|
}
|
|
155
177
|
}
|
|
156
|
-
async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
178
|
+
async function scFetchUrl(url, token, retryConfig, onRequest, fetchImpl) {
|
|
157
179
|
const config = retryConfig ?? DEFAULT_RETRY;
|
|
158
180
|
const headers = { Accept: "application/json" };
|
|
159
181
|
if (token) headers["Authorization"] = `OAuth ${token}`;
|
|
@@ -172,8 +194,9 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
172
194
|
});
|
|
173
195
|
};
|
|
174
196
|
let lastResponse;
|
|
197
|
+
const fetchFn = fetchImpl ?? fetch;
|
|
175
198
|
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
176
|
-
const response = await
|
|
199
|
+
const response = await fetchFn(url, { method: "GET", headers, redirect: "manual" });
|
|
177
200
|
finalStatus = response.status;
|
|
178
201
|
if (response.status === 302) {
|
|
179
202
|
const location = response.headers.get("location");
|
|
@@ -237,6 +260,14 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
|
|
|
237
260
|
throw err;
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
// src/utils/base64.ts
|
|
264
|
+
var toBase64 = (value) => {
|
|
265
|
+
if (typeof Buffer !== "undefined") {
|
|
266
|
+
return Buffer.from(value).toString("base64");
|
|
267
|
+
}
|
|
268
|
+
return btoa(value);
|
|
269
|
+
};
|
|
270
|
+
|
|
240
271
|
// src/client/paginate.ts
|
|
241
272
|
async function* paginate(firstPage, fetchNext) {
|
|
242
273
|
let page = await firstPage();
|
|
@@ -355,6 +386,28 @@ var RawClient = class {
|
|
|
355
386
|
}
|
|
356
387
|
};
|
|
357
388
|
|
|
389
|
+
// src/client/dedupe.ts
|
|
390
|
+
var InFlightDeduper = class {
|
|
391
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
392
|
+
/**
|
|
393
|
+
* Return an existing in-flight promise for `key`, or start a new one via `factory`.
|
|
394
|
+
* The entry is removed from the map once the promise settles (resolve or reject).
|
|
395
|
+
*/
|
|
396
|
+
add(key, factory) {
|
|
397
|
+
const existing = this.inFlight.get(key);
|
|
398
|
+
if (existing) return existing;
|
|
399
|
+
const promise = factory().finally(() => {
|
|
400
|
+
this.inFlight.delete(key);
|
|
401
|
+
});
|
|
402
|
+
this.inFlight.set(key, promise);
|
|
403
|
+
return promise;
|
|
404
|
+
}
|
|
405
|
+
/** Number of currently in-flight requests */
|
|
406
|
+
get size() {
|
|
407
|
+
return this.inFlight.size;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
358
411
|
// src/client/SoundCloudClient.ts
|
|
359
412
|
function resolveToken(tokenGetter, explicit) {
|
|
360
413
|
const t = explicit ?? tokenGetter();
|
|
@@ -399,6 +452,14 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
399
452
|
onDebug: config.onDebug,
|
|
400
453
|
onRetry: config.onRetry
|
|
401
454
|
};
|
|
455
|
+
const sharedCtx = {
|
|
456
|
+
retry: retryConfig,
|
|
457
|
+
onRequest: config.onRequest,
|
|
458
|
+
fetchImpl: config.fetch,
|
|
459
|
+
deduper: config.dedupe ?? true ? new InFlightDeduper() : void 0,
|
|
460
|
+
cache: config.cache,
|
|
461
|
+
cacheTtlMs: config.cacheTtlMs
|
|
462
|
+
};
|
|
402
463
|
const refreshCtx = config.onTokenRefresh ? {
|
|
403
464
|
getToken,
|
|
404
465
|
onTokenRefresh: async () => {
|
|
@@ -406,16 +467,14 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
406
467
|
return result;
|
|
407
468
|
},
|
|
408
469
|
setToken: (a, r) => this.setToken(a, r),
|
|
409
|
-
|
|
410
|
-
onRequest: config.onRequest
|
|
470
|
+
...sharedCtx
|
|
411
471
|
} : {
|
|
412
472
|
getToken,
|
|
413
473
|
setToken: (
|
|
414
474
|
/* v8 ignore next */
|
|
415
475
|
(a, r) => this.setToken(a, r)
|
|
416
476
|
),
|
|
417
|
-
|
|
418
|
-
onRequest: config.onRequest
|
|
477
|
+
...sharedCtx
|
|
419
478
|
};
|
|
420
479
|
this.auth = new _SoundCloudClient.Auth(this.config);
|
|
421
480
|
this.me = new _SoundCloudClient.Me(getToken, refreshCtx);
|
|
@@ -467,7 +526,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
467
526
|
paginate(firstPage) {
|
|
468
527
|
const token = this._accessToken;
|
|
469
528
|
const onReq = this.config.onRequest;
|
|
470
|
-
return paginate(firstPage, (url) => scFetchUrl(url, token, void 0, onReq));
|
|
529
|
+
return paginate(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch));
|
|
471
530
|
}
|
|
472
531
|
/**
|
|
473
532
|
* Async generator that yields individual items across all pages.
|
|
@@ -485,7 +544,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
485
544
|
paginateItems(firstPage) {
|
|
486
545
|
const token = this._accessToken;
|
|
487
546
|
const onReq = this.config.onRequest;
|
|
488
|
-
return paginateItems(firstPage, (url) => scFetchUrl(url, token, void 0, onReq));
|
|
547
|
+
return paginateItems(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch));
|
|
489
548
|
}
|
|
490
549
|
/**
|
|
491
550
|
* Collects all pages into a single flat array.
|
|
@@ -504,7 +563,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
504
563
|
fetchAll(firstPage, options) {
|
|
505
564
|
const token = this._accessToken;
|
|
506
565
|
const onReq = this.config.onRequest;
|
|
507
|
-
return fetchAll(firstPage, (url) => scFetchUrl(url, token, void 0, onReq), options);
|
|
566
|
+
return fetchAll(firstPage, (url) => scFetchUrl(url, token, void 0, onReq, this.config.fetch), options);
|
|
508
567
|
}
|
|
509
568
|
};
|
|
510
569
|
((SoundCloudClient2) => {
|
|
@@ -513,7 +572,25 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
513
572
|
this.config = config;
|
|
514
573
|
}
|
|
515
574
|
fetch(opts) {
|
|
516
|
-
|
|
575
|
+
const ctx = {
|
|
576
|
+
getToken: (
|
|
577
|
+
/* v8 ignore next */
|
|
578
|
+
() => void 0
|
|
579
|
+
),
|
|
580
|
+
setToken: (
|
|
581
|
+
/* v8 ignore next */
|
|
582
|
+
() => {
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
retry: {
|
|
586
|
+
maxRetries: this.config.maxRetries ?? 3,
|
|
587
|
+
retryBaseDelay: this.config.retryBaseDelay ?? 1e3,
|
|
588
|
+
onDebug: this.config.onDebug,
|
|
589
|
+
onRetry: this.config.onRetry
|
|
590
|
+
},
|
|
591
|
+
fetchImpl: this.config.fetch
|
|
592
|
+
};
|
|
593
|
+
return scFetch(opts, ctx, this.config.onRequest);
|
|
517
594
|
}
|
|
518
595
|
/**
|
|
519
596
|
* Build the authorization URL to redirect users to SoundCloud's OAuth login page.
|
|
@@ -561,7 +638,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
561
638
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
562
639
|
*/
|
|
563
640
|
async getClientToken() {
|
|
564
|
-
const credentials =
|
|
641
|
+
const credentials = toBase64(`${this.config.clientId}:${this.config.clientSecret}`);
|
|
565
642
|
return this.fetch({
|
|
566
643
|
path: "/oauth/token",
|
|
567
644
|
method: "POST",
|
|
@@ -588,6 +665,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
588
665
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
589
666
|
*/
|
|
590
667
|
async getUserToken(code, codeVerifier) {
|
|
668
|
+
if (!this.config.redirectUri) throw new Error("redirectUri is required for getUserToken");
|
|
591
669
|
const params = {
|
|
592
670
|
grant_type: "authorization_code",
|
|
593
671
|
client_id: this.config.clientId,
|
|
@@ -618,6 +696,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
618
696
|
* @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
|
|
619
697
|
*/
|
|
620
698
|
async refreshUserToken(refreshToken) {
|
|
699
|
+
if (!this.config.redirectUri) throw new Error("redirectUri is required for refreshUserToken");
|
|
621
700
|
return this.fetch({
|
|
622
701
|
path: "/oauth/token",
|
|
623
702
|
method: "POST",
|
|
@@ -646,7 +725,8 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
646
725
|
* ```
|
|
647
726
|
*/
|
|
648
727
|
async signOut(accessToken) {
|
|
649
|
-
const
|
|
728
|
+
const fetchFn = this.config.fetch ?? fetch;
|
|
729
|
+
const res = await fetchFn("https://secure.soundcloud.com/sign-out", {
|
|
650
730
|
method: "POST",
|
|
651
731
|
headers: { "Content-Type": "application/json" },
|
|
652
732
|
body: JSON.stringify({ access_token: accessToken })
|
|
@@ -1645,28 +1725,6 @@ exports.SoundCloudClient = class _SoundCloudClient {
|
|
|
1645
1725
|
SoundCloudClient2.Reposts = Reposts;
|
|
1646
1726
|
})(exports.SoundCloudClient || (exports.SoundCloudClient = {}));
|
|
1647
1727
|
|
|
1648
|
-
// src/client/dedupe.ts
|
|
1649
|
-
var InFlightDeduper = class {
|
|
1650
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1651
|
-
/**
|
|
1652
|
-
* Return an existing in-flight promise for `key`, or start a new one via `factory`.
|
|
1653
|
-
* The entry is removed from the map once the promise settles (resolve or reject).
|
|
1654
|
-
*/
|
|
1655
|
-
add(key, factory) {
|
|
1656
|
-
const existing = this.inFlight.get(key);
|
|
1657
|
-
if (existing) return existing;
|
|
1658
|
-
const promise = factory().finally(() => {
|
|
1659
|
-
this.inFlight.delete(key);
|
|
1660
|
-
});
|
|
1661
|
-
this.inFlight.set(key, promise);
|
|
1662
|
-
return promise;
|
|
1663
|
-
}
|
|
1664
|
-
/** Number of currently in-flight requests */
|
|
1665
|
-
get size() {
|
|
1666
|
-
return this.inFlight.size;
|
|
1667
|
-
}
|
|
1668
|
-
};
|
|
1669
|
-
|
|
1670
1728
|
// src/client/registry.ts
|
|
1671
1729
|
var IMPLEMENTED_OPERATIONS = [
|
|
1672
1730
|
// Auth
|
|
@@ -1731,7 +1789,7 @@ var IMPLEMENTED_OPERATIONS = [
|
|
|
1731
1789
|
|
|
1732
1790
|
// src/auth/getClientToken.ts
|
|
1733
1791
|
var getClientToken = (clientId, clientSecret) => {
|
|
1734
|
-
const credentials =
|
|
1792
|
+
const credentials = toBase64(`${clientId}:${clientSecret}`);
|
|
1735
1793
|
return scFetch({
|
|
1736
1794
|
path: "/oauth/token",
|
|
1737
1795
|
method: "POST",
|
|
@@ -2100,5 +2158,5 @@ exports.unrepostPlaylist = unrepostPlaylist;
|
|
|
2100
2158
|
exports.unrepostTrack = unrepostTrack;
|
|
2101
2159
|
exports.updatePlaylist = updatePlaylist;
|
|
2102
2160
|
exports.updateTrack = updateTrack;
|
|
2103
|
-
//# sourceMappingURL=chunk-
|
|
2104
|
-
//# sourceMappingURL=chunk-
|
|
2161
|
+
//# sourceMappingURL=chunk-YBNVXQ2E.js.map
|
|
2162
|
+
//# sourceMappingURL=chunk-YBNVXQ2E.js.map
|