soundcloud-core 1.0.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included
12
+ in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ [![NPM Version](http://img.shields.io/npm/v/soundcloud-core.svg?style=flat-square)](https://npmjs.com/package/soundcloud-core)
2
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-success?style=flat-square)](https://www.npmjs.com/package/soundcloud-core?activeTab=dependencies)
3
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D%2018.0.0-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/soundcloud-core.svg?style=flat-square)](https://npm-stat.com/charts.html?package=soundcloud-core)
7
+ [![Install Size](https://packagephobia.com/badge?p=soundcloud-core&style=flat-square)](https://packagephobia.com/result?p=soundcloud-core)
8
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/soundcloud-core?style=flat-square)](https://bundlephobia.com/package/soundcloud-core@latest)
9
+
10
+ ![GitHub last commit](https://img.shields.io/github/last-commit/blazeinferno64/soundcloud-core?style=flat-square)
11
+ ![GitHub issues](https://img.shields.io/github/issues/blazeinferno64/soundcloud-core?style=flat-square)
12
+ [![Gitpod Ready-to-code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/blazeinferno64/soundcloud-core)
13
+ [![Author](https://img.shields.io/badge/author-BlazeInferno64-orange?style=flat-square&logo=github)](https://github.com/blazeinferno64)
14
+ ![GitHub Repo stars](https://img.shields.io/github/stars/blazeinferno64/soundcloud-core?style=social)
15
+
16
+ # soundcloud-core
17
+
18
+ > Fast, Minimalist, Unofficial SoundCloud v2 API client wrapper for [Node.js](https://nodejs.org)
19
+
20
+ ## Disclaimer & Terms of Use
21
+
22
+ This project is an independent, unofficial wrapper for the **SoundCloud v2 API**. It is **not** affiliated, endorsed, sponsored, or officially connected with SoundCloud Limited in any way.
23
+
24
+ The author and contributors do not encourage, condone, or support any misuse of the [SoundCloud API](https://developers.soundcloud.com/), scraping practices, or any actions that violate [SoundCloud's Terms of Service](https://soundcloud.com/terms-of-use), [API Terms of Use](https://developers.soundcloud.com/docs/api/terms-of-use), or platform policies. This library is provided for educational and experimental purposes only. Users are solely responsible for ensuring that their usage of this software complies with **SoundCloud's official terms and applicable laws**.
25
+
26
+ # Installation
27
+
28
+ To get started with `soundcloud-core`, simply run the following command in your terminal:
29
+
30
+ Using [npm](https://npmjs.com) installation command:
31
+
32
+ ```bash
33
+ $ npm i soundcloud-core
34
+ ```
35
+
36
+ Using [yarn](https://yarnpkg.com) installation command:
37
+
38
+ ```bash
39
+ $ yarn add soundcloud-core
40
+ ```
41
+
42
+ Using [bun](https://bun.sh) installation command:
43
+
44
+ ```bash
45
+ $ bun add soundcloud-core
46
+ ```
47
+ # Info
48
+
49
+ New to Promises?
50
+
51
+ If you're not familiar with promises, check out the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to learn more.
52
+
53
+ ## Built on Top of Node.js HTTP library
54
+
55
+ Under the hood, `soundcloud-core` uses the native [Fetch API](https://nodejs.org/learn/getting-started/fetch) provided by Node.js (undici), ensuring a seamless and efficient experience without any third party packages.
56
+
57
+ # Getting started
58
+
59
+ First, require/import this library to your project as follows:
60
+
61
+ ```js
62
+ const { SoundCloudClient } = require("soundcloud-core");
63
+ ```
64
+
65
+ If it's an ES Module then import it to your project as follows:
66
+
67
+ ```js
68
+ import { SoundCloudClient } from "soundcloud-core";
69
+ ```
70
+
71
+ Then spin up a client and you're good to go:
72
+
73
+ ```js
74
+ const client = new SoundCloudClient();
75
+ ```
76
+
77
+ # Client Id
78
+
79
+ As per [SoundCloud's v2 API](https://developers.soundcloud.com/), no client id is strictly necessary to use this library - if you don't pass one in, `soundcloud-core` will quietly go fetch a fresh one for you the first time it actually needs it (by pulling it straight off [soundcloud.com](https://soundcloud.com)), and reuse it for the lifetime of the client.
80
+
81
+ That said, if you already have a `client_id` lying around (or you're hitting rate limits and want a bit more control over when/how it's fetched), you can just hand it to the constructor yourself:
82
+
83
+ ```js
84
+ const client = new SoundCloudClient({
85
+ // Leave it empty, incase you don't have one!
86
+ clientId: "your_client_id"
87
+ });
88
+ ```
89
+
90
+ You can also grab whatever `client_id` the client ends up using, in case you want to cache it or reuse it elsewhere:
91
+
92
+ ```js
93
+ const clientId = await client.getClientId();
94
+ console.log(clientId);
95
+ ```
96
+
97
+ # Usage
98
+
99
+ Every method on `SoundCloudClient` returns a promise, so `await` them (or `.then()` them) inside an `async` function.
100
+
101
+ ## Fetching a track
102
+
103
+ ```js
104
+ const client = new SoundCloudClient();
105
+
106
+ const track = await client.getMetaData({
107
+ url: "https://soundcloud.com/martingarrix/martin-garrix-animals-original" // Martin Garrix - Animals
108
+ });
109
+
110
+ console.log(track.title); // "Track Name"
111
+ console.log(track.streamUrl); // Playable HLS/progressive stream (Best for audio quality) URL (short-lived!)
112
+ ```
113
+
114
+ Keep in mind `streamUrl` is short-lived and will expire after a while, so don't go caching it for later - fetch it fresh whenever you actually need to stream the track.
115
+
116
+ ## Fetching a playlist
117
+
118
+ ```js
119
+ const playlist = await client.getPlaylist({
120
+ // Place your playlist url here, for example I'am placing my playlist url here
121
+ url: "https://soundcloud.com/blazeinferno64/sets/only-house",
122
+ limit: 20 // grab up to 20 tracks, or pass "max" for the whole playlist
123
+ });
124
+
125
+ console.log(playlist.title, playlist.trackCount);
126
+ playlist.tracks.forEach(track => console.log(track.title));
127
+ ```
128
+
129
+ ## Fetching a user profile
130
+
131
+ ```js
132
+ // A bare username works just as well as a full profile URL
133
+ const profile = await client.getProfile({
134
+ // Your username here, for example I'am placing my username here
135
+ username: "BlazeInferno64"
136
+ });
137
+
138
+ console.log(profile.username, profile.stats.followers);
139
+ ```
140
+
141
+ ## Searching for tracks
142
+
143
+ ```js
144
+ const results = await client.search({
145
+ query: "Martin Garrix - Animals", // Any song you want to search
146
+ limit: 15
147
+ });
148
+
149
+ results.forEach(track => console.log(track.title, "-", track.artist.username));
150
+ ```
151
+
152
+ # LICENSE
153
+
154
+ `soundcloud-core` is released under the MIT License.
155
+
156
+ View the full license terms <a href="https://github.com/BlazeInferno64/soundcloud-core/blob/main/LICENSE">here</a>.
157
+
158
+ # Bugs & Issues
159
+
160
+ Found a bug or want a new feature?
161
+
162
+ Report issues and request features on the [soundcloud-core issue tracker](https://github.com/blazeinferno64/soundcloud-core/issues).
163
+
164
+ `Thanks for reading!`
165
+
166
+ `Have a great day ahead :D`
package/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 10/09/2026
6
+ //
7
+ // Permission is hereby granted, free of charge, to any person obtaining a
8
+ // copy of this software and associated documentation files (the
9
+ // "Software"), to deal in the Software without restriction, including
10
+ // without limitation the rights to use, copy, modify, merge, publish,
11
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
12
+ // persons to whom the Software is furnished to do so, subject to the
13
+ // following conditions:
14
+ //
15
+ // The above copyright notice and this permission notice shall be included
16
+ // in all copies or substantial portions of the Software.
17
+ //
18
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ "use strict";
27
+ /**
28
+ * soundcloud-core is a Fast, Minimalist, Unofficial SoundCloud v2 API client wrapper for [Node.js](https://nodejs.org)
29
+ *
30
+ * SoundCloud API Requests done right!
31
+ *
32
+ * Learn more about it from [here](https://github.com/blazeinferno64/soundcloud-core)
33
+ * @example
34
+ * // Require it in your project by doing -
35
+ * const { SoundCloudClient } = require("soundcloud-core");
36
+ *
37
+ * // Or import it to your project if its an ES module by doing -
38
+ * import { SoundCloudClient } from "soundcloud-core";
39
+ */
40
+ module.exports = require("./src/soundcloud-core");
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "dependencies": {
3
+ "cheerio": "^1.2.0"
4
+ },
5
+ "name": "soundcloud-core",
6
+ "version": "1.0.0",
7
+ "description": "Fast, Minimalist, Unofficial SoundCloud v2 API client wrapper for Node.js",
8
+ "main": "index.js",
9
+ "devDependencies": {},
10
+ "scripts": {
11
+ "test": "echo \"All success!\" && exit 1"
12
+ },
13
+ "types": "typings/index.d.ts",
14
+ "contributors": [
15
+ "BlazeInferno64",
16
+ "blazeinferno"
17
+ ],
18
+ "keywords": [
19
+ "blazed",
20
+ "blazed.js",
21
+ "HTTP-Client",
22
+ "blazed-http-client",
23
+ "nodejs",
24
+ "javascript",
25
+ "node-http",
26
+ "node-fetch",
27
+ "axios",
28
+ "gaxios",
29
+ "node",
30
+ "https",
31
+ "http",
32
+ "json",
33
+ "automatic-json-parser",
34
+ "json-server",
35
+ "http/2",
36
+ "ws",
37
+ "node-blazed.js",
38
+ "node-http/2",
39
+ "node-https",
40
+ "express",
41
+ "typescript",
42
+ "connect",
43
+ "got",
44
+ "git",
45
+ "htmx",
46
+ "www",
47
+ "npm",
48
+ "high-peformance",
49
+ "keep-Alive",
50
+ "apache",
51
+ "http/1.1",
52
+ "http/1",
53
+ "http/3",
54
+ "ai",
55
+ "openai",
56
+ "api",
57
+ "binary",
58
+ "microsoft",
59
+ "google",
60
+ "gemini",
61
+ "cloudflare",
62
+ "dns",
63
+ "dns-client",
64
+ "light-weight",
65
+ "fast-http-client",
66
+ "promise-based-http-client",
67
+ "eslint",
68
+ "openjs",
69
+ "regex",
70
+ "light-weight",
71
+ "babel__core",
72
+ "soundcloud",
73
+ "soundcloud-core",
74
+ "soundcloud.ts",
75
+ "soundcloud-core",
76
+ "youtube",
77
+ "ytdl-core",
78
+ "yt-music",
79
+ "yt"
80
+ ],
81
+ "author": "BlazeInferno64",
82
+ "license": "MIT",
83
+ "homepage": "https://github.com/blazeinferno64/soundcloud-core#readme",
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "git+https://github.com/blazeinferno64/soundcloud-core.git"
87
+ },
88
+ "type": "commonjs"
89
+ }
@@ -0,0 +1,69 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 09/05/2026
6
+
7
+ const { ua } = require('./user-agent');
8
+ const { getFreshClientID } = require('./client-id');
9
+
10
+ //const cheerio = require("cheerio");
11
+
12
+ const getCDNUrl = async (track, clientID, userAgent) => {
13
+ try {
14
+ if (!track) throw new Error("Track ID is required to fetch the CDN URL.");
15
+ if (!clientID) clientID = await getFreshClientID(userAgent);
16
+
17
+ if (!track.media?.transcodings?.length) return null;
18
+
19
+ const transcodings = track.media.transcodings;
20
+
21
+ // TOP PRIORITY: High Quality AAC (HLS)
22
+ let transcoding = transcodings.find(t =>
23
+ t.format.protocol === 'hls' &&
24
+ t.preset.includes('aac')
25
+ );
26
+
27
+ // SECOND PRIORITY: Standard Progressive MP3
28
+ if (!transcoding) {
29
+ transcoding = transcodings.find(t =>
30
+ t.format.protocol === 'progressive' &&
31
+ t.format.mime_type.includes('audio/mpeg')
32
+ );
33
+ }
34
+
35
+ // LAST RESORT: Anything that isn't Opus
36
+ if (!transcoding) {
37
+ transcoding = transcodings.find(t => !t.preset.includes('opus'));
38
+ }
39
+
40
+ if (!transcoding) return null;
41
+
42
+ // Construct the final URL with the client_id and optional track_authorization
43
+ let url = `${transcoding.url}?client_id=${clientID}`;
44
+ if (track.track_authorization) {
45
+ // Append the track_authorization parameter if it exists
46
+ url += `&track_authorization=${track.track_authorization}`;
47
+ }
48
+
49
+ // Here we make the request to get the actual CDN URL to stream the track. We use the provided userAgent or default to the one from user-agent.js.
50
+ const response = await fetch(url, {
51
+ headers: {
52
+ 'User-Agent': userAgent || ua,
53
+ 'Accept': 'application/json' // Requesting JSON response
54
+ }
55
+ })
56
+
57
+ const data = await response.json();
58
+ return data.url; // Return the actual CDN URL for streaming! If HLS, this will be a .m3u8 playlist URL; if progressive, it will be a direct .mp3 URL.
59
+
60
+ } catch (e) {
61
+ // If any error occurs during the process, we throw an error with a descriptive message.
62
+ throw new Error(`Cannot fetch CDN URL for track ${track}: ${e.message ? e.message : e}`);
63
+ }
64
+ }
65
+
66
+ // Export the functions
67
+ module.exports = {
68
+ getCDNUrl
69
+ }
@@ -0,0 +1,61 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 09/05/2026
6
+
7
+ "use strict";
8
+
9
+ const { ua } = require('./user-agent');
10
+
11
+ const cheerio = require("cheerio");
12
+
13
+ const getFreshClientID = async (userAgent) => {
14
+ try {
15
+ const response = await fetch('https://soundcloud.com', {
16
+ headers: {
17
+ 'User-Agent': userAgent || ua,
18
+ }
19
+ });
20
+
21
+ const html = await response.text();
22
+ const $ = cheerio.load(html);
23
+
24
+ // Find all script tags with src attributes that contain 'sndcdn.com/assets/'
25
+ const scriptUrls = [];
26
+ $('script[src]').each((i, el) => {
27
+ const src = $(el).attr('src');
28
+ // Search for the client_id pattern in the script content
29
+ if (src && src.includes('sndcdn.com/assets/')) {
30
+ scriptUrls.push(src);
31
+ }
32
+ });
33
+
34
+ // Iterate through the script URLs in reverse order to find the latest one
35
+ for (const url of scriptUrls.reverse()) {
36
+ const jsRes = await fetch(url, {
37
+ headers: {
38
+ 'User-Agent': userAgent || ua,
39
+ // intentionally not forcing Content-Type to match original behavior
40
+ }
41
+ });
42
+ const jsContent = await jsRes.text();
43
+
44
+ // Yeps! We found the client_id in the script content
45
+ const match = jsContent.match(/client_id[:=]\s*["']([a-zA-Z0-9]{32})["']/);
46
+ if (match && match[1]) {
47
+ // FINALLY! Return the client_id if found or else skip it
48
+ return match[1];
49
+ }
50
+ }
51
+ } catch (e) {
52
+ // Well, SoundCloud might have changed their structure or something went wrong, so we throw an error or have blocked the request.
53
+ // Either way, we can't get the client_id now :(
54
+ throw new Error(`Cannot fetch clientID from SoundCloud: ${e.message ? e.message : e}`);
55
+ }
56
+ }
57
+
58
+
59
+ module.exports = {
60
+ getFreshClientID
61
+ }
@@ -0,0 +1,151 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 09/07/2026
6
+
7
+ const { ua } = require('./user-agent');
8
+ const { getFreshClientID } = require('./client-id');
9
+
10
+ const CHUNK_SIZE = 50; // SoundCloud's batch tracks endpoint caps out around here - stay safely under it
11
+
12
+ // `limit` accepts a positive integer (how many tracks to include) or the string "max" (every track in the playlist).
13
+ // Returns Infinity for "max" so the slice() below just takes everything.
14
+ const resolveLimit = (limit) => {
15
+ if (limit === 'max') return Infinity;
16
+ if (typeof limit === 'number' && Number.isInteger(limit) && limit > 0) return limit;
17
+ throw new Error(`Invalid limit provided! Expected a positive integer or "max", got: ${JSON.stringify(limit)}`);
18
+ };
19
+
20
+ // Turn a raw track (whether it came inline or from a batch fetch) into the same reduced shape.
21
+ // track === null covers the case where a stub id couldn't be resolved (deleted/private/geo-blocked).
22
+ const reduceTrack = (id, track) => {
23
+ if (!track) {
24
+ return { id, title: null, permalinkUrl: null, duration: null, genre: null, artworkUrl: null, artist: null };
25
+ }
26
+
27
+ return {
28
+ id: track.id,
29
+ title: track.title,
30
+ permalinkUrl: track.permalink_url,
31
+ duration: track.duration,
32
+ genre: track.genre,
33
+ artworkUrl: track.artwork_url
34
+ ? track.artwork_url.replace("-large.jpg", "-t500x500.jpg")
35
+ : (track.user?.avatar_url?.replace("-large.jpg", "-t500x500.jpg") || null),
36
+ artist: track.user ? {
37
+ id: track.user.id,
38
+ username: track.user.username,
39
+ profileUrl: track.user.permalink_url
40
+ } : null
41
+ };
42
+ };
43
+
44
+ // Fetches full track data for a batch of stub ids, chunked to stay under SoundCloud's per-request limit.
45
+ // Returns a Map of id -> raw track so failed/missing ids can just fall back to null downstream.
46
+ const resolveStubTracks = async (ids, userAgent, clientID) => {
47
+ const resolved = new Map();
48
+
49
+ for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
50
+ const chunk = ids.slice(i, i + CHUNK_SIZE);
51
+ const idsUrl = `https://api-v2.soundcloud.com/tracks?ids=${chunk.join(',')}&client_id=${clientID}`;
52
+
53
+ const response = await fetch(idsUrl, {
54
+ headers: {
55
+ 'User-Agent': userAgent || ua,
56
+ }
57
+ });
58
+
59
+ if (!response.ok) continue; // Skip a bad chunk rather than failing the whole playlist over a few tracks
60
+
61
+ const chunkTracks = await response.json();
62
+ for (const track of chunkTracks) {
63
+ resolved.set(track.id, track);
64
+ }
65
+ }
66
+
67
+ return resolved;
68
+ };
69
+
70
+ const fetchPlaylist = async (playlistUrl, userAgent, clientID, limit = 10) => {
71
+ try {
72
+ if (!playlistUrl || typeof playlistUrl !== "string") {
73
+ throw new Error("Invalid playlist URL provided!");
74
+ }
75
+
76
+ const maxTracks = resolveLimit(limit);
77
+
78
+ if (!clientID) clientID = await getFreshClientID(userAgent);
79
+
80
+ const resolveUrl = `https://api-v2.soundcloud.com/resolve?url=${encodeURIComponent(playlistUrl)}&client_id=${clientID}`;
81
+
82
+ const response = await fetch(resolveUrl, {
83
+ headers: {
84
+ 'User-Agent': userAgent || ua,
85
+ }
86
+ });
87
+
88
+ if (!response.ok) {
89
+ throw new Error(`Failed to resolve playlist URL. Status: ${response.status}`);
90
+ }
91
+
92
+ const playlist = await response.json();
93
+
94
+ // The resolve endpoint can also return tracks/users - playlists and "sets" both come back as kind "playlist".
95
+ if (!playlist || playlist.kind !== "playlist") {
96
+ throw new Error(`Provided URL did not resolve to a playlist (kind: ${playlist?.kind || "unknown"}).`);
97
+ }
98
+
99
+ const rawTracks = (playlist.tracks || []).slice(0, maxTracks === Infinity ? undefined : maxTracks);
100
+
101
+ // SoundCloud only inlines full data for roughly the first 5 tracks - the rest come back as stubs (id only).
102
+ // Batch-fetch the missing ones so the playlist comes back fully populated, same as the web app does.
103
+ const stubIds = rawTracks.filter(t => !t.title).map(t => t.id);
104
+ const resolvedStubs = stubIds.length
105
+ ? await resolveStubTracks(stubIds, userAgent, clientID)
106
+ : new Map();
107
+
108
+ const tracks = rawTracks.map(t =>
109
+ reduceTrack(t.id, t.title ? t : resolvedStubs.get(t.id))
110
+ );
111
+
112
+ const playlistData = {
113
+ id: playlist.id,
114
+ title: playlist.title,
115
+ description: playlist.description,
116
+ permalinkUrl: playlist.permalink_url,
117
+ trackCount: playlist.track_count, // total tracks in the playlist - not affected by `limit`, see `tracks.length` for how many actually came back
118
+ duration: playlist.duration, // total duration in milliseconds
119
+ genre: playlist.genre,
120
+ createdAt: playlist.created_at,
121
+
122
+ // Curator metadata (the user who made the playlist)
123
+ curator: {
124
+ id: playlist.user.id,
125
+ username: playlist.user.username,
126
+ profileUrl: playlist.user.permalink_url,
127
+ avatarUrl: playlist.user.avatar_url?.replace("-large.jpg", "-t500x500.jpg")
128
+ },
129
+
130
+ artworkUrl: playlist.artwork_url
131
+ ? playlist.artwork_url.replace("-large.jpg", "-t500x500.jpg")
132
+ : playlist.user.avatar_url?.replace("-large.jpg", "-t500x500.jpg"),
133
+
134
+ // Engagement stats
135
+ stats: {
136
+ likes: playlist.likes_count,
137
+ reposts: playlist.reposts_count
138
+ },
139
+
140
+ tracks
141
+ };
142
+
143
+ return playlistData;
144
+ } catch (e) {
145
+ throw new Error("Failed to fetch playlist!");
146
+ }
147
+ }
148
+
149
+ module.exports = {
150
+ fetchPlaylist
151
+ }