@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
text/typescript
/* 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;