Initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
*.srt
|
||||||
|
*.log
|
||||||
|
*.idx
|
||||||
|
*.sub
|
||||||
|
*.sup
|
||||||
|
*.mkv
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.lint": true,
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user