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}` }