soundcloud-api-ts 1.11.1 → 1.12.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.
@@ -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 result = response.json();
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 result;
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 result = response.json();
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 result;
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.
@@ -417,13 +559,17 @@ var SoundCloudClient = class _SoundCloudClient {
417
559
  * @see https://developers.soundcloud.com/docs/api/explorer/open-api#/oauth2/post_oauth2_token
418
560
  */
419
561
  async getClientToken() {
562
+ const basicAuth = Buffer.from(
563
+ `${this.config.clientId}:${this.config.clientSecret}`
564
+ ).toString("base64");
420
565
  return this.fetch({
421
566
  path: "/oauth/token",
422
567
  method: "POST",
568
+ headers: {
569
+ Authorization: `Basic ${basicAuth}`
570
+ },
423
571
  body: new URLSearchParams({
424
- grant_type: "client_credentials",
425
- client_id: this.config.clientId,
426
- client_secret: this.config.clientSecret
572
+ grant_type: "client_credentials"
427
573
  })
428
574
  });
429
575
  }
@@ -1452,6 +1598,89 @@ var SoundCloudClient = class _SoundCloudClient {
1452
1598
  SoundCloudClient2.Reposts = Reposts;
1453
1599
  })(SoundCloudClient || (SoundCloudClient = {}));
1454
1600
 
1601
+ // src/client/dedupe.ts
1602
+ var InFlightDeduper = class {
1603
+ inFlight = /* @__PURE__ */ new Map();
1604
+ /**
1605
+ * Return an existing in-flight promise for `key`, or start a new one via `factory`.
1606
+ * The entry is removed from the map once the promise settles (resolve or reject).
1607
+ */
1608
+ add(key, factory) {
1609
+ const existing = this.inFlight.get(key);
1610
+ if (existing) return existing;
1611
+ const promise = factory().finally(() => {
1612
+ this.inFlight.delete(key);
1613
+ });
1614
+ this.inFlight.set(key, promise);
1615
+ return promise;
1616
+ }
1617
+ /** Number of currently in-flight requests */
1618
+ get size() {
1619
+ return this.inFlight.size;
1620
+ }
1621
+ };
1622
+
1623
+ // src/client/registry.ts
1624
+ var IMPLEMENTED_OPERATIONS = [
1625
+ // Auth
1626
+ "post_oauth2_token",
1627
+ "delete_oauth2_token",
1628
+ // Me
1629
+ "get_me",
1630
+ "get_me_activities",
1631
+ "get_me_activities_own",
1632
+ "get_me_activities_tracks",
1633
+ "get_me_likes_tracks",
1634
+ "get_me_likes_playlists",
1635
+ "get_me_followings",
1636
+ "get_me_followings_tracks",
1637
+ "post_me_followings_user_id",
1638
+ "delete_me_followings_user_id",
1639
+ "get_me_followers",
1640
+ "get_me_playlists",
1641
+ "get_me_tracks",
1642
+ // Users
1643
+ "get_users_user_id",
1644
+ "get_users_user_id_followers",
1645
+ "get_users_user_id_followings",
1646
+ "get_users_user_id_tracks",
1647
+ "get_users_user_id_playlists",
1648
+ "get_users_user_id_likes_tracks",
1649
+ "get_users_user_id_likes_playlists",
1650
+ "get_users_user_id_web_profiles",
1651
+ // Tracks
1652
+ "get_tracks_track_id",
1653
+ "put_tracks_track_id",
1654
+ "delete_tracks_track_id",
1655
+ "get_tracks_track_id_comments",
1656
+ "post_tracks_track_id_comments",
1657
+ "get_tracks_track_id_likes",
1658
+ "get_tracks_track_id_reposts",
1659
+ "get_tracks_track_id_related",
1660
+ "get_tracks_track_id_streams",
1661
+ "post_likes_tracks_track_id",
1662
+ "delete_likes_tracks_track_id",
1663
+ "post_reposts_tracks_track_id",
1664
+ "delete_reposts_tracks_track_id",
1665
+ // Playlists
1666
+ "get_playlists_playlist_id",
1667
+ "post_playlists",
1668
+ "put_playlists_playlist_id",
1669
+ "delete_playlists_playlist_id",
1670
+ "get_playlists_playlist_id_tracks",
1671
+ "get_playlists_playlist_id_reposts",
1672
+ "post_likes_playlists_playlist_id",
1673
+ "delete_likes_playlists_playlist_id",
1674
+ "post_reposts_playlists_playlist_id",
1675
+ "delete_reposts_playlists_playlist_id",
1676
+ // Search
1677
+ "get_tracks",
1678
+ "get_users",
1679
+ "get_playlists",
1680
+ // Resolve
1681
+ "get_resolve"
1682
+ ];
1683
+
1455
1684
  // src/auth/getClientToken.ts
1456
1685
  var getClientToken = (clientId, clientSecret) => {
1457
1686
  const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
@@ -1744,6 +1973,6 @@ var unrepostPlaylist = async (token, playlistId) => {
1744
1973
  // src/utils/widget.ts
1745
1974
  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`;
1746
1975
 
1747
- 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 };
1748
- //# sourceMappingURL=chunk-DGZEPXIQ.mjs.map
1749
- //# sourceMappingURL=chunk-DGZEPXIQ.mjs.map
1976
+ export { IMPLEMENTED_OPERATIONS, InFlightDeduper, RawClient, 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 };
1977
+ //# sourceMappingURL=chunk-JLRQJWU5.mjs.map
1978
+ //# sourceMappingURL=chunk-JLRQJWU5.mjs.map