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.
@@ -0,0 +1,127 @@
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
+ // Accepts either a bare username ("BlazeInferno64") or a full profile URL and normalizes it into
11
+ // something the resolve endpoint can work with either way.
12
+ const normalizeProfileUrl = (input) => {
13
+ if (/^https?:\/\//i.test(input)) return input;
14
+ return `https://soundcloud.com/${input.trim().toLowerCase()}`;
15
+ };
16
+
17
+ const fetchProfile = async (profileInput, userAgent, clientID) => {
18
+ try {
19
+ if (!profileInput || typeof profileInput !== "string") {
20
+ throw new Error("Invalid username or profile URL provided!");
21
+ }
22
+
23
+ if (!clientID) clientID = await getFreshClientID(userAgent);
24
+
25
+ const profileUrl = normalizeProfileUrl(profileInput);
26
+ const resolveUrl = `https://api-v2.soundcloud.com/resolve?url=${encodeURIComponent(profileUrl)}&client_id=${clientID}`;
27
+
28
+ const response = await fetch(resolveUrl, {
29
+ headers: {
30
+ 'User-Agent': userAgent || ua,
31
+ }
32
+ });
33
+
34
+ if (!response.ok) {
35
+ throw new Error(`Failed to resolve profile. Status: ${response.status}`);
36
+ }
37
+
38
+ const user = await response.json();
39
+
40
+ //console.log("Resolved user data:", user); // Debugging log to inspect the resolved user object
41
+
42
+ // The resolve endpoint can also return tracks/playlists depending on what the URL points to -
43
+ // we only want an actual user profile here.
44
+ if (!user || user.kind !== "user") {
45
+ throw new Error(`Provided username/URL did not resolve to a profile (kind: ${user?.kind || "unknown"}).`);
46
+ }
47
+
48
+ // The banner image at the top of a profile page lives under visuals, not avatar_url - falls back
49
+ // to null since plenty of profiles don't have one set.
50
+ const headerUrl = user.visuals?.visuals?.[0]?.visual_url || null;
51
+
52
+ const profileData = {
53
+ id: user.id,
54
+ urn: user.urn || null, // internal stable identifier, e.g. "soundcloud:users:1197971329" - useful if you're caching/de-duping across requests
55
+ username: user.username,
56
+ firstName: user.first_name || null,
57
+ lastName: user.last_name || null,
58
+ fullName: user.full_name || null,
59
+ permalink: user.permalink || null, // short slug ("blazeinferno64"), as opposed to the full permalinkUrl below
60
+ permalinkUrl: user.permalink_url,
61
+ description: user.description || null,
62
+
63
+ kind: user.kind || null, // should always be "user" for a profile, but included for completeness
64
+
65
+ location: {
66
+ city: user.city || null,
67
+ country: user.country_code || null
68
+ },
69
+
70
+ avatarUrl: user.avatar_url?.replace("-large.jpg", "-t500x500.jpg"),
71
+ headerUrl,
72
+
73
+ // Pro/Pro Unlimited/verified badges - shown next to the username on the real profile page
74
+ badges: {
75
+ verified: !!user.badges?.verified,
76
+ pro: !!user.badges?.pro,
77
+ proUnlimited: !!user.badges?.pro_unlimited
78
+ },
79
+
80
+ subscriptions: {
81
+ creatorSubscriptions: user.creator_subscriptions.products || [], // array of subscription product IDs the user is subscribed to (e.g. "creator_subscription:premium")
82
+ // The `creator_subscription` field is a newer addition to SoundCloud's subscription model, allowing users to support specific creators directly. The `product.id` can be used to identify the specific subscription product.
83
+ creatorSubscription: user.creator_subscription.product?.id,
84
+ },
85
+
86
+ // External links from the profile's "Links" section (personal site, Instagram, Twitter/X, etc.)
87
+ webProfiles: (user.web_profiles || []).map(link => ({
88
+ network: link.network,
89
+ title: link.title || null,
90
+ url: link.url
91
+ })),
92
+
93
+ // A user's "station" is what SoundCloud autoplays after their tracks finish (their own radio mix) -
94
+ // both fields are internal identifiers, not directly a browsable page URL.
95
+ station: {
96
+ urn: user.station_urn || null,
97
+ permalink: user.station_permalink || null
98
+ },
99
+
100
+ // Stats you'd see laid out on the profile page itself
101
+ stats: {
102
+ followers: user.followers_count,
103
+ following: user.followings_count,
104
+ tracks: user.track_count,
105
+ playlists: user.playlist_count,
106
+ playlistLikes: user.playlist_likes_count,
107
+ likes: user.public_favorites_count ?? user.likes_count,
108
+ reposts: user.reposts_count,
109
+ comments: user.comments_count,
110
+ groups: user.groups_count,
111
+ dateOfBirth: user.date_of_birth || null,
112
+ // Note: SoundCloud doesn't provide a "total plays" stat for a user profile, only per-track plays.
113
+ },
114
+
115
+ createdAt: user.created_at,
116
+ lastModified: user.last_modified || null // last time this profile's data changed on SoundCloud's end
117
+ };
118
+
119
+ return profileData;
120
+ } catch (e) {
121
+ throw new Error("Failed to fetch profile!");
122
+ }
123
+ }
124
+
125
+ module.exports = {
126
+ fetchProfile
127
+ }
@@ -0,0 +1,74 @@
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 search = async (query, userAgent, clientID, options = {}) => {
11
+ try {
12
+ if (!query || typeof query !== "string") {
13
+ throw new Error("Invalid search query provided!");
14
+ }
15
+
16
+ if (!clientID) clientID = await getFreshClientID(userAgent);
17
+
18
+ const limit = options.limit || 10; // How many results to return - defaults to 10, same as the web app's initial batch
19
+
20
+ const searchUrl = `https://api-v2.soundcloud.com/search/tracks?q=${encodeURIComponent(query)}&client_id=${clientID}&limit=${limit}`;
21
+
22
+ const response = await fetch(searchUrl, {
23
+ headers: {
24
+ 'User-Agent': userAgent || ua,
25
+ }
26
+ });
27
+
28
+ if (!response.ok) {
29
+ throw new Error(`Failed to search tracks. Status: ${response.status}`);
30
+ }
31
+
32
+ const data = await response.json();
33
+
34
+ // The search endpoint wraps results in a "collection" array - bail out early if it's missing/empty
35
+ if (!data || !Array.isArray(data.collection)) {
36
+ return [];
37
+ }
38
+
39
+ // Trim each track down to what you'd actually want from a search result (not the full track.js payload -
40
+ // no streamUrl/trackAuthorization here since these are just candidates, not a resolved track yet)
41
+ const results = data.collection.map(track => ({
42
+ id: track.id,
43
+ title: track.title,
44
+ permalinkUrl: track.permalink_url,
45
+ duration: track.duration, // in milliseconds
46
+ genre: track.genre,
47
+
48
+ artworkUrl: track.artwork_url
49
+ ? track.artwork_url.replace("-large.jpg", "-t500x500.jpg")
50
+ : track.user?.avatar_url?.replace("-large.jpg", "-t500x500.jpg"),
51
+
52
+ artist: {
53
+ id: track.user?.id,
54
+ username: track.user?.username,
55
+ profileUrl: track.user?.permalink_url
56
+ },
57
+
58
+ stats: {
59
+ plays: track.playback_count,
60
+ likes: track.likes_count,
61
+ comments: track.comment_count,
62
+ reposts: track.reposts_count
63
+ }
64
+ }));
65
+
66
+ return results;
67
+ } catch (e) {
68
+ throw new Error("Failed to search tracks!");
69
+ }
70
+ }
71
+
72
+ module.exports = {
73
+ search
74
+ }
@@ -0,0 +1,102 @@
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
+ const { getCDNUrl } = require('./cdn');
10
+
11
+ const cheerio = require("cheerio");
12
+
13
+ const fetchSong = async (songUrl, userAgent, clientID) => {
14
+ try {
15
+ if (!songUrl || typeof songUrl !== "string") {
16
+ throw new Error("Invalid song URL provided!");
17
+ }
18
+
19
+ const resolveUrl = `https://api-v2.soundcloud.com/resolve?url=${encodeURIComponent(songUrl)}&client_id=${clientID}`;
20
+
21
+ const response = await fetch(resolveUrl, {
22
+ headers: {
23
+ 'User-Agent': userAgent || ua,
24
+ }
25
+ })
26
+
27
+ if (!response.ok) {
28
+ throw new Error(`Failed to resolve song URL. Status: ${response.status}`);
29
+ }
30
+
31
+ let track = await response.json();
32
+
33
+ // The resolve endpoint can also return sets/users/playlists - we only want single tracks here.
34
+ if (!track || track.kind !== "track") {
35
+ throw new Error(`Provided URL did not resolve to a single track (kind: ${track?.kind || "unknown"}).`);
36
+ }
37
+
38
+ // Some resolved tracks omit media/track_authorization - fetch the full track record.
39
+ if (!track.media || !track.track_authorization) {
40
+ const trackRes = await fetch(`https://api-v2.soundcloud.com/tracks/${track.id}?client_id=${clientID}`, {
41
+ headers: {
42
+ 'User-Agent': userAgent || ua,
43
+ }
44
+ });
45
+ track = await trackRes.json();
46
+ }
47
+
48
+ const cdnUrl = await getCDNUrl(track, clientID, userAgent || ua);
49
+
50
+ if (!cdnUrl) {
51
+ throw new Error(`No playable stream found for track: ${track.title}`);
52
+ }
53
+
54
+ const songData = {
55
+ id: track.id,
56
+ title: track.title,
57
+ description: track.description,
58
+ duration: track.duration, // in milliseconds
59
+ genre: track.genre,
60
+ createdAt: track.created_at,
61
+
62
+ // Artist Metadata
63
+ artist: {
64
+ id: track.user.id,
65
+ username: track.user.username,
66
+ profileUrl: track.user.permalink_url,
67
+ avatarUrl: track.user.avatar_url?.replace("-large.jpg", "-t500x500.jpg")
68
+ },
69
+
70
+ // Visual Assets (Upgraded to high-res, falling back to avatar)
71
+ artworkUrl: track.artwork_url
72
+ ? track.artwork_url.replace("-large.jpg", "-t500x500.jpg")
73
+ : track.user.avatar_url?.replace("-large.jpg", "-t500x500.jpg"),
74
+
75
+ // Album / Publisher Info
76
+ album: track.publisher_metadata?.album_title || null,
77
+ label: track.publisher_metadata?.label_name || null,
78
+
79
+ // Playback & CDN (The crucial part for your client)
80
+ streamUrl: cdnUrl, // The final resolved HLS or progressive MP3 link
81
+ trackAuthorization: track.track_authorization || null,
82
+
83
+ // Engagement Stats (What you see on the web page)
84
+ stats: {
85
+ plays: track.playback_count,
86
+ likes: track.likes_count,
87
+ reposts: track.reposts_count,
88
+ comments: track.comment_count
89
+ }
90
+ }
91
+
92
+ return songData;
93
+ } catch (e) {
94
+ throw new Error("Failed to fetch song!");
95
+ }
96
+ }
97
+
98
+
99
+ module.exports = {
100
+ fetchSong
101
+ }
102
+
@@ -0,0 +1,17 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 09/05/2026
6
+ "use strict";
7
+
8
+ /**
9
+ * Keep a consistent user-agent for all requests to avoid detection and rate limiting.
10
+ * This is especially important for SoundCloud API requests.
11
+ */
12
+ const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36';
13
+
14
+
15
+ module.exports = {
16
+ ua
17
+ }
@@ -0,0 +1,87 @@
1
+ // Copyright (c) 2026 BlazeInferno64 --> https://github.com/blazeinferno64.
2
+ //
3
+ // Author(s) -> BlazeInferno64
4
+ //
5
+ // Last updated: 07/09/2026
6
+ "use strict";
7
+
8
+ const { ua } = require('./plugin/user-agent');
9
+ const { getFreshClientID } = require('./plugin/client-id');
10
+ const { fetchSong } = require('./plugin/track');
11
+ const { search } = require('./plugin/search');
12
+ const { fetchPlaylist } = require('./plugin/playlist');
13
+ const { fetchProfile } = require('./plugin/profile');
14
+
15
+ //const cheerio = require("cheerio");
16
+
17
+ class SoundCloudClient {
18
+ constructor(options = {}) {
19
+ this._useragent = options ? options.userAgent : ua; // Use the provided user agent or default to the one from user-agent.js
20
+ this._clientId = options ? options.clientId : getFreshClientID(this._useragent); // Fetch one if not provided!
21
+ }
22
+
23
+ async getClientId() {
24
+ if (!this._clientId) {
25
+ this._clientId = await getFreshClientID(this._useragent);
26
+ }
27
+ return this._clientId;
28
+ }
29
+
30
+ async getMetaData(songOptions) {
31
+ const { url, userAgent } = songOptions;
32
+ const clientId = await this._clientId || await this.getClientId(this._useragent);
33
+ const finalUserAgent = userAgent || this._useragent;
34
+
35
+ // Validate inputs
36
+ if (!url) throw new Error("No song URL provided!");
37
+ if (!clientId) throw new Error("No client ID available! Please provide one or allow the client to fetch it.");
38
+
39
+ // Fetch the song metadata using the provided URL, user agent, and client ID
40
+ return await fetchSong(url, finalUserAgent, clientId);
41
+ }
42
+
43
+ async getPlaylist(playlistOptions) {
44
+ const { url, userAgent, limit = 10 } = playlistOptions;
45
+ const clientId = await this._clientId || await this.getClientId(this._useragent);
46
+ const finalUserAgent = userAgent || this._useragent;
47
+
48
+ // Validate inputs
49
+ if (!url) throw new Error("No playlist URL provided!");
50
+ if (!clientId) throw new Error("No client ID available! Please provide one or allow the client to fetch it.");
51
+
52
+ // Fetch the playlist metadata using the provided URL, user agent, and client ID.
53
+ // `limit` caps how many tracks come back - a positive integer, or "max" for the whole playlist.
54
+ return await fetchPlaylist(url, finalUserAgent, clientId, limit);
55
+ }
56
+
57
+ async getProfile(profileOptions) {
58
+ const { username, userAgent } = profileOptions;
59
+ const clientId = await this._clientId || await this.getClientId(this._useragent);
60
+ const finalUserAgent = userAgent || this._useragent;
61
+
62
+ // Validate inputs
63
+ if (!username) throw new Error("No username or profile URL provided!");
64
+ if (!clientId) throw new Error("No client ID available! Please provide one or allow the client to fetch it.");
65
+
66
+ // `username` accepts either a bare username ("BlazeInferno64") or a full profile URL
67
+ return await fetchProfile(username, finalUserAgent, clientId);
68
+ }
69
+
70
+ async search(searchOptions = {}) {
71
+ const { query, userAgent, ...options } = searchOptions;
72
+ const clientId = await this._clientId || await this.getClientId(this._useragent);
73
+ const finalUserAgent = userAgent || this._useragent;
74
+
75
+ // Validate inputs
76
+ if (!query) throw new Error("No search query provided!");
77
+ if (!clientId) throw new Error("No client ID available! Please provide one or allow the client to fetch it.");
78
+
79
+ // Search for tracks matching the query, using the provided user agent and client ID
80
+ return await search(query, finalUserAgent, clientId, options);
81
+ }
82
+ }
83
+
84
+
85
+ module.exports = {
86
+ SoundCloudClient
87
+ }