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.
@@ -17,7 +17,7 @@ function getRetryDelay(response, attempt, config) {
17
17
  const retryAfter = response.headers.get("retry-after");
18
18
  if (retryAfter) {
19
19
  const seconds = Number(retryAfter);
20
- if (!Number.isNaN(seconds)) return seconds * 1e3;
20
+ if (!Number.isNaN(seconds)) return Math.min(seconds * 1e3, 6e4);
21
21
  }
22
22
  }
23
23
  const base = config.retryBaseDelay * Math.pow(2, attempt);
@@ -93,9 +93,26 @@ async function scFetch(options, refreshCtx, onRequest) {
93
93
  return void 0;
94
94
  }
95
95
  if (response.ok) {
96
- const result = response.json();
96
+ const data = await response.json();
97
+ if (typeof data === "object" && data !== null) {
98
+ const metaHeaders = {};
99
+ if (typeof response.headers.forEach === "function") {
100
+ response.headers.forEach((value, key) => {
101
+ metaHeaders[key] = value;
102
+ });
103
+ }
104
+ try {
105
+ Object.defineProperty(data, "_meta", {
106
+ value: { status: response.status, headers: metaHeaders },
107
+ enumerable: false,
108
+ configurable: true,
109
+ writable: true
110
+ });
111
+ } catch {
112
+ }
113
+ }
97
114
  emitTelemetry();
98
- return result;
115
+ return data;
99
116
  }
100
117
  if (!isRetryable(response.status)) {
101
118
  const body2 = await parseErrorBody(response);
@@ -110,6 +127,13 @@ async function scFetch(options, refreshCtx, onRequest) {
110
127
  retryConfig.onDebug?.(
111
128
  `Retry ${attempt + 1}/${retryConfig.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
112
129
  );
130
+ retryConfig.onRetry?.({
131
+ attempt: retryCount,
132
+ delayMs,
133
+ reason: `${response.status} ${response.statusText}`,
134
+ status: response.status,
135
+ url
136
+ });
113
137
  await delay(delayMs);
114
138
  }
115
139
  }
@@ -163,9 +187,26 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
163
187
  return void 0;
164
188
  }
165
189
  if (response.ok) {
166
- const result = response.json();
190
+ const data = await response.json();
191
+ if (typeof data === "object" && data !== null) {
192
+ const metaHeaders = {};
193
+ if (typeof response.headers.forEach === "function") {
194
+ response.headers.forEach((value, key) => {
195
+ metaHeaders[key] = value;
196
+ });
197
+ }
198
+ try {
199
+ Object.defineProperty(data, "_meta", {
200
+ value: { status: response.status, headers: metaHeaders },
201
+ enumerable: false,
202
+ configurable: true,
203
+ writable: true
204
+ });
205
+ } catch {
206
+ }
207
+ }
167
208
  emitTelemetry();
168
- return result;
209
+ return data;
169
210
  }
170
211
  if (!isRetryable(response.status)) {
171
212
  const body2 = await parseErrorBody(response);
@@ -180,6 +221,13 @@ async function scFetchUrl(url, token, retryConfig, onRequest) {
180
221
  config.onDebug?.(
181
222
  `Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
182
223
  );
224
+ config.onRetry?.({
225
+ attempt: retryCount,
226
+ delayMs,
227
+ reason: `${response.status} ${response.statusText}`,
228
+ status: response.status,
229
+ url
230
+ });
183
231
  await delay(delayMs);
184
232
  }
185
233
  }
@@ -217,6 +265,96 @@ async function fetchAll(firstPage, fetchNext, options) {
217
265
  return result;
218
266
  }
219
267
 
