/*
* Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
*
* This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for
* more information.
*/
use core::panic;
use std::{collections::HashMap, fs::DirEntry, io, path::Path, process::Command};
use clap::Parser;
mod mkvinfo;
use mkvinfo::MkvInfo;
mod terminal;
mod util;
#[derive(Parser)]
#[command(version)]
struct Cli {
path_in: String,
path_out: String,
/// Remove input file after mkvmerge
#[arg(long, default_value_t = false)]
remove_input: bool,
}
fn select_subtitles_to_keep(mkv: &matroska::Matroska, out: &mut Vec<u64>) {
let mut stdout = io::stdout();
for tr in mkv.subtitle_tracks() {
terminal::save_cursor(&mut stdout);
println!("Track number: {}", tr.number);
match &tr.language {
Some(lang) => println!("Track language: {:?}", lang),
None => println!("Track language not set, possibly English, or might match audio language. Check manually.")
}
if let Some(name) = &tr.name {
println!("Track name: {}", name);
}
println!("Track codec: {}", tr.codec_id);
match tr.codec_id.as_str() {
"S_HDMV/PGS" => println!("\tStandard Blu-ray subtitles."),
"S_TEXT/UTF8" => println!("\tSubRip subtitles."),
_ => {}
}
if inquire::Confirm::new("Keep track?")
.with_default(true)
.prompt()
.unwrap()
{
out.push(tr.number);
}
terminal::restore_cursor_and_clear(&mut stdout);
}
}
fn make_track_name_edits(mkv: &matroska::Matroska, out: &mut HashMap<u64, String>) {
let mut stdout = io::stdout();
for track in &mkv.tracks {
terminal::save_cursor(&mut stdout);
println!("Track type: {:?}", track.tracktype);
println!("Track name: {:?}", track.name);
if inquire::Confirm::new("Modify name?")
.with_default(false)
.prompt()
.unwrap()
{
let new_name = inquire::Text::new("New name (empty for None):")
.prompt()
.unwrap();
out.insert(track.number, new_name);
}
terminal::restore_cursor_and_clear(&mut stdout);
}
}
/// Phase one, changes that are made with mkvmerge
///
/// Returns the out file path and MkvInfo for phase two.
fn phase_one(cli: &Cli, file: &DirEntry) -> (String, MkvInfo) {
let file_name = file.file_name();
let mkv = matroska::open(file.path().to_str().unwrap()).expect("could not open matroska file");
println!("\nFirst, let's look at some metadata and see if that needs modifying or setting.");
println!("Let's also set some other variables that are needed for further operations.");
let mkv_info = MkvInfo::new(&mkv, &file_name.to_string_lossy());
println!("\nThen, let's see what's going on with subtitles.");
let mut subtitles_to_keep = vec![];
select_subtitles_to_keep(&mkv, &mut subtitles_to_keep);
println!("You chose to keep subtitle tracks: {:?}", subtitles_to_keep);
println!("\nExecuting mkvmerge next, multiplexing speed is determined by disk speed, this may take anywhere from a few seconds to eternity.");
let out_file = format!(
"{}/{}",
cli.path_out.trim_end_matches("/"),
mkv_info.file_name
);
let mut args = vec!["-o".into(), out_file.clone()];
if !subtitles_to_keep.is_empty() {
args.push("--subtitle-tracks".into());
args.push(
subtitles_to_keep
.iter()
.map(|i| (i - 1).to_string())
.collect::<Vec<_>>()
.join(","),
);
}
args.push(file.path().to_string_lossy().into());
let merge_output = Command::new("mkvmerge")
.args(args)
.output()
.expect("failed to execute mkvmerge");
println!("{}", String::from_utf8_lossy(&merge_output.stdout));
if cli.remove_input {
println!("Removing input file.");
std::fs::remove_file(file.path()).expect("failed to remove input file");
}
(out_file, mkv_info)
}
/// Phase two, changes that are made to the out file of phase one with mkvpropedit
///
/// Goes over tracks of the out file, and asks to rename those.
/// Deletes tags from the out file if so desired.
fn phase_two(phase_one_out_file: &str, mkv_info: &MkvInfo) {
let mkv = matroska::open(phase_one_out_file).expect("could not open matroska file");
println!("\nLet's quickly check all track names.");
let mut track_name_edits = HashMap::new();
make_track_name_edits(&mkv, &mut track_name_edits);
println!(
"The following track names will be set: {:?}",
track_name_edits
);
println!("\nLastly, let's see if there's tags to delete.");
println!("If you answer yes, all but the tags written by mkvmerge will be deleted. They'll lose some of their target data for some reason, but are still kept.");
println!("Most likely this only contains mkvmerge tags anyway, but sometimes there's tags that you'd rather not have.");
println!("The following tag names were found:");
let tags = mkv
.tags
.iter()
.map(|t| t.simple.iter().map(|t| t.name.clone()).collect::<Vec<_>>())
.collect::<Vec<_>>();
println!("{:?}", tags);
let delete_tags = inquire::Confirm::new("Delete tags from output file?")
.with_default(false)
.prompt()
.unwrap();
let mut propedit_args = vec![
phase_one_out_file.to_string(),
"--edit".to_string(),
"info".to_string(),
"--set".to_string(),
format!(
"title={}",
format!("{} ({})", mkv_info.title, mkv_info.year)
),
];
for (number, name) in track_name_edits {
propedit_args.push("--edit".to_string());
propedit_args.push(format!("track:{}", number));
if name.is_empty() {
propedit_args.push("--delete".to_string());
propedit_args.push("name".to_string());
} else {
propedit_args.push("--set".to_string());
propedit_args.push(format!("name={}", name));
}
}
if delete_tags {
propedit_args.push("--tags".to_string());
propedit_args.push("global:".to_string());
}
println!("\nExecuting mkvpropedit next, this should be fast.");
let propedit_output = Command::new("mkvpropedit")
.args(propedit_args)
.output()
.expect("failed to execute mkvpropedit");
println!("{}", String::from_utf8_lossy(&propedit_output.stdout));
}
fn main() {
let cli = Cli::parse();
let path_in = Path::new(&cli.path_in);
if !path_in.is_dir() {
panic!("path_in is not a directory")
}
let mut stdout = io::stdout();
let mut files = vec![];
util::collect_files(path_in, &mut files);
for file in files {
terminal::clear_and_reset_cursor(&mut stdout);
let file_name = file.file_name();
println!("Filename: {}", &file_name.to_string_lossy());
match inquire::Confirm::new("Edit file?")
.with_default(true)
.prompt()
{
Ok(true) => {}
Ok(false) => continue,
Err(_) => panic!("something went wrong when asking to edit"),
}
let (out_file, mkv_info) = phase_one(&cli, &file);
phase_two(&out_file, &mkv_info);
match inquire::Confirm::new("Move to next?")
.with_default(true)
.prompt()
{
Ok(true) => {}
Ok(false) => break,
Err(_) => panic!("something went wrong when asking to move to next"),
}
}
}