@manhgdev/soundcloud-web
Version:
JavaScript wrapper for SoundCloud API
1,162 lines (1,146 loc) • 33.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
// node_modules/query-string/base.js
var exports_base = {};
__export(exports_base, {
stringifyUrl: () => stringifyUrl,
stringify: () => stringify,
pick: () => pick,
parseUrl: () => parseUrl,
parse: () => parse,
extract: () => extract,
exclude: () => exclude
});
// node_modules/decode-uri-component/index.js
var token = "%[a-f0-9]{2}";
var singleMatcher = new RegExp("(" + token + ")|([^%]+?)", "gi");
var multiMatcher = new RegExp("(" + token + ")+", "gi");
function decodeComponents(components, split) {
try {
return [decodeURIComponent(components.join(""))];
} catch {}
if (components.length === 1) {
return components;
}
split = split || 1;
const left = components.slice(0, split);
const right = components.slice(split);
return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right));
}
function decode(input) {
try {
return decodeURIComponent(input);
} catch {
let tokens = input.match(singleMatcher) || [];
for (let i = 1;i < tokens.length; i++) {
input = decodeComponents(tokens, i).join("");
tokens = input.match(singleMatcher) || [];
}
return input;
}
}
function customDecodeURIComponent(input) {
const replaceMap = {
"%FE%FF": "��",
"%FF%FE": "��"
};
let match = multiMatcher.exec(input);
while (match) {
try {
replaceMap[match[0]] = decodeURIComponent(match[0]);
} catch {
const result = decode(match[0]);
if (result !== match[0]) {
replaceMap[match[0]] = result;
}
}
match = multiMatcher.exec(input);
}
replaceMap["%C2"] = "�";
const entries = Object.keys(replaceMap);
for (const key of entries) {
input = input.replace(new RegExp(key, "g"), replaceMap[key]);
}
return input;
}
function decodeUriComponent(encodedURI) {
if (typeof encodedURI !== "string") {
throw new TypeError("Expected `encodedURI` to be of type `string`, got `" + typeof encodedURI + "`");
}
try {
return decodeURIComponent(encodedURI);
} catch {
return customDecodeURIComponent(encodedURI);
}
}
// node_modules/split-on-first/index.js
function splitOnFirst(string, separator) {
if (!(typeof string === "string" && typeof separator === "string")) {
throw new TypeError("Expected the arguments to be of type `string`");
}
if (string === "" || separator === "") {
return [];
}
const separatorIndex = string.indexOf(separator);
if (separatorIndex === -1) {
return [];
}
return [
string.slice(0, separatorIndex),
string.slice(separatorIndex + separator.length)
];
}
// node_modules/filter-obj/index.js
function includeKeys(object, predicate) {
const result = {};
if (Array.isArray(predicate)) {
for (const key of predicate) {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
if (descriptor?.enumerable) {
Object.defineProperty(result, key, descriptor);
}
}
} else {
for (const key of Reflect.ownKeys(object)) {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
if (descriptor.enumerable) {
const value = object[key];
if (predicate(key, value, object)) {
Object.defineProperty(result, key, descriptor);
}
}
}
}
return result;
}
// node_modules/query-string/base.js
var isNullOrUndefined = (value) => value === null || value === undefined;
var strictUriEncode = (string) => encodeURIComponent(string).replace(/[!'()*]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);
var encodeFragmentIdentifier = Symbol("encodeFragmentIdentifier");
function encoderForArrayFormat(options) {
switch (options.arrayFormat) {
case "index": {
return (key) => (result, value) => {
const index = result.length;
if (value === undefined || options.skipNull && value === null || options.skipEmptyString && value === "") {
return result;
}
if (value === null) {
return [
...result,
[encode(key, options), "[", index, "]"].join("")
];
}
return [
...result,
[encode(key, options), "[", encode(index, options), "]=", encode(value, options)].join("")
];
};
}
case "bracket": {
return (key) => (result, value) => {
if (value === undefined || options.skipNull && value === null || options.skipEmptyString && value === "") {
return result;
}
if (value === null) {
return [
...result,
[encode(key, options), "[]"].join("")
];
}
return [
...result,
[encode(key, options), "[]=", encode(value, options)].join("")
];
};
}
case "colon-list-separator": {
return (key) => (result, value) => {
if (value === undefined || options.skipNull && value === null || options.skipEmptyString && value === "") {
return result;
}
if (value === null) {
return [
...result,
[encode(key, options), ":list="].join("")
];
}
return [
...result,
[encode(key, options), ":list=", encode(value, options)].join("")
];
};
}
case "comma":
case "separator":
case "bracket-separator": {
const keyValueSep = options.arrayFormat === "bracket-separator" ? "[]=" : "=";
return (key) => (result, value) => {
if (value === undefined || options.skipNull && value === null || options.skipEmptyString && value === "") {
return result;
}
value = value === null ? "" : value;
if (result.length === 0) {
return [[encode(key, options), keyValueSep, encode(value, options)].join("")];
}
return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
};
}
default: {
return (key) => (result, value) => {
if (value === undefined || options.skipNull && value === null || options.skipEmptyString && value === "") {
return result;
}
if (value === null) {
return [
...result,
encode(key, options)
];
}
return [
...result,
[encode(key, options), "=", encode(value, options)].join("")
];
};
}
}
}
function parserForArrayFormat(options) {
let result;
switch (options.arrayFormat) {
case "index": {
return (key, value, accumulator) => {
result = /\[(\d*)]$/.exec(key);
key = key.replace(/\[\d*]$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === undefined) {
accumulator[key] = {};
}
accumulator[key][result[1]] = value;
};
}
case "bracket": {
return (key, value, accumulator) => {
result = /(\[])$/.exec(key);
key = key.replace(/\[]$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === undefined) {
accumulator[key] = [value];
return;
}
accumulator[key] = [...accumulator[key], value];
};
}
case "colon-list-separator": {
return (key, value, accumulator) => {
result = /(:list)$/.exec(key);
key = key.replace(/:list$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === undefined) {
accumulator[key] = [value];
return;
}
accumulator[key] = [...accumulator[key], value];
};
}
case "comma":
case "separator": {
return (key, value, accumulator) => {
const isArray = typeof value === "string" && value.includes(options.arrayFormatSeparator);
const isEncodedArray = typeof value === "string" && !isArray && decode2(value, options).includes(options.arrayFormatSeparator);
value = isEncodedArray ? decode2(value, options) : value;
const newValue = isArray || isEncodedArray ? value.split(options.arrayFormatSeparator).map((item) => decode2(item, options)) : value === null ? value : decode2(value, options);
accumulator[key] = newValue;
};
}
case "bracket-separator": {
return (key, value, accumulator) => {
const isArray = /(\[])$/.test(key);
key = key.replace(/\[]$/, "");
if (!isArray) {
accumulator[key] = value ? decode2(value, options) : value;
return;
}
const arrayValue = value === null ? [] : value.split(options.arrayFormatSeparator).map((item) => decode2(item, options));
if (accumulator[key] === undefined) {
accumulator[key] = arrayValue;
return;
}
accumulator[key] = [...accumulator[key], ...arrayValue];
};
}
default: {
return (key, value, accumulator) => {
if (accumulator[key] === undefined) {
accumulator[key] = value;
return;
}
accumulator[key] = [...[accumulator[key]].flat(), value];
};
}
}
}
function validateArrayFormatSeparator(value) {
if (typeof value !== "string" || value.length !== 1) {
throw new TypeError("arrayFormatSeparator must be single character string");
}
}
function encode(value, options) {
if (options.encode) {
return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
}
return value;
}
function decode2(value, options) {
if (options.decode) {
return decodeUriComponent(value);
}
return value;
}
function keysSorter(input) {
if (Array.isArray(input)) {
return input.sort();
}
if (typeof input === "object") {
return keysSorter(Object.keys(input)).sort((a, b) => Number(a) - Number(b)).map((key) => input[key]);
}
return input;
}
function removeHash(input) {
const hashStart = input.indexOf("#");
if (hashStart !== -1) {
input = input.slice(0, hashStart);
}
return input;
}
function getHash(url) {
let hash = "";
const hashStart = url.indexOf("#");
if (hashStart !== -1) {
hash = url.slice(hashStart);
}
return hash;
}
function parseValue(value, options) {
if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === "string" && value.trim() !== "")) {
value = Number(value);
} else if (options.parseBooleans && value !== null && (value.toLowerCase() === "true" || value.toLowerCase() === "false")) {
value = value.toLowerCase() === "true";
}
return value;
}
function extract(input) {
input = removeHash(input);
const queryStart = input.indexOf("?");
if (queryStart === -1) {
return "";
}
return input.slice(queryStart + 1);
}
function parse(query, options) {
options = {
decode: true,
sort: true,
arrayFormat: "none",
arrayFormatSeparator: ",",
parseNumbers: false,
parseBooleans: false,
...options
};
validateArrayFormatSeparator(options.arrayFormatSeparator);
const formatter = parserForArrayFormat(options);
const returnValue = Object.create(null);
if (typeof query !== "string") {
return returnValue;
}
query = query.trim().replace(/^[?#&]/, "");
if (!query) {
return returnValue;
}
for (const parameter of query.split("&")) {
if (parameter === "") {
continue;
}
const parameter_ = options.decode ? parameter.replace(/\+/g, " ") : parameter;
let [key, value] = splitOnFirst(parameter_, "=");
if (key === undefined) {
key = parameter_;
}
value = value === undefined ? null : ["comma", "separator", "bracket-separator"].includes(options.arrayFormat) ? value : decode2(value, options);
formatter(decode2(key, options), value, returnValue);
}
for (const [key, value] of Object.entries(returnValue)) {
if (typeof value === "object" && value !== null) {
for (const [key2, value2] of Object.entries(value)) {
value[key2] = parseValue(value2, options);
}
} else {
returnValue[key] = parseValue(value, options);
}
}
if (options.sort === false) {
return returnValue;
}
return (options.sort === true ? Object.keys(returnValue).sort() : Object.keys(returnValue).sort(options.sort)).reduce((result, key) => {
const value = returnValue[key];
result[key] = Boolean(value) && typeof value === "object" && !Array.isArray(value) ? keysSorter(value) : value;
return result;
}, Object.create(null));
}
function stringify(object, options) {
if (!object) {
return "";
}
options = {
encode: true,
strict: true,
arrayFormat: "none",
arrayFormatSeparator: ",",
...options
};
validateArrayFormatSeparator(options.arrayFormatSeparator);
const shouldFilter = (key) => options.skipNull && isNullOrUndefined(object[key]) || options.skipEmptyString && object[key] === "";
const formatter = encoderForArrayFormat(options);
const objectCopy = {};
for (const [key, value] of Object.entries(object)) {
if (!shouldFilter(key)) {
objectCopy[key] = value;
}
}
const keys = Object.keys(objectCopy);
if (options.sort !== false) {
keys.sort(options.sort);
}
return keys.map((key) => {
const value = object[key];
if (value === undefined) {
return "";
}
if (value === null) {
return encode(key, options);
}
if (Array.isArray(value)) {
if (value.length === 0 && options.arrayFormat === "bracket-separator") {
return encode(key, options) + "[]";
}
return value.reduce(formatter(key), []).join("&");
}
return encode(key, options) + "=" + encode(value, options);
}).filter((x) => x.length > 0).join("&");
}
function parseUrl(url, options) {
options = {
decode: true,
...options
};
let [url_, hash] = splitOnFirst(url, "#");
if (url_ === undefined) {
url_ = url;
}
return {
url: url_?.split("?")?.[0] ?? "",
query: parse(extract(url), options),
...options && options.parseFragmentIdentifier && hash ? { fragmentIdentifier: decode2(hash, options) } : {}
};
}
function stringifyUrl(object, options) {
options = {
encode: true,
strict: true,
[encodeFragmentIdentifier]: true,
...options
};
const url = removeHash(object.url).split("?")[0] || "";
const queryFromUrl = extract(object.url);
const query = {
...parse(queryFromUrl, { sort: false }),
...object.query
};
let queryString = stringify(query, options);
if (queryString) {
queryString = `?${queryString}`;
}
let hash = getHash(object.url);
if (object.fragmentIdentifier) {
const urlObjectForFragmentEncode = new URL(url);
urlObjectForFragmentEncode.hash = object.fragmentIdentifier;
hash = options[encodeFragmentIdentifier] ? urlObjectForFragmentEncode.hash : `#${object.fragmentIdentifier}`;
}
return `${url}${queryString}${hash}`;
}
function pick(input, filter, options) {
options = {
parseFragmentIdentifier: true,
[encodeFragmentIdentifier]: false,
...options
};
const { url, query, fragmentIdentifier } = parseUrl(input, options);
return stringifyUrl({
url,
query: includeKeys(query, filter),
fragmentIdentifier
}, options);
}
function exclude(input, filter, options) {
const exclusionFilter = Array.isArray(filter) ? (key) => !filter.includes(key) : (key, value) => !filter(key, value);
return pick(input, exclusionFilter, options);
}
// node_modules/query-string/index.js
var query_string_default = exports_base;
// src/modules/search.js
class SearchModule {
constructor(api) {
this.api = api;
}
all(query, options = {}) {
const defaultOptions = {
q: query,
limit: 20,
offset: 0,
facet: "model",
linked_partitioning: 1
};
return this.api.request("/search", {
...defaultOptions,
...options
});
}
tracks(query, options = {}) {
const defaultOptions = {
q: query,
limit: 20,
offset: 0,
facet: "genre",
linked_partitioning: 1
};
return this.api.request("/search/tracks", {
...defaultOptions,
...options
});
}
users(query, options = {}) {
const defaultOptions = {
q: query,
limit: 20,
offset: 0,
facet: "place",
linked_partitioning: 1
};
return this.api.request("/search/users", {
...defaultOptions,
...options
});
}
albums(query, options = {}) {
const defaultOptions = {
q: query,
limit: 20,
offset: 0,
facet: "genre",
linked_partitioning: 1
};
return this.api.request("/search/albums", {
...defaultOptions,
...options
});
}
playlists(query, options = {}) {
const defaultOptions = {
q: query,
limit: 20,
offset: 0,
facet: "genre",
linked_partitioning: 1
};
return this.api.request("/search/playlists_without_albums", {
...defaultOptions,
...options
});
}
byGenre(genre, options = {}) {
const defaultOptions = {
q: "*",
"filter.genre_or_tag": genre,
sort: "popular",
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request("/search/tracks", {
...defaultOptions,
...options
});
}
}
var search_default = SearchModule;
// src/modules/tracks.js
class TracksModule {
constructor(api) {
this.api = api;
}
getMultiple(ids) {
if (!Array.isArray(ids)) {
throw new Error("IDs must be an array");
}
return this.api.request("/tracks", {
ids: ids.join(",")
});
}
getComments(trackId, options = {}) {
const defaultOptions = {
threaded: 0,
limit: 200,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/tracks/${trackId}/comments`, {
...defaultOptions,
...options
});
}
getRelated(trackId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/tracks/${trackId}/related`, {
...defaultOptions,
...options
});
}
async getOembed(url, options = {}) {
try {
const defaultOptions = {
format: "json",
url,
maxwidth: 600,
auto_play: false,
...options
};
const trackInfo = await this.api.request(`/oembed`, defaultOptions, "https://soundcloud.com");
if (!trackInfo) {
throw new Error("Track oembed information not available");
}
return trackInfo;
} catch (error) {
throw new Error(`Failed to get oembed URL: ${error.message}`);
}
}
async getResolveUrl(url) {
try {
let normalizedUrl = url;
if (url.includes("on.soundcloud.com")) {
const data = await this.getOembed(url);
const iframeSrc = data.html.match(/src="([^"]+)"/)[1];
normalizedUrl = decodeURIComponent(iframeSrc.split("&url=")[1].split("&")[0]);
}
const trackInfo = await this.api.request(`/resolve`, { url: normalizedUrl });
if (!trackInfo) {
throw new Error("Track Resolve information not available");
}
return trackInfo;
} catch (error) {
throw new Error(`Failed to get Resolve URL: ${error.message}`);
}
}
async getStreamUrl(trackId) {
try {
const trackInfo = await this.api.request(`/tracks/${trackId}`);
if (!trackInfo || !trackInfo.media || !trackInfo.media.transcodings) {
throw new Error("Track streaming information not available");
}
const hlsTranscoding = trackInfo.media.transcodings.find((t) => t.format.protocol === "hls" && t.format.mime_type === "audio/mpeg");
if (!hlsTranscoding) {
throw new Error("HLS streaming not available for this track");
}
const mediaUrl = hlsTranscoding.url;
const mediaTranscodingId = mediaUrl.split("/").pop();
const streamInfo = await this.api.request(`/media/soundcloud:tracks:${trackId}/${mediaTranscodingId}/stream/hls`, { track_authorization: trackInfo.track_authorization });
return streamInfo.url;
} catch (error) {
throw new Error(`Failed to get stream URL: ${error.message}`);
}
}
}
var tracks_default = TracksModule;
// src/modules/users.js
class UsersModule {
constructor(api) {
this.api = api;
}
getUser(userId) {
return this.api.request(`/users/${userId}`);
}
getSpotlight(userId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/spotlight`, {
...defaultOptions,
...options
});
}
getFeaturedProfiles(userId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/featured-profiles`, {
...defaultOptions,
...options
});
}
getLikes(userId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/likes`, {
...defaultOptions,
...options
});
}
getFollowings(userId, options = {}) {
const defaultOptions = {
limit: 3,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/followings`, {
...defaultOptions,
...options
});
}
getRelatedArtists(userId, options = {}) {
const defaultOptions = {
creators_only: false,
page_size: 12,
limit: 12,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/relatedartists`, {
...defaultOptions,
...options
});
}
getComments(userId, options = {}) {
const defaultOptions = {
limit: 20,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/comments`, {
...defaultOptions,
...options
});
}
getStream(userId, options = {}) {
const defaultOptions = {
limit: 20,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/stream/users/${userId}`, {
...defaultOptions,
...options
});
}
getTopTracks(userId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/toptracks`, {
...defaultOptions,
...options
});
}
getTracks(userId, options = {}) {
const defaultOptions = {
limit: 20,
representation: ""
};
return this.api.request(`/users/${userId}/tracks`, {
...defaultOptions,
...options
});
}
getPlaylists(userId, options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/users/${userId}/playlists_without_albums`, {
...defaultOptions,
...options
});
}
getWebProfiles(userId) {
return this.api.request(`/users/soundcloud:users:${userId}/web-profiles`);
}
}
var users_default = UsersModule;
// src/modules/playlists.js
class PlaylistsModule {
constructor(api) {
this.api = api;
}
getPlaylist(playlistId, options = {}) {
const defaultOptions = {
representation: "full"
};
return this.api.request(`/playlists/${playlistId}`, {
...defaultOptions,
...options
});
}
getLikers(playlistId, options = {}) {
const defaultOptions = {
limit: 9,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/playlists/${playlistId}/likers`, {
...defaultOptions,
...options
});
}
getReposters(playlistId, options = {}) {
const defaultOptions = {
limit: 9,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/playlists/${playlistId}/reposters`, {
...defaultOptions,
...options
});
}
getByGenre(genre, options = {}) {
const defaultOptions = {
tag: genre,
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request("/playlists/discovery", {
...defaultOptions,
...options
});
}
}
var playlists_default = PlaylistsModule;
// src/modules/https.js
import https from "https";
function getData(url) {
return new Promise((resolve, reject) => {
https.get(url, (response) => {
let data = "";
response.on("data", (chunk) => {
data += chunk;
});
response.on("end", () => {
const jsonData = JSON.parse(data);
resolve({
statusCode: response.statusCode,
data: jsonData
});
});
}).on("error", (err) => {
reject(err);
});
});
}
async function httpsGet(url) {
try {
const result = await getData(url);
return result.data;
} catch (error) {
console.log("Error:", error.message);
return url;
}
}
// src/modules/media.js
class MediaModule {
constructor(api) {
this.api = api;
}
getStreamURL(mediaUrl, trackAuthorization, clientId) {
return `${mediaUrl}?client_id=${clientId}&track_authorization=${trackAuthorization}`;
}
async getPlaybackUrl(trackId) {
try {
return await this.getMediaUrl(null, trackId, "hls");
} catch (error) {
throw new Error(`Failed to get playback URL: ${error.message}`);
}
}
async getDownloadUrl(trackId) {
try {
return await this.getMediaUrl(null, trackId, "progressive");
} catch (error) {
throw new Error(`Failed to get download URL: ${error.message}`);
}
}
async getMediaUrl(url, trackId, protocol = "hls") {
try {
let trackData;
if (url) {
const track = await this.api.tracks.getResolveUrl(url);
if (!track) {
throw new Error("Track not found");
}
trackData = track;
} else {
const track = await this.api.tracks.getMultiple([trackId]);
if (!track || !track.length || !track[0]) {
throw new Error("Track not found");
}
trackData = track[0];
}
if (!trackData.media || !trackData.media.transcodings || !trackData.media.transcodings.length) {
throw new Error("No media transcodings available for this track");
}
const transcoding = trackData.media.transcodings.find((t) => t.format.protocol === protocol && t.format.mime_type === "audio/mpeg");
if (!transcoding) {
throw new Error(`No suitable ${protocol} media transcoding found`);
}
const mediaUrl = transcoding.url;
const urlRequest = this.getStreamURL(mediaUrl, trackData.track_authorization, this.api.clientId);
const response = await httpsGet(urlRequest);
return {
streamUrl: response.url,
...trackData
};
} catch (error) {
throw new Error(`Failed to get media URL: ${error.message}`);
}
}
}
var media_default = MediaModule;
// src/modules/discover.js
class DiscoverModule {
constructor(api) {
this.api = api;
}
getHomeContent(options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request("/mixed-selections", {
...defaultOptions,
...options
});
}
getRecentTracks(genre = "all genres", options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request(`/recent-tracks/${encodeURIComponent(genre)}`, {
...defaultOptions,
...options
});
}
getRecentTracksByCountry(options = {}) {
const defaultOptions = {
limit: 10,
offset: 0,
linked_partitioning: 1
};
return this.api.request("/recent-tracks/country", {
...defaultOptions,
...options
});
}
}
var discover_default = DiscoverModule;
// src/index.js
var instances = new Map;
function createOptionsHash(options) {
const normalizedOptions = {
clientId: options.clientId || null,
appVersion: options.appVersion || "1753870647",
appLocale: options.appLocale || "en",
autoFetchClientId: options.autoFetchClientId !== false
};
return JSON.stringify(normalizedOptions);
}
class SoundCloudAPI {
constructor(options = {}) {
const optionsHash = createOptionsHash(options);
if (instances.has(optionsHash)) {
return instances.get(optionsHash);
}
this.clientId = options.clientId;
this.baseURL = "https://api-v2.soundcloud.com";
this.appVersion = options.appVersion || "1753870647";
this.appLocale = options.appLocale || "en";
this.autoFetchClientId = options.autoFetchClientId !== false;
this.clientIdCache = {
value: this.clientId,
expirationTime: this.clientId ? Date.now() + 900000 : 0
};
this.clientIdPromise = null;
this.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
Accept: "application/json, text/javascript, */*; q=0.01",
Origin: "https://soundcloud.com",
Referer: "https://soundcloud.com/",
"Accept-Language": "en,vi;q=0.9,en-US;q=0.8"
};
this.search = new search_default(this);
this.tracks = new tracks_default(this);
this.users = new users_default(this);
this.playlists = new playlists_default(this);
this.media = new media_default(this);
this.discover = new discover_default(this);
if (this.autoFetchClientId) {}
instances.set(optionsHash, this);
}
static resetInstances() {
instances.clear();
}
static getInstance(options = {}) {
const optionsHash = createOptionsHash(options);
if (!instances.has(optionsHash)) {
new SoundCloudAPI(options);
}
return instances.get(optionsHash);
}
async _initClientId() {
try {
const newClientId = await this.getClientId();
} catch (error) {
console.error("[SOUNDCLOUD] Error initializing client ID:", error.message);
}
}
async getClientId() {
if (this.autoFetchClientId && (!this.clientId || Date.now() > this.clientIdCache.expirationTime)) {
try {
if (this.clientIdPromise) {
return await this.clientIdPromise;
}
this.clientIdPromise = this.fetchClientIdFromWeb();
const newClientId = await this.clientIdPromise;
if (newClientId) {
this.clientId = newClientId;
this.clientIdCache = {
value: newClientId,
expirationTime: Date.now() + 180000
};
}
this.clientIdPromise = null;
} catch (error) {
this.clientIdPromise = null;
console.error("[SOUNDCLOUD] Error fetching client ID:", error.message);
}
}
return this.clientId;
}
async fetchClientIdFromWeb() {
const webURL = "https://www.soundcloud.com/";
let script = "";
try {
const response = await fetch(webURL, {
headers: this.headers
});
if (!response.ok) {
throw new Error(`Failed to fetch SoundCloud page: ${response.status}`);
}
const html = await response.text();
const scriptUrlRegex = /(?!<script crossorigin src=")https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*\.js)(?=">)/g;
const urls = html.match(scriptUrlRegex) || [];
if (urls.length === 0) {
throw new Error("No script URLs found in SoundCloud page");
}
for (let i = urls.length - 1;i >= 0; i--) {
const scriptUrl = urls[i];
const scriptResponse = await fetch(scriptUrl, {
headers: this.headers
});
if (!scriptResponse.ok)
continue;
script = await scriptResponse.text();
if (script.includes(',client_id:"')) {
const match = script.match(/,client_id:"(\w+)"/);
if (match && match[1]) {
const clientId = match[1];
return clientId;
}
}
}
throw new Error("Client ID not found in any script");
} catch (error) {
console.error("[SOUNDCLOUD] Error in fetchClientIdFromWeb:", error.message);
throw error;
}
}
async request(endpoint, params = {}, customBaseURL) {
const clientId = await this.getClientId();
if (!clientId) {
throw new Error("SoundCloud API Error: No client ID available. Please provide a client ID or enable autoFetchClientId.");
}
const defaultParams = {
client_id: clientId,
app_version: this.appVersion,
app_locale: this.appLocale
};
const queryParams = query_string_default.stringify({
...defaultParams,
...params
});
const url = customBaseURL ? `${customBaseURL}${endpoint}?${queryParams}` : `${this.baseURL}${endpoint}?${queryParams}`;
try {
const response = await fetch(url, {
method: "GET",
headers: this.headers
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`SoundCloud API Error: ${response.status} - ${JSON.stringify(errorData)}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("SoundCloud API Error: Request was aborted");
} else if (error.name === "TypeError") {
throw new Error("SoundCloud API Error: Network error");
} else {
throw error;
}
}
}
}
var src_default = SoundCloudAPI;
export {
src_default as default
};