268
+ // src/client/raw.ts
269
+ var RawClient = class {
270
+ constructor(baseUrl, getToken, fetchFn) {
271
+ this.baseUrl = baseUrl;
272
+ this.getToken = getToken;
273
+ this.fetchFn = fetchFn;
274
+ }
275
+ /**
276
+ * Make a raw HTTP request. Path template placeholders like `{id}` are substituted
277
+ * from matching keys in `query` before the remaining query params are appended to
278
+ * the URL as search parameters.
279
+ */
280
+ async request({
281
+ method,
282
+ path,
283
+ query,
284
+ body,
285
+ token
286
+ }) {
287
+ let resolvedPath = path;
288
+ const remainingQuery = {};
289
+ if (query) {
290
+ for (const [key, value] of Object.entries(query)) {
291
+ if (value === void 0) continue;
292
+ const placeholder = `{${key}}`;
293
+ if (resolvedPath.includes(placeholder)) {
294
+ resolvedPath = resolvedPath.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
295
+ } else {
296
+ remainingQuery[key] = String(value);
297
+ }
298
+ }
299
+ }
300
+ const fullUrl = resolvedPath.startsWith("http") ? new URL(resolvedPath) : new URL(resolvedPath, this.baseUrl);
301
+ for (const [key, value] of Object.entries(remainingQuery)) {
302
+ fullUrl.searchParams.set(key, value);
303
+ }
304
+ const headers = {
305
+ Accept: "application/json"
306
+ };
307
+ const authToken = token ?? this.getToken();
308
+ if (authToken) {
309
+ headers["Authorization"] = `OAuth ${authToken}`;
310
+ }
311
+ let fetchBody;
312
+ if (body !== void 0) {
313
+ headers["Content-Type"] = "application/json";
314
+ fetchBody = JSON.stringify(body);
315
+ }
316
+ const response = await this.fetchFn(fullUrl.toString(), {
317
+ method,
318
+ headers,
319
+ body: fetchBody
320
+ });
321
+ const responseHeaders = {};
322
+ if (typeof response.headers.forEach === "function") {
323
+ response.headers.forEach((value, key) => {
324
+ responseHeaders[key] = value;
325
+ });
326
+ }
327
+ let data;
328
+ const contentLength = response.headers.get("content-length");
329
+ if (response.status === 204 || contentLength === "0") {
330
+ data = void 0;
331
+ } else {
332
+ try {
333
+ data = await response.json();
334
+ } catch {
335
+ data = void 0;
336
+ }
337
+ }
338
+ return { data, status: response.status, headers: responseHeaders };
339
+ }
340
+ /** GET shorthand */
341
+ get(path, params) {
342
+ return this.request({ method: "GET", path, query: params });
343
+ }
344
+ /** POST shorthand */
345
+ post(path, body) {
346
+ return this.request({ method: "POST", path, body });
347
+ }
348
+ /** PUT shorthand */
349
+ put(path, body) {
350
+ return this.request({ method: "PUT", path, body });
351
+ }
352
+ /** DELETE shorthand */
353
+ delete(path) {
354
+ return this.request({ method: "DELETE", path });
355
+ }
356
+ };
357
+
220
358
  // src/client/SoundCloudClient.ts
