/* * Copyright (C) 2024 Jonni Liljamo * * 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) { 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) { 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::>() .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::>()) .collect::>(); 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"), } } }