UNPKG

@nasheedstation/soundcloud-downloader

Version:

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

234 lines (213 loc) 6.69 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 = [] 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