soundcloud-api-ts 1.13.3 → 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.
@@ -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 fetch(url, {
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 fetch(url, { method: "GET", headers, redirect: "manual" });
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
- retry: retryConfig,
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
- retry: retryConfig,
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
- return scFetch(opts, void 0, this.config.onRequest);
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 = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
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 res = await fetch("https://secure.soundcloud.com/sign-out", {
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,13 +1789,13 @@ var IMPLEMENTED_OPERATIONS = [
1731
1789
 
1732
1790
  // src/auth/getClientToken.ts
1733
1791
  var getClientToken = (clientId, clientSecret) => {
1792
+ const credentials = toBase64(`${clientId}:${clientSecret}`);
1734
1793
  return scFetch({
1735
1794
  path: "/oauth/token",
1736
1795
  method: "POST",
1796
+ headers: { Authorization: `Basic ${credentials}` },
1737
1797
  body: new URLSearchParams({
1738
- grant_type: "client_credentials",
1739
- client_id: clientId,
1740
- client_secret: clientSecret
1798
+ grant_type: "client_credentials"
1741
1799
  })
1742
1800
  });
1743
1801
  };
@@ -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-ZOHFLO4B.js.map
2104
- //# sourceMappingURL=chunk-ZOHFLO4B.js.map
2161
+ //# sourceMappingURL=chunk-YBNVXQ2E.js.map
2162
+ //# sourceMappingURL=chunk-YBNVXQ2E.js.map