221
359
  function resolveToken(tokenGetter, explicit) {
222
360
  const t = explicit ?? tokenGetter();
@@ -245,6 +383,8 @@ exports.SoundCloudClient = class _SoundCloudClient {
245
383
  likes;
246
384
  /** Repost/unrepost actions (/reposts) */
247
385
  reposts;
386
+ /** Low-level raw HTTP client — returns status/headers without throwing on non-2xx */
387
+ raw;
248
388
  /**
249
389
  * Creates a new SoundCloudClient instance.
250
390
  *
@@ -256,7 +396,8 @@ exports.SoundCloudClient = class _SoundCloudClient {
256
396
  const retryConfig = {
257
397
  maxRetries: config.maxRetries ?? 3,
258
398
  retryBaseDelay: config.retryBaseDelay ?? 1e3,
259
- onDebug: config.onDebug
399
+ onDebug: config.onDebug,
400
+ onRetry: config.onRetry
260
401
  };
261
402
  const refreshCtx = config.onTokenRefresh ? {
262
403
  getToken,
@@ -285,6 +426,7 @@ exports.SoundCloudClient = class _SoundCloudClient {
285
426
  this.resolve = new _SoundCloudClient.Resolve(getToken, refreshCtx);
286
427
  this.likes = new _SoundCloudClient.Likes(getToken, refreshCtx);
287
428
  this.reposts = new _SoundCloudClient.Reposts(getToken, refreshCtx);
429
+ this.raw = new RawClient("https://api.soundcloud.com", getToken, config.fetch ?? globalThis.fetch);
288
430
  }
289
431
  /**
290
432
  * Store an access token (and optionally refresh token) on this client instance.
@@ -732,6 +874,27 @@ exports.SoundCloudClient = class _SoundCloudClient {
732
874
  const t = resolveToken(this.getToken, options?.token);
733
875
  return this.fetch({ path: `/me/tracks?${limit ? `limit=${limit}&` : ""}linked_partitioning=true`, method: "GET", token: t });
734
876
  }
877
+ /**
878
+ * List the authenticated user's connected external social accounts.
879
+ *
880
+ * @param options - Optional token override
881
+ * @returns Array of connection objects for linked social services (Twitter, Facebook, etc.)
882
+ * @throws {SoundCloudError} When the API returns an error
883
+ *
884
+ * @remarks This endpoint may require elevated API access or app approval.
885
+ *
886
+ * @example
887
+ * ```ts
888
+ * const connections = await sc.me.getConnections();
889
+ * connections.forEach(c => console.log(c.service, c.display_name));
890
+ * ```
891
+ *
892
+ * @see https://developers.soundcloud.com/docs/api/explorer/open-api#/me/get_me_connections
893
+ */
894
+ async getConnections(options) {
895
+ const t = resolveToken(this.getToken, options?.token);
896
+ return this.fetch({ path: "/me/connections", method: "GET", token: t });
897
+ }
735
898
  }
736
899
  SoundCloudClient2.Me = Me;
737
900
  class Users {
@@ -903,6 +1066,26 @@ exports.SoundCloudClient = class _SoundCloudClient {
903
1066
  const t = resolveToken(this.getToken, options?.token);
904
1067
  return this.fetch({ path: `/tracks/${trackId}`, method: "GET", token: t });
905
1068
  }
1069
+ /**
1070
+ * Fetch multiple tracks by their IDs in a single request.
1071
+ *
1072
+ * @param ids - Array of track IDs (numeric or string URNs)
1073
+ * @param options - Optional token override
1074
+ * @returns Array of track objects (may be shorter than `ids` if some tracks are unavailable)
1075
+ * @throws {SoundCloudError} When the API returns an error
1076
+ *
1077
+ * @example
1078
+ * ```ts
1079
+ * const tracks = await sc.tracks.getTracks([123456, 234567, 345678]);
1080
+ * tracks.forEach(t => console.log(t.title));
1081
+ * ```
1082
+ *
1083
+ * @see https://developers.soundcloud.com/docs/api/explorer/open-api#/tracks/get_tracks
1084
+ */
1085
+ async getTracks(ids, options) {
1086
+ const t = resolveToken(this.getToken, options?.token);
1087
+ return this.fetch({ path: `/tracks?ids=${ids.join(",")}`, method: "GET", token: t });
1088
+ }
906
1089
  /**
907
1090
  * Get stream URLs for a track.
908
1091
  *
@@ -1458,6 +1641,90 @@ exports.SoundCloudClient = class _SoundCloudClient {
1458
1641
  SoundCloudClient2.Reposts = Reposts;
1459
1642
  })(exports.SoundCloudClient || (exports.SoundCloudClient = {}));
1460
1643
 
1644
+ // src/client/dedupe.ts
1645
+ var InFlightDeduper = class {
1646
+ inFlight = /* @__PURE__ */ new Map();
1647
+ /**
1648
+ * Return an existing in-flight promise for `key`, or start a new one via `factory`.
1649
+ * The entry is removed from the map once the promise settles (resolve or reject).
1650
+ */
1651
+ add(key, factory) {
1652
+ const existing = this.inFlight.get(key);
1653
+ if (existing) return existing;
1654
+ const promise = factory().finally(() => {
1655
+ this.inFlight.delete(key);
1656
+ });
1657
+ this.inFlight.set(key, promise);
1658
+ return promise;
1659
+ }
1660
+ /** Number of currently in-flight requests */
1661
+ get size() {
1662
+ return this.inFlight.size;
1663
+ }
1664
+ };
1665
+
1666
+ // src/client/registry.ts
1667
+ var IMPLEMENTED_OPERATIONS = [
1668
+ // Auth
1669
+ "post_oauth2_token",
1670
+ "delete_oauth2_token",
1671
+ // Me
1672
+ "get_me",
1673
+ "get_me_activities",
1674
+ "get_me_activities_own",
1675
+ "get_me_activities_tracks",
1676
+ "get_me_likes_tracks",
1677
+ "get_me_likes_playlists",
1678
+ "get_me_followings",
1679
+ "get_me_followings_tracks",
1680
+ "post_me_followings_user_id",
1681
+ "delete_me_followings_user_id",
1682
+ "get_me_followers",
1683
+ "get_me_playlists",
1684
+ "get_me_tracks",
1685
+ "get_me_connections",
1686
+ // Users
1687
+ "get_users_user_id",
1688
+ "get_users_user_id_followers",
1689
+ "get_users_user_id_followings",
1690
+ "get_users_user_id_tracks",
1691
+ "get_users_user_id_playlists",
1692
+ "get_users_user_id_likes_tracks",
1693
+ "get_users_user_id_likes_playlists",
1694
+ "get_users_user_id_web_profiles",
1695
+ // Tracks
1696
+ "get_tracks_track_id",
1697
+ "put_tracks_track_id",
1698
+ "delete_tracks_track_id",
1699
+ "get_tracks_track_id_comments",
1700
+ "post_tracks_track_id_comments",
1701
+ "get_tracks_track_id_likes",
1702
+ "get_tracks_track_id_reposts",
1703
+ "get_tracks_track_id_related",
1704
+ "get_tracks_track_id_streams",
1705
+ "post_likes_tracks_track_id",
1706
+ "delete_likes_tracks_track_id",
1707
+ "post_reposts_tracks_track_id",
1708
+ "delete_reposts_tracks_track_id",
1709
+ // Playlists
1710
+ "get_playlists_playlist_id",
1711
+ "post_playlists",
1712
+ "put_playlists_playlist_id",
1713
+ "delete_playlists_playlist_id",
1714
+ "get_playlists_playlist_id_tracks",
1715
+ "get_playlists_playlist_id_reposts",
1716
+ "post_likes_playlists_playlist_id",
1717
+ "delete_likes_playlists_playlist_id",
1718
+ "post_reposts_playlists_playlist_id",
1719
+ "delete_reposts_playlists_playlist_id",
1720
+ // Search
1721
+ "get_tracks",
1722
+ "get_users",
1723
+ "get_playlists",
1724
+ // Resolve
1725
+ "get_resolve"
1726
+ ];
1727
+
1461
1728
  // src/auth/getClientToken.ts
1462
1729
  var getClientToken = (clientId, clientSecret) => {
1463
1730
  const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
@@ -1577,6 +1844,13 @@ var getUserWebProfiles = (token, userId) => scFetch({ path: `/users/${userId}/we
1577
1844
  // src/tracks/getTrack.ts
1578
1845
  var getTrack = (token, trackId) => scFetch({ path: `/tracks/${trackId}`, method: "GET", token });
1579
1846
 
1847
+ // src/tracks/getTracks.ts
1848
+ var getTracks = (token, ids) => scFetch({
1849
+ path: `/tracks?ids=${ids.join(",")}`,
1850
+ method: "GET",
1851
+ token
1852
+ });
1853
+
1580
1854
  // src/tracks/getComments.ts
1581
1855
  var getTrackComments = (token, trackId, limit) => scFetch({ path: `/tracks/${trackId}/comments?threaded=1&filter_replies=0${limit ? `&limit=${limit}` : ""}&linked_partitioning=true`, method: "GET", token });
1582
1856
 
@@ -1695,6 +1969,9 @@ var getMePlaylists = (token, limit) => scFetch({ path: `/me/playlists?${limit ?
1695
1969
  // src/me/tracks.ts
1696
1970
  var getMeTracks = (token, limit) => scFetch({ path: `/me/tracks?${limit ? `limit=${limit}&` : ""}linked_partitioning=true`, method: "GET", token });
1697
1971
 
1972
+ // src/me/connections.ts
1973
+ var getMeConnections = (token) => scFetch({ path: "/me/connections", method: "GET", token });
1974
+
1698
1975
  // src/likes/index.ts
1699
1976
  var likePlaylist = async (token, playlistId) => {
1700
1977
  try {
@@ -1750,6 +2027,9 @@ var unrepostPlaylist = async (token, playlistId) => {
1750
2027
  // src/utils/widget.ts
1751
2028
  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`;
1752
2029
 
2030
+ exports.IMPLEMENTED_OPERATIONS = IMPLEMENTED_OPERATIONS;
2031
+ exports.InFlightDeduper = InFlightDeduper;
2032
+ exports.RawClient = RawClient;
1753
2033
  exports.createPlaylist = createPlaylist;
1754
2034
  exports.createTrackComment = createTrackComment;
1755
2035
  exports.deletePlaylist = deletePlaylist;
@@ -1766,6 +2046,7 @@ exports.getMe = getMe;
1766
2046
  exports.getMeActivities = getMeActivities;
1767
2047
  exports.getMeActivitiesOwn = getMeActivitiesOwn;
1768
2048
  exports.getMeActivitiesTracks = getMeActivitiesTracks;
2049
+ exports.getMeConnections = getMeConnections;
1769
2050
  exports.getMeFollowers = getMeFollowers;
1770
2051
  exports.getMeFollowings = getMeFollowings;
1771
2052
  exports.getMeFollowingsTracks = getMeFollowingsTracks;
@@ -1783,6 +2064,7 @@ exports.getTrackComments = getTrackComments;
1783
2064
  exports.getTrackLikes = getTrackLikes;
1784
2065
  exports.getTrackReposts = getTrackReposts;
1785
2066
  exports.getTrackStreams = getTrackStreams;
2067
+ exports.getTracks = getTracks;
1786
2068
  exports.getUser = getUser;
1787
2069
  exports.getUserLikesPlaylists = getUserLikesPlaylists;
1788
2070
  exports.getUserLikesTracks = getUserLikesTracks;
@@ -1811,5 +2093,5 @@ exports.unrepostPlaylist = unrepostPlaylist;
1811
2093
  exports.unrepostTrack = unrepostTrack;
1812
2094
  exports.updatePlaylist = updatePlaylist;
1813
2095
  exports.updateTrack = updateTrack;
1814
- //# sourceMappingURL=chunk-2747SK6H.js.map
1815
- //# sourceMappingURL=chunk-2747SK6H.js.map
2096
+ //# sourceMappingURL=chunk-VBDIRSOG.js.map
2097
+ //# sourceMappingURL=chunk-VBDIRSOG.js.map