spotify-dl
Version:
Spotify Songs, Playlist & Album Downloader
173 lines (155 loc) • 4.78 kB
JavaScript
import fs from 'fs';
import ytdl from '@distube/ytdl-core';
import ffmpeg from 'fluent-ffmpeg';
import { SponsorBlock } from 'sponsorblock-api';
import Config from '../config.js';
import Constants from '../util/constants.js';
import {
logStart,
updateSpinner,
logInfo,
logSuccess,
} from '../util/log-helper.js';
import { cliInputs } from './setup.js';
const { youtubeDLConfig, isTTY } = Config;
const sponsorBlock = new SponsorBlock(1234);
const {
SPONSOR_BLOCK: {
CATEGORIES: {
SPONSOR,
INTRO,
OUTRO,
INTERACTION,
SELF_PROMO,
MUSIC_OFF_TOPIC,
},
},
FFMPEG: { ASET, TIMEOUT_MINUTES },
} = Constants;
const sponsorComplexFilter = async link => {
const videoID = new URLSearchParams(new URL(link).search).get('v');
let segments = [];
let complexFilter = null;
try {
segments = (
await sponsorBlock.getSegments(
videoID,
SPONSOR,
INTRO,
OUTRO,
INTERACTION,
SELF_PROMO,
MUSIC_OFF_TOPIC
)
)
.sort((a, b) => a.startTime - b.startTime)
.reduce((acc, { startTime, endTime }) => {
const previousSegment = acc[acc.length - 1];
// if segments overlap merge
if (previousSegment && previousSegment.endTime > startTime) {
acc[acc.length - 1].endTime = endTime;
} else {
acc.push({ startTime, endTime });
}
return acc;
}, []);
// we have to catch as it throws if none found
} catch (_) { /* empty */ }
const segmentLength = segments.length;
if (segmentLength) {
// 0 -> start1 , end1 -> start2, end2
// we want everything but the segment `start -> end`
complexFilter = segments.map((segment, i) => {
const startString = `start=${i ? segments[i - 1].endTime : 0}`;
const endString = `:end=${segment.startTime}`;
return `[0:a]atrim=${startString}${endString},${ASET}[${i}a];`;
});
complexFilter.push(
`[0:a]atrim=start=${segments[segmentLength - 1].endTime}` +
`,${ASET}[${segmentLength}a];`
);
complexFilter.push(
`${complexFilter.map((_, i) => `[${i}a]`).join('')}` +
`concat=n=${segmentLength + 1}:v=0:a=1[outa]`
);
complexFilter = complexFilter.join('\n');
}
return complexFilter;
};
const progressFunction = (_, downloaded, total) => {
const downloadedMb = (downloaded / 1024 / 1024).toFixed(2);
const toBeDownloadedMb = (total / 1024 / 1024).toFixed(2);
const downloadText = `Downloaded ${downloadedMb}/${toBeDownloadedMb} MB`;
if (isTTY || downloadedMb % 1 == 0 || toBeDownloadedMb == downloadedMb) {
updateSpinner(downloadText);
}
};
const getYoutubeDLConfig = () => {
const { cookieFile } = cliInputs();
if (fs.existsSync(cookieFile)) {
const cookieFileContents = fs
.readFileSync(cookieFile, 'utf-8')
.split('\n')
.reduce((cookie, line) => {
const segments = line.split(/[\t]+|[ ]+/);
if (segments.length == 7) {
cookie += `${segments[5]}=${segments[6]}; `;
}
return cookie;
}, '')
.trim();
youtubeDLConfig.requestOptions = {
headers: {
Cookie: cookieFileContents,
},
};
}
return youtubeDLConfig;
};
/**
* This function downloads the given youtube video
* in best audio format as mp3 file
*
* @param {array} youtubeLinks array of links of youtube videos
* @param {string} output path/name of file to be downloaded(songname.mp3)
*/
const downloader = async (youtubeLinks, output) => {
const { outputFileType } = cliInputs();
let attemptCount = 0;
let downloadSuccess = false;
while (attemptCount < youtubeLinks.length && !downloadSuccess) {
const link = youtubeLinks[attemptCount];
logStart(`Trying youtube link (${link}) ${attemptCount + 1}...`);
const complexFilter = await sponsorComplexFilter(link);
const doDownload = (resolve, reject) => {
const download = ytdl(link, getYoutubeDLConfig());
download.on('progress', progressFunction);
const ffmpegCommand = ffmpeg({ timeout: TIMEOUT_MINUTES * 60 });
if (complexFilter) {
ffmpegCommand.complexFilter(complexFilter).map('[outa]');
}
ffmpegCommand
.on('error', e => {
reject(e);
})
.on('end', () => {
resolve();
})
.input(download)
.audioBitrate(256)
.save(`${output}`)
.format(outputFileType);
};
try {
await new Promise(doDownload);
downloadSuccess = true;
logSuccess(`Download completed (${output}).`);
} catch (e) {
logInfo(e.message);
logInfo('Youtube error retrying download');
attemptCount++;
}
}
return downloadSuccess;
};
export default downloader;