@zibot/scdl
Version:
Soucloud download
154 lines (132 loc) • 4.48 kB
JavaScript
const axios = require("axios");
const m3u8stream = require("m3u8stream");
class SoundCloud {
constructor(options = {}) {
const defaultOptions = { init: true, apiBaseUrl: "https://api-v2.soundcloud.com" };
options = { ...defaultOptions, ...options };
this.clientId = null;
this.apiBaseUrl = options.apiBaseUrl;
if (options.init) this.init();
}
// Auto-fetch Client ID
async init() {
const clientIdRegex = /client_id=(:?[\w\d]{32})/;
const soundCloudDom = (await axios.get("https://soundcloud.com")).data;
const scriptUrls = (soundCloudDom.match(/<script crossorigin src="(.*?)"><\/script>/g) || [])
.map((tag) => tag.match(/src="(.*?)"/)?.[1])
.filter(Boolean);
for (const url of scriptUrls) {
const response = await axios.get(url);
const match = response.data.match(clientIdRegex);
if (match) {
this.clientId = match[1];
return;
}
}
throw new Error("Failed to fetch client ID");
}
// Search SoundCloud
async searchTracks({ query, limit = 30, offset = 0, type = "all" }) {
const path = type === "all" ? "" : `/${type}`;
const url = `${this.apiBaseUrl}/search${path}?q=${encodeURIComponent(
query,
)}&limit=${limit}&offset=${offset}&access=playable&client_id=${this.clientId}`;
try {
const { data } = await axios.get(url);
if (!data || !data?.collection?.length) {
return [];
}
return data.collection.filter((track) => {
if (!track.permalink_url || !track.title || !track.duration) return false;
return true;
});
} catch (error) {
console.error("Search error:", error.message || error);
throw new Error("Search failed");
}
}
// Get track details
async getTrackDetails(trackUrl) {
try {
return await this.fetchItem(trackUrl);
} catch (error) {
throw new Error("Invalid track URL");
}
}
// Get playlist details
async getPlaylistDetails(playlistUrl) {
try {
const playlist = await this.fetchItem(playlistUrl);
const { tracks } = playlist;
const loadedTracks = tracks.filter((track) => track.title);
const unloadedTrackIds = tracks.filter((track) => !track.title).map((track) => track.id);
if (unloadedTrackIds.length > 0) {
const moreTracks = await this.fetchTracksByIds(unloadedTrackIds);
playlist.tracks = loadedTracks.concat(moreTracks);
}
return playlist;
} catch (error) {
throw new Error("Invalid playlist URL");
}
}
// Download track stream
async downloadTrack(trackUrl, options = {}) {
try {
const track = await this.getTrackDetails(trackUrl);
const transcoding = track?.media?.transcodings?.find((t) => t.format.protocol === "hls");
if (!transcoding) throw new Error("No valid HLS stream found");
const m3u8Url = await this.getStreamUrl(transcoding.url);
return m3u8stream(m3u8Url, options);
} catch (e) {
console.error("Failed to download track");
return null;
}
}
// Fetch single item (track/playlist/user)
async fetchItem(itemUrl) {
const url = `${this.apiBaseUrl}/resolve?url=${itemUrl}&client_id=${this.clientId}`;
try {
const { data } = await axios.get(url);
return data;
} catch (error) {
throw new Error("Failed to fetch item details");
}
}
// Fetch multiple tracks by their IDs
async fetchTracksByIds(trackIds) {
const chunkSize = 50; // Adjust chunk size as needed based on API limits
const chunks = [];
for (let i = 0; i < trackIds.length; i += chunkSize) {
chunks.push(trackIds.slice(i, i + chunkSize));
}
try {
const results = await Promise.all(
chunks.map(async (chunk) => {
const ids = chunk.join(",");
const url = `${this.apiBaseUrl}/tracks?ids=${ids}&client_id=${this.clientId}`;
const { data } = await axios.get(url);
return data;
}),
);
// Combine results from all chunks
return results.flat();
} catch (error) {
console.error("Failed to fetch tracks by IDs:", {
clientId: this.clientId,
error: error.response?.data || error.message,
});
throw new Error("Failed to fetch tracks by IDs");
}
}
// Get HLS stream URL
async getStreamUrl(transcodingUrl) {
const url = `${transcodingUrl}?client_id=${this.clientId}`;
try {
const { data } = await axios.get(url);
return data.url;
} catch (error) {
throw new Error("Failed to fetch stream URL");
}
}
}
module.exports = SoundCloud;