mirror of
https://git.stupid.fish/teidesu/scripts.git
synced 2025-07-28 02:32:11 +10:00
129 lines
3.3 KiB
TypeScript
129 lines
3.3 KiB
TypeScript
import { rm } from 'node:fs/promises'
|
|
|
|
import { $, question } from 'zx'
|
|
|
|
import { fileExists } from '../../utils/fs.ts'
|
|
|
|
let filename = await question('filename >')!
|
|
const startTs = await question('start timestamp >')
|
|
const endTs = await question('end timestamp >')
|
|
const outputFilename = await question('output filename [output.mp4] >') || 'assets/output.mp4'
|
|
|
|
if (filename[0] === '\'' && filename[filename.length - 1] === '\'') {
|
|
filename = filename.slice(1, -1)
|
|
}
|
|
|
|
const ffprobe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${filename}`.json()
|
|
|
|
async function chooseStream(type: string, options: any[], allowNone = false) {
|
|
console.log(`Found ${type} streams:`)
|
|
for (let i = 0; i < options.length; i++) {
|
|
const stream = options[i]
|
|
console.log(`[${i + 1}] (${stream.codec_name}, ${stream.tags.language}) ${stream.tags.title}`)
|
|
}
|
|
|
|
if (allowNone) {
|
|
console.log(`[0] No ${type}`)
|
|
}
|
|
|
|
const res = await question(`select ${type} >`) || '0'
|
|
if (res === '0' && allowNone) {
|
|
return null
|
|
}
|
|
|
|
const streamIndex = Number.parseInt(res)
|
|
if (Number.isNaN(streamIndex) || streamIndex < 1 || streamIndex > options.length) {
|
|
console.error('Invalid input')
|
|
process.exit(1)
|
|
}
|
|
|
|
return streamIndex - 1
|
|
}
|
|
|
|
const allVideos = ffprobe.streams.filter(stream => stream.codec_type === 'video')
|
|
const allAudios = ffprobe.streams.filter(stream => stream.codec_type === 'audio')
|
|
const allSubtitles = ffprobe.streams.filter(stream => stream.codec_type === 'subtitle')
|
|
|
|
let videoStream: number | null = null
|
|
let audioStream: number | null = null
|
|
let subtitleStream: number | null = null
|
|
|
|
if (allVideos.length > 1) {
|
|
videoStream = await chooseStream('video', allVideos)
|
|
} else if (allVideos.length > 0) {
|
|
videoStream = 0
|
|
} else {
|
|
console.error('No video streams found')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (allAudios.length > 1) {
|
|
audioStream = await chooseStream('audio', allAudios)
|
|
} else if (allAudios.length > 0) {
|
|
audioStream = 0
|
|
} else {
|
|
console.warn('No audio streams found, proceeding without audio')
|
|
}
|
|
|
|
if (allSubtitles.length > 0) {
|
|
subtitleStream = await chooseStream('subtitle', allSubtitles, true)
|
|
}
|
|
|
|
const args: string[] = [
|
|
'-i',
|
|
filename,
|
|
'-c:v',
|
|
'libx264',
|
|
'-map',
|
|
`0:v:${videoStream}`,
|
|
'-c:v',
|
|
'libx264',
|
|
]
|
|
|
|
if (audioStream !== null) {
|
|
args.push('-map', `0:a:${audioStream}`)
|
|
}
|
|
|
|
if (subtitleStream !== null) {
|
|
const filenameEscaped = filename.replace(/'/g, "'\\\\\\''")
|
|
args.push('-vf', `format=yuv420p,subtitles='${filenameEscaped}':si=${subtitleStream}`)
|
|
} else {
|
|
args.push('-vf', 'format=yuv420p')
|
|
}
|
|
|
|
if (audioStream !== null) {
|
|
args.push('-c:a', 'libopus')
|
|
|
|
if (allAudios[audioStream].codec_name === 'flac') {
|
|
args.push('-b:a', '320k')
|
|
}
|
|
}
|
|
|
|
args.push(
|
|
'-ss',
|
|
startTs!,
|
|
'-to',
|
|
endTs!,
|
|
outputFilename,
|
|
)
|
|
|
|
if (await fileExists(outputFilename)) {
|
|
const overwrite = await question('Output file already exists, overwrite? [y/N] >')
|
|
if (overwrite?.toLowerCase() !== 'y') {
|
|
process.exit(0)
|
|
}
|
|
|
|
await rm(outputFilename)
|
|
}
|
|
|
|
try {
|
|
$.env.AV_LOG_FORCE_COLOR = 'true'
|
|
await $`ffmpeg ${args}`
|
|
} catch (e) {
|
|
process.exit(1)
|
|
}
|
|
|
|
const openDir = await question('open output directory? [Y/n] >')
|
|
if (!openDir || openDir?.toLowerCase() === 'y') {
|
|
await $`open -R ${outputFilename}`
|
|
}
|