@manhgdev/soundcloud-web
Version:
JavaScript wrapper for SoundCloud API
242 lines (203 loc) • 7.74 kB
JavaScript
import queryString from "query-string";
import SearchModule from "./modules/search.js";
import TracksModule from "./modules/tracks.js";
import UsersModule from "./modules/users.js";
import PlaylistsModule from "./modules/playlists.js";
import MediaModule from "./modules/media.js";
import DiscoverModule from "./modules/discover.js";
// Map lưu trữ các instance theo options hash
const instances = new Map();
// Hàm tạo hash key từ options
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);
// Nếu đã có instance với options này, trả về instance đó
if (instances.has(optionsHash)) {
return instances.get(optionsHash);
}
// Chỉ sử dụng client ID được cung cấp, không có giá trị mặc định
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, // Nếu không có clientId, đặt expirationTime = 0 để buộc fetch
};
this.clientIdPromise = null; // Track ongoing client ID fetch requests
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 SearchModule(this);
this.tracks = new TracksModule(this);
this.users = new UsersModule(this);
this.playlists = new PlaylistsModule(this);
this.media = new MediaModule(this);
this.discover = new DiscoverModule(this);
// Luôn fetch client ID khi autoFetchClientId là true
if (this.autoFetchClientId) {
// Không await ở đây để tránh làm constructor trở thành async
// this._initClientId();
}
// Lưu instance này vào map theo options hash
instances.set(optionsHash, this);
}
// Reset tất cả instances (chủ yếu để phục vụ testing)
static resetInstances() {
instances.clear();
}
// Phương thức tĩnh để lấy instance theo options
static getInstance(options = {}) {
const optionsHash = createOptionsHash(options);
if (!instances.has(optionsHash)) {
new SoundCloudAPI(options); // Constructor sẽ tự thêm vào map
}
return instances.get(optionsHash);
}
async _initClientId() {
try {
const newClientId = await this.getClientId(); // Use getClientId to avoid duplication
} catch (error) {
console.error(
"[SOUNDCLOUD] Error initializing client ID:",
error.message,
);
}
}
async getClientId() {
// Nếu không có clientId hoặc cache đã hết hạn, fetch mới
if (
this.autoFetchClientId &&
(!this.clientId || Date.now() > this.clientIdCache.expirationTime)
) {
try {
// If there's already a fetch in progress, return that promise
if (this.clientIdPromise) {
return await this.clientIdPromise;
}
// Create a new promise for fetching the client ID
this.clientIdPromise = this.fetchClientIdFromWeb();
const newClientId = await this.clientIdPromise;
if (newClientId) {
this.clientId = newClientId;
this.clientIdCache = {
value: newClientId,
expirationTime: Date.now() + 180000, // 3 minutes
};
}
// Clear the promise after it's resolved
this.clientIdPromise = null;
} catch (error) {
// Clear the promise if there's an 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 {
// console.log("[SOUNDCLOUD] Fetching client ID from web...");
// Fetch the main SoundCloud page
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();
// Extract script URLs
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");
}
// Find the script containing the client ID
for (let i = urls.length - 1; i >= 0; i--) {
const scriptUrl = urls[i];
// console.log(`[SOUNDCLOUD] Checking script: ${scriptUrl}`);
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];
// console.log(`[SOUNDCLOUD] Found client ID: ${clientId}`);
return clientId;
}
}
}
// Nếu không tìm thấy client ID trong bất kỳ script nào
throw new Error("Client ID not found in any script");
} catch (error) {
console.error(
"[SOUNDCLOUD] Error in fetchClientIdFromWeb:",
error.message,
);
throw error; // Ném lại lỗi để caller xử lý
}
}
async request(endpoint, params = {}, customBaseURL) {
// Get the latest client ID before making the request
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 = queryString.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;
}
}
}
}
export default SoundCloudAPI;