UNPKG

@manhgdev/soundcloud-web

Version:

JavaScript wrapper for SoundCloud API

1,162 lines (1,146 loc) 33.8 kB
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 };