add-music-to-video
Version:
Add background music to my videos very easily
367 lines (355 loc) • 12.7 kB
JavaScript
;
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
});