114 lines
3.2 KiB
TypeScript
Executable file
114 lines
3.2 KiB
TypeScript
Executable file
import Ffmpeg from 'fluent-ffmpeg';
|
|
import { spawnSync as spawn } from 'child_process';
|
|
|
|
import { ISO6393To1 } from './lang-codes';
|
|
|
|
|
|
const SPAWN_OPTS = {
|
|
shell: true,
|
|
stdio: [process.stdin, process.stdout, process.stderr],
|
|
};
|
|
|
|
/**
|
|
* These are the cli arguments
|
|
*
|
|
* @param videoPath the directory in which we want to sync the subtitles
|
|
* @param languages a comma-separated list of languages (3 letters) to sync the subtitles
|
|
*/
|
|
const video = process.argv[2];
|
|
const languages = process.argv[3]?.split(',');
|
|
|
|
|
|
const getSubPath = (baseName: string, sub: Ffmpeg.FfprobeStream): string => {
|
|
const language = ISO6393To1.get(sub.tags.language);
|
|
|
|
const forced = sub.disposition?.forced === 0 ?
|
|
'' :
|
|
'.forced';
|
|
|
|
const hearingImpaired = sub.disposition?.hearing_impaired === 0 ?
|
|
'' :
|
|
'.sdh';
|
|
|
|
return `${baseName}${forced}.${language}${hearingImpaired}.srt`;
|
|
};
|
|
|
|
const main = (videoPath: string) => {
|
|
const subIndexes: number[] = [];
|
|
const baseName = videoPath.split('/').at(-1)!.replace(/\.[^.]*$/, '');
|
|
|
|
// ffprobe the video file to see available sub tracks
|
|
Ffmpeg.ffprobe(videoPath, (_e, data) => {
|
|
if (!data?.streams) {
|
|
console.error('Couldn\'t find streams in video file');
|
|
|
|
return;
|
|
}
|
|
|
|
languages.forEach((lang) => {
|
|
let subs = data.streams.filter((s) => {
|
|
return s['tags'] &&
|
|
s['tags']['language'] &&
|
|
s['tags']['language'] === lang &&
|
|
s.codec_type === 'subtitle';
|
|
});
|
|
|
|
const pgs = subs.filter((s) => s.codec_name === 'hdmv_pgs_subtitle');
|
|
|
|
// If we only have PGS subs, warn user
|
|
if (pgs.length === subs.length) {
|
|
console.warn(`No SRT subtitle tracks were found for ${lang}`);
|
|
}
|
|
// Remove PGS streams from subs
|
|
subs = subs.filter((s) => s.codec_name !== 'hdmv_pgs_subtitle');
|
|
|
|
if (subs.length === 0) {
|
|
console.warn(`No subtitle tracks were found for ${lang}`);
|
|
|
|
return;
|
|
}
|
|
|
|
subs.forEach((sub) => {
|
|
const subFile = getSubPath(baseName, sub);
|
|
|
|
// Extract subtitle
|
|
spawn('ffmpeg', [
|
|
'-i', `'${videoPath}'`,
|
|
'-map', `"0:${sub.index}"`, `'${subFile}'`,
|
|
], SPAWN_OPTS);
|
|
|
|
subIndexes.push(sub.index);
|
|
});
|
|
});
|
|
|
|
// Delete subtitles from video
|
|
spawn('mv', [
|
|
`'${videoPath}'`,
|
|
`'${videoPath}.bak'`,
|
|
], SPAWN_OPTS);
|
|
|
|
spawn('ffmpeg', [
|
|
'-i', `'${videoPath}.bak'`,
|
|
'-map', '0',
|
|
...subIndexes.map((i) => [
|
|
'-map', `-0:${i}`,
|
|
]).flat(),
|
|
'-c', 'copy', `'${videoPath}'`,
|
|
], SPAWN_OPTS);
|
|
|
|
spawn('rm', [
|
|
`'${videoPath}.bak'`,
|
|
], SPAWN_OPTS);
|
|
});
|
|
};
|
|
|
|
const escapePath = (p: string): string => p.replaceAll("'", "'\\''");
|
|
|
|
// Check if there are 2 params
|
|
if (video && languages) {
|
|
main(escapePath(video));
|
|
}
|
|
else {
|
|
console.error('Error: no argument passed');
|
|
process.exit(1);
|
|
}
|