commit c9c476160e1e6318591e711db5cb6c29389cbadb Author: Esa Kataja Date: Thu Aug 14 16:12:13 2025 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d9f9d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.srt +*.log +*.idx +*.sub +*.sup +*.mkv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7fe6ae6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "deno.enable": true, + "deno.lint": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "editor.formatOnSave": true +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..a3430a7 --- /dev/null +++ b/deno.json @@ -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" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ae66324 --- /dev/null +++ b/src/main.ts @@ -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 = {}; + 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(); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0ac93a7 --- /dev/null +++ b/src/types.ts @@ -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[]; +}