Discord-Media-Compressor-su.../index.js

423 lines
No EOL
13 KiB
JavaScript

const fs = require('fs')
const { exec, execSync } = require('child_process')
const cliProgress = require('cli-progress')
const term = require("terminal-kit").terminal
/**
* TODO : Adapt audio quality as well to accomodate long videos(Currently 5m is too much)
* FIND A WAY TO COMPILE THIS:..
*/
//Parse file inputs (Drag n drop or arguments)
inputList = process.argv.slice(2)
input = inputList[0]
let file, fileType, bar1
//if launched without params
if (!input) {
startMenu()
}
else {
file = input.split("\\")
file = file[file.length - 1]
fileType = file.split(".")[1]
if (!fs.existsSync(input)) {
term.italic(`${input}`).bold.red(" <- Path or File doesn't exist\n")
term.grey("press enter to exit...")
term.inputField(function () { process.exit() })
}
else {
bar1 = new cliProgress.SingleBar({
synchronousUpdate: true,
align: "left",
hideCursor: true
}, cliProgress.Presets.shades_classic)
if (fileType == "jpg" || fileType == "JPG" || fileType == "png" || fileType == "PNG" || fileType == "webp") {
encodePicture(input, file.split(".")[0])
}
else if (fileType == "webm" || fileType == "mp4" || fileType == "mov" || fileType == "mkv" || fileType == "avi") {
encodeVideo(input, file.split(".")[0])
}
else if ("ogg" || "mp3" || "aiff" || "wav" || "flac") {
encodeAudio(input, file.split(".")[0])
}
else {
term.italic(`${file}`).bold.red(` <- Unsupported format\n`)
term.grey("press enter to exit...")
term.inputField(function () { process.exit() })
}
}
}
async function encodeVideo(path, out) {
//create progress bar
const [command, presetName, duration, isTwoPass] = await constructVideoCommand(path, out)
bar1.start(duration, 0, { speed: "N/A" })
let isPastHalf = false
let encoder = exec(command)
encoder.stderr.on("data", (chunk) => {
currentTime = chunk.split("time=")[1]?.split(" ")[0]
if (currentTime) {
const arr = currentTime.split(":") // splitting the string by colon
let seconds = Number.parseFloat(arr[0] * 3600 + arr[1] * 60 + (+arr[2])) // converting to s
console.clear()
//If 2nd pass add that portion in
console.log(`Encoding ${out}.webm with "${presetName}" preset...`)
if (isTwoPass) {
if (seconds / 2 >= (duration - 0.2) / 2) isPastHalf = true
isPastHalf ? bar1.update(Math.round(seconds * 50) / 100 + (duration / 2)) : bar1.update(Math.round(seconds * 50) / 100)
}
else {
bar1.update(Math.round(seconds * 100) / 100)
}
}
})
encoder.on("close", () => {
console.clear()
bar1.stop()
fs.rm("ffmpeg2pass-0.log", (error) => { error })
term.bold.green("Finished!\n")
term.grey("press enter to exit...\n")
term.inputField(() => { process.exit() })
})
}
async function encodeAudio(path, out) {
let [duration, resolution] = await getDurationAndResolution(path)
const bitrateLimit = Math.round(62000 / duration)
bar1.start(duration, 0, { speed: "N/A" })
const encoder = exec(`ffmpeg -y -i "${path}" -c:a libvorbis -b:a ${bitrateLimit}k ${out}.ogg`)
encoder.stderr.on("data", (chunk) => {
currentTime = chunk.split("time=")[1]?.split(" ")[0]
if (currentTime) {
const arr = currentTime.split(":")
let seconds = Number.parseFloat(arr[0] * 3600 + arr[1] * 60 + (+arr[2])) // converting to s
console.clear()
console.log(`Encoding ${out}.ogg`)
bar1.update(Math.round(seconds * 100) / 100)
}
})
encoder.on("close", () => {
console.clear()
term.bold.green("Finished!\n")
term.grey("press enter to exit...\n")
term.inputField(() => { process.exit() })
})
}
async function encodePicture(path, out) {
const encoder = exec(`ffmpeg -y -i "${path}" -qscale 80 -compression_level 6 ${out}.webp`)
encoder.stderr.on("data", (chunk) => {
console.clear()
term.yellow(`Encoding ${out}.webp...`)
})
encoder.on("close", () => {
console.clear()
term.bold.green("Finished!\n")
term.grey("press enter to exit...\n")
term.inputField(() => { process.exit() })
})
}
async function constructVideoCommand(path, out) {
//gets settings file, if doesnt exist makes a new file and uses those defaults
let settings = await getSettings().catch(async (err) => {
settings = undefined
})
if (!settings) settings = await makeNewSettingsFile()
settings = JSON.parse(settings.toString())
settings = settings.presets[settings.currentSetting]
let [duration, resolutionHeight] = await getDurationAndResolution(path)
//Calculates video bitrate to fit right under 8mb @224kb vorbis audio bitrate
const bitrateLimit = Math.round((62000 - (224 * duration)) / duration)
let command = ""
let crfIndex = 0
let isTwoPass = true
while (resolutionHeight > settings.crfMap[crfIndex].resolution) {
crfIndex++
//if the resolution is still higher, just use highest res
if (!settings.crfMap[crfIndex]?.resolution) {
crfIndex--
break
}
}
for (pass = 1; pass <= 2; pass++) {
command += `ffmpeg -y -i "${path}" -vcodec libvpx-vp9 -acodec libvorbis -qscale:a 7 `
command += `-deadline ${settings.deadline} `
command += `-cpu-used ${settings.cpuUsed} `
if (settings?.minrate) {
command += `-b:v ${Math.round(bitrateLimit * 0.95)}k `
command += `-minrate ${Math.round(bitrateLimit / 100 * settings.minrate)}k `
command += `-maxrate ${bitrateLimit}k `
}
else {
command += `-b:v ${bitrateLimit}k `
command += `-crf ${settings.crfMap[crfIndex].crf} `
}
//realtime doesnt support two pass
if (settings.deadline == "realtime") {
command += `-row-mt 1 "${out}.webm"`
isTwoPass = false
break
}
pass == 1 ? command += `-pass 1 -row-mt 1 -f webm NUL && ` : command += `-pass 2 -row-mt 1 "${out}.webm" `
}
return [command, settings.name, duration, isTwoPass]
}
async function getDurationAndResolution(file) {
let query = await ffprobe(file)
//duration in seconds
duration = query.split("Duration: ")[1].split(",")[0]
const arr = duration.split(":") // splitting the string by colon
const seconds = arr[0] * 3600 + arr[1] * 60 + (+arr[2]) // converting to s
//resolution height
resolutionHeight = query.split("Stream #0:0")[1]?.split(",")[2].split(" ")[1].split("x")[1]
return [Number.parseFloat(seconds), resolutionHeight]
}
function ffprobe(file) {
return new Promise((resolve, reject) => {
exec(`ffprobe "${file}"`, (error, stdout, stderr) => {
resolve(stderr)
})
})
}
function getSettings() {
return new Promise((resolve, reject) => {
getSettings = fs.readFile("settings.json", (err, data) => {
resolve(data)
reject(err)
})
})
}
async function startMenu() {
console.clear()
//gets settings file, if doesnt exist makes a new file and uses those defaults
let settings = await getSettings().catch(async (err) => {
settings = undefined
})
if (!settings) settings = await makeNewSettingsFile()
settings = JSON.parse(settings.toString())
let menu = []
for (i = 0; i < settings.presets.length; i++) {
menu.push(`${i}. ${settings.presets[i].name}`)
}
term.italic("How to convert: [app] [filename.extension]\n")
term.yellow("Hello! This menu is for selecting performance/speed preset.\n")
term.yellow("Currently using ").bgMagenta(`"${settings.presets[settings.currentSetting].name}"`).yellow(" preset")
term.singleColumnMenu(menu, (error, response) => {
settings.currentSetting = response.selectedIndex
fs.writeFileSync("settings.json", JSON.stringify(settings))
term.green("\n Using").green.bold(` ${settings.presets[settings.currentSetting].name} `).green("setting\n")
term.grey("Press enter to exit...")
term.inputField(() => { process.exit() })
})
}
function makeNewSettingsFile() {
const settings = `
{
"currentSetting": 2,
"presets": [{
"name": "Most efficient 8 megabytes of your life",
"cpuUsed": 0,
"deadline": "best",
"minrate": 90,
"crfMap": [{
"resolution": 240,
"crf": 1
},
{
"resolution": 360,
"crf": 1
},
{
"resolution": 480,
"crf": 1
},
{
"resolution": 720,
"crf": 1
},
{
"resolution": 1080,
"crf": 1
},
{
"resolution": 1440,
"crf": 1
},
{
"resolution": 2160,
"crf": 1
}
]
},
{
"name": "I have some time to kill",
"cpuUsed": 1,
"deadline": "good",
"minrate": 75,
"crfMap": [{
"resolution": 240,
"crf": 20
},
{
"resolution": 360,
"crf": 20
},
{
"resolution": 480,
"crf": 20
},
{
"resolution": 720,
"crf": 20
},
{
"resolution": 1080,
"crf": 17
},
{
"resolution": 1440,
"crf": 15
},
{
"resolution": 2160,
"crf": 10
}
]
},
{
"name": "Mid",
"cpuUsed": 3,
"deadline": "good",
"minrate":75,
"crfMap": [{
"resolution": 240,
"crf": 30
},
{
"resolution": 360,
"crf": 30
},
{
"resolution": 480,
"crf": 30
},
{
"resolution": 720,
"crf": 25
},
{
"resolution": 1080,
"crf": 20
},
{
"resolution": 1440,
"crf": 15
},
{
"resolution": 2160,
"crf": 10
}
]
},
{
"name": "I don't like waiting",
"cpuUsed": 4,
"deadline": 100,
"minrate": 90,
"crfMap": [{
"resolution": 240,
"crf": 45
},
{
"resolution": 360,
"crf": 42
},
{
"resolution": 480,
"crf": 40
},
{
"resolution": 720,
"crf": 35
},
{
"resolution": 1080,
"crf": 30
},
{
"resolution": 1440,
"crf": 25
},
{
"resolution": 2160,
"crf": 20
}
]
},
{
"name": "I want it, NOW!",
"cpuUsed": 4,
"deadline": "realtime",
"minrate": 50,
"crfMap": [{
"resolution": 240,
"crf": 40
},
{
"resolution": 360,
"crf": 35
},
{
"resolution": 480,
"crf": 30
},
{
"resolution": 720,
"crf": 25
},
{
"resolution": 1080,
"crf": 20
},
{
"resolution": 1440,
"crf": 15
},
{
"resolution": 2160,
"crf": 10
}
]
}
]
}
`
return new Promise((resolve, reject) => {
fs.writeFile("settings.json", settings, () => {
resolve(settings)
})
})
}