DEVELOPMENT ENVIRONMENT

~liljamo/entamin

ref: 9b1cc2229c33bbb23d849302db4b35d7c9bd7120 entamin/src/main.rs -rw-r--r-- 7.6 KiB
9b1cc222Jonni Liljamo fix: allow keeping no subtitles 7 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/*
 * 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("--no-subtitles".into());
    } else {
        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"),
        }
    }
}