rmv/src/main.rs

153 lines
4.9 KiB
Rust

use clap::Parser;
use glob::glob;
use regex::Regex;
use std::path::{Path, PathBuf};
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Path to the directory where the files to be renamed are located.
#[arg(short = 'i', long)]
path: String,
/// Regular expression pattern to match against the filenames in the directory.
///
/// The pattern should contain match groups (expressed in parentheses).
///
/// Example: "S(\d+)E(\d+)"
#[arg(short = 's', long)]
pattern: String,
/// Replacement pattern for the matched filenames.
///
/// You can use `\1`, `\2`, etc. to reference the match groups in the pattern.
///
/// There are also the following tags available:
///
/// * {full_name} - file name with extension
///
/// * {name} - filename without extension
///
/// * {ext} - extension
///
/// Example: "Season \1 Episode \2.{ext}"
#[arg(short = 'r', long)]
replacement: String,
/// Filename wildcard to filter the files to be renamed in the directory.
///
/// For example, "*.mkv" will match all files with the ".mkv" extension.
#[arg(short = 'w', long, default_value = "*")]
wildcard: String,
}
fn get_file_list(dir_path: &str, wildcard: &str) -> Vec<String> {
let pattern = Path::new(dir_path).join(wildcard);
let dir_entries: Vec<Result<PathBuf, _>> = glob(&pattern.to_string_lossy()).unwrap().collect();
dir_entries
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry: &PathBuf| entry.is_file())
.map(|entry| entry.file_name().unwrap().to_string_lossy().to_string())
.collect()
}
fn replace_extra_tags(original_file_name: &str, new_file_name: &str) -> String {
let path = Path::new(original_file_name);
let name = path.file_stem().unwrap().to_str().unwrap();
let extension = path.extension().unwrap().to_str().unwrap();
let re = Regex::new(r"(\{[^{}]+})").unwrap();
re.find_iter(new_file_name)
.map(|m| {
let tag = m.as_str();
match tag {
"{ext}" => new_file_name.replace(tag, extension),
"{full_name}" => new_file_name.replace(tag, original_file_name),
"{name}" => new_file_name.replace(tag, name),
_ => panic!("Invalid tag: {}", tag),
}
})
.fold(String::new(), |result, s| result + &s)
}
fn replace_name(file_name: &str, pattern: &str, replacement: &str) -> String {
let mut new_file_name = String::from(file_name);
let re = Regex::new(pattern).unwrap();
for captures in re.captures_iter(file_name) {
let mut replaced_text = String::from(replacement);
for (i, capture) in captures.iter().enumerate() {
if let Some(capture) = capture {
let capture_text = capture.as_str().to_string();
let replace_key = format!("\\{}", i);
replaced_text = replaced_text.replace(&replace_key, &capture_text);
}
}
new_file_name = replaced_text
}
replace_extra_tags(file_name, &new_file_name)
}
fn main() {
let args = Args::parse();
let dir_path = &args.path;
let pattern = &args.pattern;
let replacement = &args.replacement;
let wildcard = &args.wildcard;
let files = get_file_list(dir_path, wildcard);
for file_name in files {
let replaced_name = replace_name(&file_name, pattern, replacement);
println!("mv \"{}\" \"{}\"", file_name, replaced_name);
}
}
#[test]
fn groups_and_ext_test() {
const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv";
const RESULT: &str = "Season 1 - Episode 16 - The Best Show - Bla Bla Bla - 720p-HDTV.mkv";
const PATTERN: &str = r"(.*) - S0?(\d+)E0?(\d+) - (.*) (HDTV|WEBDL)-(720p|1080p)";
const REPLACEMENT: &str = r"Season \2 - Episode \3 - \1 - \4 - \6-\5.{ext}";
assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT);
}
#[test]
fn name_test() {
const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv";
const RESULT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.crap";
const PATTERN: &str = r"";
const REPLACEMENT: &str = r"{name}.crap";
assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT);
}
#[test]
fn full_name_test() {
const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv";
const RESULT: &str = "REVIEW - The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv";
const PATTERN: &str = r"";
const REPLACEMENT: &str = r"REVIEW - {full_name}";
assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT);
}
#[test]
fn file_list_test() {
const DIRECTORY: &str = ".";
const WILDCARD: &str = "*.toml";
assert_eq!(get_file_list(DIRECTORY, WILDCARD), vec!["Cargo.toml"]);
}
#[test]
fn empty_file_list_test() {
const DIRECTORY: &str = "src";
const WILDCARD: &str = "*.exe";
assert_eq!(get_file_list(DIRECTORY, WILDCARD), Vec::<String>::new());
}