Initial commit

This commit is contained in:
Esa Kataja
2025-08-14 16:12:13 +03:00
commit c9c476160e
5 changed files with 162 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
*.srt
*.log
*.idx
*.sub
*.sup
*.mkv
+6
View File
@@ -0,0 +1,6 @@
{
"deno.enable": true,
"deno.lint": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
}
+8
View File
@@ -0,0 +1,8 @@
{
"tasks": {
"dev": "deno run --watch --allow-run --allow-write --allow-read src/main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
+110
View File
@@ -0,0 +1,110 @@
import { videoFile } from "./types.ts";
const input = "/home/esa/Videos/tmp/3MUSKETEERS_D1_PAL/B3_t00.mkv";
// TODO: Check if mkvmerge, ffmpeg and vobsub2srt are installed.
const lang_codes = {
"eng": "en",
"fin": "fi",
};
function main() {
const command = new Deno.Command("mkvmerge", {
args: ["-J", input],
});
const { code, stdout, stderr } = command.outputSync();
const mkvmerge_output = JSON.parse(
new TextDecoder().decode(stdout),
) as videoFile;
const error = new TextDecoder().decode(stderr);
if (code !== 0) {
console.error(error);
return;
}
const languages_in_file: Record<string, string> = {};
for (const track of mkvmerge_output.tracks) {
if (!(track.type === "subtitles")) {
continue;
}
const lang =
lang_codes[track.properties.language as keyof typeof lang_codes];
if (!lang) {
console.log("Unknown language: " + track.properties.language);
continue;
}
console.log(track.id + " " + lang);
languages_in_file[track.id] = lang;
}
console.log(languages_in_file);
const formatted_language_string = Object.entries(languages_in_file)
.map(([id, lang]) => `${id}:${lang}`);
console.log(formatted_language_string);
const rip_cmd = new Deno.Command("mkvextract", {
args: ["tracks", input, ...formatted_language_string],
});
rip_cmd.outputSync();
for (const [id, lang] of Object.entries(languages_in_file)) {
const convert_cmd = new Deno.Command("vobsub2srt", {
args: ["-l", lang, "--blacklist", "|", lang],
});
convert_cmd.outputSync();
Deno.remove(lang + ".idx");
Deno.remove(lang + ".sub");
}
const ffmpeg_cmd_raw = [];
const subTracksById = new Map(
mkvmerge_output.tracks
.filter((t) => t.type === "subtitles")
.map((t) => [String(t.id), t]),
);
const entries = Object.entries(languages_in_file);
// 1) Add all inputs first
for (const [, lang] of entries) {
ffmpeg_cmd_raw.push("-i", `${lang}.srt`);
}
// 2) Explicitly set subtitle codec copy (optional but clearer)
ffmpeg_cmd_raw.push("-c:s", "copy");
// 3) Per-subtitle options after all inputs
let i = 0;
for (const [id, lang] of entries) {
const iso639_2 = subTracksById.get(id)?.properties.language ?? "und";
const inputIndex = i; // each SRT is its own input in order
// Map the subtitle stream from the ith SRT input
ffmpeg_cmd_raw.push("-map", `${inputIndex}:s:0`);
// Set language metadata for subtitle stream i in the output
ffmpeg_cmd_raw.push(`-metadata:s:s:${i}`, `language=${iso639_2}`);
// Optionally set default disposition for Finnish
if (lang === "fi") {
ffmpeg_cmd_raw.push(`-disposition:s:${i}`, "default");
}
i++;
}
// 4) Output last
ffmpeg_cmd_raw.push("-y", "subit.mkv");
const ffmpeg_cmd = new Deno.Command("ffmpeg", {
args: ffmpeg_cmd_raw,
});
const { code: ffmpeg_code, stdout: ffmpeg_stdout, stderr: ffmpeg_stderr } =
ffmpeg_cmd.outputSync();
const ffmpeg_error = new TextDecoder().decode(ffmpeg_stderr);
if (ffmpeg_code !== 0) {
console.error(ffmpeg_error);
return;
}
for (const [_, lang] of Object.entries(languages_in_file)) {
Deno.remove(lang + ".srt");
}
}
if (import.meta.main) {
main();
}
+32
View File
@@ -0,0 +1,32 @@
export interface Track {
id: number;
type: string;
codec: string;
properties: TrackProperties;
}
export interface TrackProperties {
codec_id: string;
codec_private_data: string;
codec_private_length: number;
default_track: boolean;
enabled_track: boolean;
forced_track: boolean;
language: string;
num_index_entries: number;
number: number;
tag__statistics_tags: string;
tag__statistics_writing_app: string;
tag__statistics_writing_date_utc: string;
tag_bps: string;
tag_duration: string;
tag_number_of_bytes: string;
tag_number_of_frames: string;
tag_source_id: string;
uid: number;
}
export interface videoFile {
file_name: string;
tracks: Track[];
}