UNPKG

pl2mp3

Version:

Watches Youtube Playlists and automatically downloads & converts them to mp3

264 lines (224 loc) 6.48 kB
const path = require("upath"); const fs = require("fs"); const ffprobe = require("node-ffprobe"); const ffmpeg = require("fluent-ffmpeg"); const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path; const ffprobePath = require("@ffprobe-installer/ffprobe").path; const ytdl = require("ytdl-core"); const sanitize = require("sanitize-filename"); let ffmetadata; let directory; let options; /** * download * @param {String} dir directory, file, file with regex, or youtube url * @param {*} op options */ async function download(dir, config, ytVideo) { if (typeof dir == undefined) throw "please specify an directory"; directory = dir; //set default value when calling as a module options = Object.assign({ duration: 180, full: true, metadata: true }, config); //parse time stamps to seconds options.duration = stringToSeconds(options.duration); //sets path variables for ffmpeg await checkffmpeg(); let videoPath //Download yt videos if (ytdl.validateURL(ytVideo)) { let title = await getVideoTitle(ytVideo); title = sanitize(title) //make sure there are no illeagale characters videoPath = path.join(directory, title + ".mp4") await downloadVideo(ytVideo, videoPath); } //Split track let seconds = await getFileLength(videoPath); let files = await splitTrack(videoPath, directory, Number(seconds)); //updating meta data if (options.metadata) { for (let file of files) { let ext = path.extname(directory); let name = path.removeExt(path.basename(directory), ext); await writeMusicMetadata(file, name); } } await deleteFile(videoPath) } /** * Sets the required ffmpeg path to all * packages that require it */ function checkffmpeg() { ffmpeg.setFfmpegPath(ffmpegPath); ffmpeg.setFfprobePath(ffprobePath); process.env.FFMPEG_PATH = ffmpegPath; ffprobe.FFPROBE_PATH = ffprobePath; ffmetadata = require("ffmetadata"); return ffmpegPath; } /** * Extracts one clip out of a longer mp3 file using the * seekInput and duration fuction. * Gets called when splitting up a larger file smaller ones * @param {String} input * @param {String} output * @param {Number} start * @param {Number} duration */ function segmentMp3(input, output, start, duration) { return new Promise((resolve, reject) => { ffmpeg(input).seekInput(start).duration(duration).save(output) .on("end", function (stdout, stderr) { resolve(); }).on('error', function (err, stdout, stderr) { reject('Cannot process video: ' + err.message); }) }); }; /** * Splits a mp3 file into multiple smaller sized parts and renames them * if part is shorter than 30 seconds it gets skipped * @param {String} baseDirectory * @param {String} outputDirectory * @param {String} name * @param {Number} duration */ async function splitTrack(videoPath, directory, duration) { let ext = path.extname(videoPath); let name = path.removeExt(path.basename(videoPath), ext); let files = [] //if you dont want seprate clips if (options.full) { let newName = path.join(directory, sanitize(name) + ".mp3") await segmentMp3(videoPath, newName, 0, duration); files.push(newName) return files; } let durationIndex = 0; while ((durationIndex + options.duration) <= (duration)) { let segmentName = path.join(directory, getSegmentName(name, durationIndex, durationIndex + options.duration)); await segmentMp3(videoPath, segmentName, durationIndex, options.duration); files.push(segmentName) durationIndex += options.duration; } if ((duration - durationIndex) >= 30) { let segmentName = path.join(directory, getSegmentName(name, durationIndex, duration)); await segmentMp3(videoPath, segmentName, durationIndex, (duration - durationIndex)); files.push(segmentName) } return files } /** * Generates Name for a Segment * @param {String} name * @param {Number} start * @param {Number} end */ function getSegmentName(name, start, end) { let ext = path.extname(name); name = path.removeExt(name, ext); return sanitize(`${name}_${secondsToTimeString(start)}-${secondsToTimeString(end)}.mp3`); } /** * Converts seconds into a ISO time string * @param {Number} seconds */ function secondsToTimeString(seconds) { return new Date(seconds * 1000).toISOString().substr(14, 5).replace(":", "."); } /** * Returns seconds from strings like 00:00 or 10000 * @param {String} timeString */ function stringToSeconds(timeString) { let seconds = 0; if (!isNaN(timeString)) seconds = timeString; else if (typeof timeString === "string" || timeString instanceof String) { if (timeString.indexOf(":") > -1) { let ms = timeString.split(":"); seconds = (+ms[0]) * 60 + (+ms[1]); } } else throw timeString + " is not a number, please only use formats like 123 or 1:30"; return Number(seconds); } /** * Returns the duration of a given * media file * @param {*} file */ function getFileLength(file) { return new Promise((resolve, reject) => { ffprobe(file, (err, probeData) => { if (err) reject(err); resolve(probeData.format.duration); }); }); } /** * Writes music meta data and cover to the given file * Also sets disc:1 to join all mp3 files into one copilation * @param {String} file * @param {String} playlistName * @param {String} cover */ function writeMusicMetadata(file, playlistName) { return new Promise((resolve, reject) => { let isodate = new Date(); let data = { disc: 1, album: playlistName, date: isodate }; ffmetadata.write(file, data, {}, function (err) { if (err) reject(err); resolve(); }); }); } /** * Promise wrap for deleting a file * @param {*} file */ function deleteFile(file) { return new Promise((resolve, reject) => { fs.unlink(file, function (error) { if (error) { reject(error); } resolve(); }); }); } /** * Downloads youtube video and saves it as mp4 * @param {String} url of the youtube video * @param {String} dir where the video should be placed */ async function downloadVideo(url, dir) { return new Promise((resolve, reject) => { ytdl(url) .pipe(fs.createWriteStream(dir)).on("finish", () => { resolve(dir); }); }); } /** * Gets the title of a video * @param {} url */ async function getVideoTitle(url) { return new Promise((resolve, reject) => { ytdl.getInfo(url, (err, info) => { if (err) throw reject(err); resolve(info.title); }) }); } module.exports.download = download; module.exports.stringToSeconds = stringToSeconds