movie-rename/src/functions.rs

218 lines
8.1 KiB
Rust

use inquire::{
ui::{Color, IndexPrefix, RenderConfig, Styled},
InquireError, Select,
};
use std::{collections::HashMap, fs, path::Path};
use tmdb_api::{
client::{reqwest::ReqwestExecutor, Client},
movie::{credits::MovieCredits, search::MovieSearch},
prelude::Command,
};
use torrent_name_parser::Metadata;
use crate::structs::{get_long_lang, Language, MovieEntry};
// Function to process movie entries
pub async fn process_file(
filename: &String,
tmdb: &Client<ReqwestExecutor>,
pattern: &str,
dry_run: bool,
lucky: bool,
movie_list: Option<&HashMap<String, Option<String>>>,
// The last bool tells whether the entry should be added to the movie_list or not
// The first String is filename without extension, and the second String is
// new basename, if any.
) -> (String, Option<String>, bool) {
// Set RenderConfig for the menu items
inquire::set_global_render_config(get_render_config());
// Get the basename
let mut file_base = String::from(filename);
let mut parent = String::new();
if let Some(parts) = filename.rsplit_once('/') {
{
parent = String::from(parts.0);
file_base = String::from(parts.1);
}
}
// Split the filename into parts for a couple of checks and some later use
let filename_parts: Vec<&str> = filename.rsplit('.').collect();
let filename_without_ext = if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
filename.rsplitn(3, '.').last().unwrap().to_string()
} else {
filename.rsplit_once('.').unwrap().0.to_string()
};
// Check if the filename (without extension) has already been processed
// If yes, we'll use the older results
let mut preprocessed = false;
let mut new_name_base = match movie_list {
None => String::new(),
Some(list) => {
if list.contains_key(&filename_without_ext) {
preprocessed = true;
list[&filename_without_ext].clone().unwrap_or_default()
} else {
String::new()
}
}
};
// Check if it should be ignored
if preprocessed && new_name_base.is_empty() {
eprintln!(" Ignoring {file_base} as per previous choice for related files...");
return (filename_without_ext, None, false);
}
// Parse the filename for metadata
let metadata = Metadata::from(file_base.as_str()).expect(" Could not parse filename!");
// Process only if it's a valid file format
let mut extension = metadata.extension().unwrap_or("").to_string();
if ["mp4", "avi", "mkv", "flv", "m4a", "srt", "ssa"].contains(&extension.as_str()) {
println!(" Processing {file_base}...");
} else {
println!(" Ignoring {file_base}...");
return (filename_without_ext, None, false);
}
// Only do the TMDb API stuff if it's not preprocessed
if !preprocessed {
// Search using the TMDb API
let year = metadata.year().map(|y| y as u16);
let search = MovieSearch::new(metadata.title().to_string()).with_year(year);
let reply = search.execute(tmdb).await;
let results = match reply {
Ok(res) => Ok(res.results),
Err(e) => {
eprintln!(" There was an error while searching {file_base}!");
Err(e)
}
};
let mut movie_list: Vec<MovieEntry> = Vec::new();
// Create movie entry from the result
if results.is_ok() {
for result in results.unwrap() {
let mut movie_details = MovieEntry::from(result);
// Get director's name, if needed
if pattern.contains("{director}") {
let credits_search = MovieCredits::new(movie_details.id);
let credits_reply = credits_search.execute(tmdb).await;
if credits_reply.is_ok() {
let mut crew = credits_reply.unwrap().crew;
// Only keep the director(s)
crew.retain(|x| x.job == *"Director");
if !crew.is_empty() {
let directors: Vec<String> =
crew.iter().map(|x| x.person.name.clone()).collect();
let mut directors_text = directors.join(", ");
if let Some(pos) = directors_text.rfind(',') {
directors_text.replace_range(pos..pos + 2, " and ");
}
movie_details.director = Some(directors_text);
}
}
}
movie_list.push(movie_details);
}
}
// If nothing is found, skip
if movie_list.is_empty() {
eprintln!(" Could not find any entries matching {file_base}!");
return (filename_without_ext, None, true);
}
let choice;
if lucky {
// Take first choice if in lucky mode
choice = movie_list.into_iter().next().unwrap();
} else {
// Choose from the possible entries
choice = match Select::new(
format!(" Possible choices for {file_base}:").as_str(),
movie_list,
)
.prompt()
{
Ok(movie) => movie,
Err(error) => {
println!(" {error}");
let flag = matches!(
error,
InquireError::OperationCanceled | InquireError::OperationInterrupted
);
return (filename_without_ext, None, flag);
}
};
};
// Create the new name
new_name_base = choice.rename_format(String::from(pattern));
} else {
println!(" Using previous choice for related files...");
}
// Handle the case for subtitle files
if ["srt", "ssa"].contains(&extension.as_str()) {
// Try to detect if there's already language info in the filename, else ask user to choose
if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
println!(
" Keeping language {} as detected in the subtitle file's extension...",
get_long_lang(filename_parts[1])
);
extension = format!("{}.{}", filename_parts[1], extension);
} else {
let lang_list = Language::generate_list();
let lang_choice =
Select::new(" Choose the language for the subtitle file:", lang_list)
.prompt()
.expect(" Invalid choice!");
if lang_choice.short != *"none" {
extension = format!("{}.{}", lang_choice.short, extension);
}
}
}
// Add extension and stuff to the new name
let mut new_name_with_ext = new_name_base.clone();
if !extension.is_empty() {
new_name_with_ext = format!("{new_name_with_ext}.{extension}");
}
let mut new_name = new_name_with_ext.clone();
if !parent.is_empty() {
new_name = format!("{parent}/{new_name}");
}
// Process the renaming
if *filename == new_name {
println!(" [file] '{file_base}' already has correct name.");
} else {
println!(" [file] '{file_base}' -> '{new_name_with_ext}'");
// Only do the rename of --dry-run isn't passed
if !dry_run {
if !Path::new(new_name.as_str()).is_file() {
fs::rename(filename, new_name.as_str()).expect(" Unable to rename file!");
} else {
eprintln!(" Destination file already exists, skipping...");
}
}
}
(filename_without_ext, Some(new_name_base), true)
}
// RenderConfig for the menu items
fn get_render_config() -> RenderConfig<'static> {
let mut render_config = RenderConfig::default();
render_config.option_index_prefix = IndexPrefix::Simple;
render_config.error_message = render_config
.error_message
.with_prefix(Styled::new("").with_fg(Color::LightRed));
render_config
}