UNPKG

add-music-to-video

Version:

Add background music to my videos very easily

367 lines (355 loc) 12.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/run.ts var run_exports = {}; __export(run_exports, { default: () => run_default, run: () => run }); module.exports = __toCommonJS(run_exports); var import_fs4 = __toESM(require("fs")); var import_fast_glob = __toESM(require("fast-glob")); var import_path5 = __toESM(require("path")); var import_ffmpeg = __toESM(require("ffmpeg")); var import_ffmpeg_static = __toESM(require("ffmpeg-static")); var import_child_process4 = require("child_process"); // src/utils/assert.utils.ts function invariant(condition, message) { if (condition) { return; } const prefix = "Invariant failed"; const providedMessage = typeof message === "function" ? message() : message; const value = providedMessage ? `${prefix}: ${providedMessage}` : prefix; const error = new Error(value); error.name = "AssertError"; throw error; } var assert = (condition, message) => { invariant( condition, typeof message === "string" ? `AssertError: ${message}` : message || `AssertError: condition must be truthy` ); const _condition = condition; return _condition; }; // src/core/video/index.ts var import_dist = __toESM(require("cli-file-select/dist")); var import_prompts2 = __toESM(require("prompts")); // src/core/video/youtube.video.ts var import_prompts = __toESM(require("prompts")); var import_fs = __toESM(require("fs")); var import_path = __toESM(require("path")); var import_ytdl_core = __toESM(require("ytdl-core")); var downloadFromYoutube = async (url) => { return new Promise((resolve, reject) => { const file = import_path.default.join(__dirname, "random-video.mp4"); const downloadStream = (0, import_ytdl_core.default)(url).pipe(import_fs.default.createWriteStream(file)); downloadStream.on("error", (error) => { console.error(error); downloadStream.destroy && downloadStream.destroy(); downloadStream.close && downloadStream.close(); reject(); }); downloadStream.on("close", () => { resolve(file); }); }); }; var downloadYoutubeVideo = async ({ getYoutubeUrl = () => (0, import_prompts.default)({ type: "text", name: "url", message: "Enter a valid youtube URL" }).then((res) => res.url), download = downloadFromYoutube } = {}) => { const url = await getYoutubeUrl(); console.log("Downloading:", url); return download(url); }; // src/core/video/index.ts var channels = [ { title: "Local File System", value: "local" }, { title: "Youtube", value: "youtube" } ]; var selectChannel = async () => { return (0, import_prompts2.default)({ type: "select", name: "channel", message: "Where can your video be found?", choices: channels }).then((res) => res.channel); }; var selectVideoFile = async ({ select = selectChannel, getLocalVideo = import_dist.default, getYoutubeVideo = downloadYoutubeVideo } = {}) => { const channel = await select(); if (channel === "local") { return getLocalVideo(); } else { return getYoutubeVideo(); } }; // src/core/music/index.ts var import_prompts4 = __toESM(require("prompts")); // src/core/music/random.music.ts var import_fs2 = __toESM(require("fs")); var import_path2 = __toESM(require("path")); var import_get_random_music = __toESM(require("get-random-music")); var import_child_process = require("child_process"); var expandMp3 = async (audioPath, duration) => { console.log("Expanding Music to fit video of", duration, "seconds"); const audioRepeatCount = Math.max(Math.ceil(duration / 15), 1); const repeatInputFile = import_path2.default.join(__dirname, "audio-repeat.txt").replace(/\\/g, "\\\\"); import_fs2.default.writeFileSync( repeatInputFile, `file '${import_path2.default.relative(__dirname, audioPath)}' `.repeat(audioRepeatCount), "utf8" ); const audioDir = import_path2.default.dirname(audioPath); const audioFilename = audioPath.replace(audioDir, "").replace(import_path2.default.delimiter, "").replace(/\.\w+$/, ""); const audioRepeatedFilePath = import_path2.default.join( import_path2.default.dirname(audioPath), `${audioFilename}_repeated_${duration}s.mp3` ); (0, import_child_process.execSync)( `ffmpeg -y -t ${duration} -f concat -i ${repeatInputFile} -c copy ${audioRepeatedFilePath}`, { cwd: import_path2.default.dirname(repeatInputFile) } ); return audioRepeatedFilePath; }; var downloadRandomMusic = async (duration, { getStream = (outFilePath) => (0, import_get_random_music.default)().then((res) => { res.data.pipe(import_fs2.default.createWriteStream(outFilePath)); return res.data; }) } = {}) => { console.log("Downloading Random Music"); const musicFilePath = import_path2.default.join(__dirname, "random-music.mp3"); const downloadStream = await getStream(musicFilePath); await new Promise((resolve, reject) => { downloadStream.on("error", (error) => { console.error(error); downloadStream.destroy && downloadStream.destroy(); downloadStream.close && downloadStream.close(); reject(); }); downloadStream.on("close", () => { resolve(musicFilePath); }); }); return expandMp3(musicFilePath, duration); }; // src/core/music/youtube.music.ts var import_fs3 = __toESM(require("fs")); var import_path3 = __toESM(require("path")); var import_prompts3 = __toESM(require("prompts")); var import_ytdl_core2 = __toESM(require("ytdl-core")); var import_child_process2 = require("child_process"); var downloadFromYoutube2 = async (url) => { return new Promise((resolve, reject) => { const file = import_path3.default.join(__dirname, "random-music.m4a"); const downloadStream = (0, import_ytdl_core2.default)(url, { filter: "audioonly" }).pipe(import_fs3.default.createWriteStream(file)); downloadStream.on("error", (error) => { console.error(error); downloadStream.destroy && downloadStream.destroy(); downloadStream.close && downloadStream.close(); reject(); }); downloadStream.on("close", () => { resolve(file); }); }); }; var convertM4aToMp3 = async (m4aFile) => { const mp3File = import_path3.default.join(__dirname, "random-music.mp3"); (0, import_child_process2.execSync)( `ffmpeg -i "${m4aFile}" -c:v copy -c:a libmp3lame -q:a 4 "${mp3File}"` ); return mp3File; }; var truncateMp3 = async (mp3File, duration) => { const truncatedMp3File = import_path3.default.join(__dirname, "truncated-random-music.mp3"); (0, import_child_process2.execSync)(`ffmpeg -i "${mp3File}" -ss ${0} -to ${duration} "${truncatedMp3File}"`); return truncatedMp3File; }; var downloadYoutubeMusic = async (duration, { getYoutubeUrl = () => (0, import_prompts3.default)({ type: "text", name: "url", message: "Enter a valid youtube URL" }).then((res) => res.url), download = downloadFromYoutube2, convert = convertM4aToMp3, truncate = truncateMp3 } = {}) => { const url = await getYoutubeUrl(); console.log("Downloading:", url); const m4aFile = await download(url); const mp3File = await convert(m4aFile); return truncate(mp3File, duration); }; // src/core/music/local.music.ts var import_child_process3 = require("child_process"); var import_cli_file_select = __toESM(require("cli-file-select")); var import_path4 = __toESM(require("path")); var truncateMp32 = async (mp3File, duration) => { const truncatedMp3File = import_path4.default.join(__dirname, "truncated-random-music.mp3"); (0, import_child_process3.execSync)( `ffmpeg -i "${mp3File}" -ss ${0} -to ${duration} "${truncatedMp3File}"` ); return truncatedMp3File; }; var downloadLocalMusic = async (duration, { truncate = truncateMp32, getFilePath = import_cli_file_select.default } = {}) => { const file = await getFilePath(); return truncate(file, duration); }; // src/core/music/index.ts var channels2 = [ { title: "Local File System", value: "local" }, { title: "Youtube", value: "youtube" }, { title: "Random", value: "random" } ]; var selectChannel2 = async () => { return (0, import_prompts4.default)({ type: "select", name: "channel", message: "Where can your music be found?", choices: channels2 }).then((res) => res.channel); }; var downloadMusic = async (duration, options = {}, { select = selectChannel2, getLocalMusic = downloadLocalMusic, getYoutubeMusic = downloadYoutubeMusic, getRandomMusic: getRandomMusic2 = downloadRandomMusic } = {}) => { const channel = options.musicSource || await select(); if (channel === "local") { if (!options.localMusicPath) { throw new Error( "Local music path is required when using local music source" ); } return getLocalMusic(duration, { getFilePath: async () => options.localMusicPath }); } else if (channel === "youtube") { if (!options.youtubeUrl) { throw new Error( "YouTube URL is required when using YouTube music source" ); } return getYoutubeMusic(duration, { getYoutubeUrl: async () => options.youtubeUrl }); } else { return getRandomMusic2(duration); } }; // src/run.ts import_ffmpeg.default.bin = import_ffmpeg_static.default; var cleanupFiles = async () => { console.log("Cleaning up files"); const files = await (0, import_fast_glob.default)(["*.mp3", "*.txt", "*.m4a"], { cwd: __dirname }); for (let file of files) { import_fs4.default.unlinkSync(import_path5.default.join(__dirname, file)); console.log("Removed", import_path5.default.join(__dirname, file)); } }; var addMusicToVideo = async (musicFilePath, videoFilePath) => { const outDir = import_path5.default.dirname(videoFilePath); const filename = videoFilePath.replace(outDir, "").replace(import_path5.default.delimiter, "").replace(/\.\w+$/, ""); const outVideoFilePath = import_path5.default.join( outDir, `${filename}_with_music.mp4`.replace(/ /g, "_") ); (0, import_child_process4.execSync)( `ffmpeg -y -i "${videoFilePath}" -i "${musicFilePath}" -c:v copy -map 0:v:0 -map 1:a:0 -f mp4 "${outVideoFilePath}"` ); return outVideoFilePath; }; var run = async (options, { getInputVideoFilePath = selectVideoFile, getVideoDuration = async (filePath) => { const video = await new import_ffmpeg.default(filePath); return assert(video.metadata.duration).seconds || 60; }, getMusicFilePath = downloadMusic, cleanup = cleanupFiles, join = addMusicToVideo } = {}) => { const inVideoFilePath = options.inputVideoFilePath || await getInputVideoFilePath(); const allowedFormats = [".mp4"]; if (!allowedFormats.some((format) => inVideoFilePath.endsWith(format))) { console.error("Input file not matching allowedFormats", { allowedFormats, file: inVideoFilePath }); process.exit(0); } console.log("Input:", inVideoFilePath); const duration = await getVideoDuration(inVideoFilePath); console.log("Duration:", duration); const audioFilePath = options.outputPath || await getMusicFilePath(duration, options); const outVideoFilePath = await join(audioFilePath, inVideoFilePath); await cleanup(); console.log("Output:", outVideoFilePath); return outVideoFilePath; }; var run_default = run; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { run });