UNPKG

@snwfdhmp/soundcloud-downloader

Version:

Download Soundcloud audio with Node.js, this is a forked version, the original script was written by @zackradisic and forked by @nasheedstation

313 lines (292 loc) 7.12 kB
/* eslint-disable camelcase */ import { AxiosInstance } from "axios"; import { handleRequestErrs, appendURL, extractIDFromPersonalizedTrackURL, } from "./util"; import STREAMING_PROTOCOLS from "./protocols"; import FORMATS from "./formats"; /** * A Soundcloud user */ export interface User { kind: string; avatar_url: string; city: string; comments_count: number; country_code: string; created_at: string; description: string; followers_count: number; followings_count: number; first_name: string; full_name: string; groups_count: number; id: number; last_name: string; permalink_url: string; uri: string; username: string; } /** * Details about the track */ export interface TrackInfo { kind: string; monetization_model: string; id: number; policy: string; comment_count?: number; full_duration?: number; downloadable?: false; created_at?: string; description?: string; media?: { transcodings: Transcoding[] }; title?: string; publisher_metadata?: string; duration?: number; has_downloads_left?: boolean; artwork_url?: string; public?: boolean; streamable?: true; tag_list?: string; genre?: string; reposts_count?: number; label_name?: string; state?: string; last_modified?: string; commentable?: boolean; uri?: string; download_count?: number; likes_count?: number; display_date?: string; user_id?: number; waveform_url?: string; permalink?: string; permalink_url?: string; user?: User; playback_count?: number; } /** * Details about a Set */ export interface SetInfo { duration: number; permalink_url: string; reposts_count: number; genre: string; permalink: string; purchase_url?: string; description?: string; uri: string; label_name?: string; tag_list: string; set_type: string; public: boolean; track_count: number; user_id: number; last_modified: string; license: string; tracks: TrackInfo[]; id: number; release_date?: string; display_date: string; sharing: string; secret_token?: string; created_at: string; likes_count: number; kind: string; purchase_title?: string; managed_by_feeds: boolean; artwork_url?: string; is_album: boolean; user: User; published_at: string; embeddable_by: string; } /** * Represents an audio link to a Soundcloud Track */ export interface Transcoding { url: string; preset: string; snipped: boolean; format: { protocol: STREAMING_PROTOCOLS; mime_type: FORMATS }; } const getTrackInfoBase = async ( clientID: string, axiosRef: AxiosInstance, ids: number[], playlistID?: number, playlistSecretToken?: string ): Promise<TrackInfo[]> => { let url = appendURL( "https://api-v2.soundcloud.com/tracks", "ids", ids.join(","), "client_id", clientID ); if (playlistID && playlistSecretToken) { url = appendURL( url, "playlistId", "" + playlistID, "playlistSecretToken", playlistSecretToken ); } try { const { data } = await axiosRef.get(url); return data as TrackInfo[]; } catch (err) { throw handleRequestErrs(err); } }; /** @internal */ export const getInfoBase = async <T extends TrackInfo | SetInfo>( url: string, clientID: string, axiosRef: AxiosInstance ): Promise<T> => { try { const res = await axiosRef.get( appendURL( "https://api-v2.soundcloud.com/resolve", "url", url, "client_id", clientID ), { withCredentials: true, } ); return res.data as T; } catch (err) { throw handleRequestErrs(err); } }; /** @internal */ const getSetInfoBase = async ( url: string, clientID: string, axiosRef: AxiosInstance ): Promise<SetInfo> => { const setInfo = await getInfoBase<SetInfo>(url, clientID, axiosRef); const temp = [...setInfo.tracks].map((track) => track.id); const playlistID = setInfo.id; const playlistSecretToken = setInfo.secret_token; const incompleteTracks = setInfo.tracks.filter((track) => !track.title); if (incompleteTracks.length === 0) { return setInfo; } const completeTracks = setInfo.tracks.filter((track) => track.title); const ids = incompleteTracks.map((t) => t.id); if (ids.length > 50) { const splitIds: number[][] = []; for (let x = 0; x <= Math.floor(ids.length / 50); x++) { splitIds.push([]); } for (let x = 0; x < ids.length; x++) { const i = Math.floor(x / 50); splitIds[i].push(ids[x]); } const promises = splitIds.map( async (ids) => await getTrackInfoByID( clientID, axiosRef, ids, playlistID, playlistSecretToken ) ); const info = await Promise.all(promises); setInfo.tracks = completeTracks.concat(...info); setInfo.tracks = sortTracks(setInfo.tracks, temp); return setInfo; } const info = await getTrackInfoByID( clientID, axiosRef, ids, playlistID, playlistSecretToken ); setInfo.tracks = completeTracks.concat(info); setInfo.tracks = sortTracks(setInfo.tracks, temp); return setInfo; }; /** @internal */ const sortTracks = (tracks: TrackInfo[], ids: number[]): TrackInfo[] => { for (let i = 0; i < ids.length; i++) { if (tracks[i].id !== ids[i]) { for (let j = 0; j < tracks.length; j++) { if (tracks[j].id === ids[i]) { const temp = tracks[i]; tracks[i] = tracks[j]; tracks[j] = temp; } } } } return tracks; }; /** @internal */ const getInfo = async ( url: string, clientID: string, axiosInstance: AxiosInstance ): Promise<TrackInfo> => { let data; if ( url.includes("https://soundcloud.com/discover/sets/personalized-tracks::") ) { const idString = extractIDFromPersonalizedTrackURL(url); if (!idString) throw new Error("Could not parse track ID from given URL: " + url); let id: number; try { id = parseInt(idString); } catch (err) { throw new Error("Could not parse track ID from given URL: " + url); } data = (await getTrackInfoByID(clientID, axiosInstance, [id]))[0]; if (!data) throw new Error("Could not find track with ID: " + id); } else { data = await getInfoBase<TrackInfo>(url, clientID, axiosInstance); } if (!data.media) throw new Error("The given URL does not link to a Soundcloud track"); return data; }; /** @internal */ export const getSetInfo = async ( url: string, clientID: string, axiosInstance: AxiosInstance ): Promise<SetInfo> => { const data = await getSetInfoBase(url, clientID, axiosInstance); if (!data.tracks) throw new Error("The given URL does not link to a Soundcloud set"); return data; }; /** @intenral */ export const getTrackInfoByID = async ( clientID: string, axiosInstance: AxiosInstance, ids: number[], playlistID?: number, playlistSecretToken?: string ): Promise<TrackInfo[]> => { return await getTrackInfoBase( clientID, axiosInstance, ids, playlistID, playlistSecretToken ); }; export default getInfo;