diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 0000000..8b217e8 --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, fs, process::exit}; +use tmdb::{model::*, themoviedb::*}; +use torrent_name_parser::Metadata; +use youchoose; + +use crate::structs::{Language, MovieEntry}; + +// Get the version from Cargo.toml +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Function to process movie entries +pub fn process_file( + filename: &String, + tmdb: &TMDb, + pattern: &str, + dry_run: bool, +) -> (String, bool) { + // Get the basename + let mut file_base = String::from(filename); + let mut parent = String::from(""); + match filename.rsplit_once("/") { + Some(parts) => { + parent = parts.0.to_string(); + file_base = parts.1.to_string(); + } + None => {} + } + + // Parse the filename for metadata + let metadata = Metadata::from(file_base.as_str()).expect("Could not parse filename!"); + // Search using the TMDb API + let mut search = tmdb.search(); + search.title(metadata.title()); + + // Check if year is present in filename + if let Some(year) = metadata.year() { + search.year(year as u64); + } + + let mut results = Vec::new(); + if let Ok(search_results) = search.execute() { + results = search_results.results; + } else { + eprintln!("There was an error while searching {}!", filename); + } + + let mut movie_list: Vec = Vec::new(); + // Create movie entry from the result + for result in results { + let mut movie_details = MovieEntry::from(result); + // Get director's name, if needed + if pattern.contains("{director}") { + let with_credits: Result = + tmdb.fetch().id(movie_details.id).append_credits().execute(); + if let Ok(movie) = with_credits { + if let Some(cre) = movie.credits { + let mut directors = cre.crew; + directors.retain(|x| x.job == "Director"); + for person in directors { + movie_details.director = person.name; + } + } + } + } + movie_list.push(movie_details); + } + + // If nothing is found, skip + if movie_list.len() == 0 { + eprintln!("Could not find any entries matching {}!", filename); + return ("".to_string(), true); + } + + // Choose from the possible entries + let mut menu = youchoose::Menu::new(movie_list.iter()) + .preview(display) + .preview_label(file_base.to_string()); + let choice = menu.show()[0]; + + let mut extension = metadata.extension().unwrap_or("").to_string(); + // Handle the case for subtitle files + let mut is_subtitle = false; + if ["srt", "ssa"].contains(&extension.as_str()) { + let languages = Language::generate_list(); + let mut lang_menu = youchoose::Menu::new(languages.iter()); + let lang_choice = lang_menu.show()[0]; + if languages[lang_choice].short != "none".to_string() { + extension = format!("{}.{}", languages[lang_choice].short, extension); + } + is_subtitle = true; + } + + // Create the new name + let mut new_name_base = movie_list[choice].rename_format(pattern.to_string()); + if extension != "" { + new_name_base = format!("{}.{}", new_name_base, extension); + } + let mut new_name = String::from(new_name_base.clone()); + if parent != "".to_string() { + new_name = format!("{}/{}", parent, new_name); + } + + // Process the renaming + if *filename == new_name { + println!("[file] {} already has correct name.", filename); + } else { + println!("[file] {} -> {}", file_base, new_name_base); + // Only do the rename of --dry-run isn't passed + if dry_run == false { + fs::rename(filename, new_name.as_str()).expect("Unable to rename file!"); + } + } + (new_name_base, is_subtitle) +} + +// Display function for preview in menu +fn display(movie: &MovieEntry) -> String { + let mut buffer = String::new(); + buffer.push_str(&format!("Title: {}\n", movie.title)); + buffer.push_str(&format!("Release year: {}\n", movie.year)); + buffer.push_str(&format!("Language: {}\n", movie.language)); + buffer.push_str(&format!("Director: {}\n", movie.director)); + buffer.push_str(&format!("TMDb ID: {}\n", movie.id)); + buffer.push_str(&format!("Overview: {}\n", movie.overview)); + buffer +} + +// Function to process the passed arguments +pub fn process_args(mut args: Vec) -> (Vec, HashMap<&'static str, bool>) { + // Remove the entry corresponding to the running process + args.remove(0); + let mut entries = Vec::new(); + let mut settings = HashMap::from([("dry_run", false), ("directory", false)]); + for arg in args { + match arg.as_str() { + "--help" | "-h" => { + println!(" The expected syntax is:"); + println!( + " movie_rename [-n|--dry-run] [-d|--directory] [-v|--version]" + ); + println!( + " There needs to be a config file names movie_rename.conf in your $XDG_CONFIG_HOME." + ); + println!(" It should consist of two lines. The first line should have your TMDb API key."); + println!( + " The second line should have a pattern, that will be used for the rename." + ); + println!(" In the pattern, the variables need to be enclosed in {{}}, the supported variables are `title`, `year` and `director`."); + println!( + " Default pattern is `{{title}} ({{year}}) - {{director}}`. Extension is always kept." + ); + println!("Passing --directory assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles."); + println!(" Pass --help to get this again."); + exit(0); + } + "--dry-run" | "-n" => { + println!("Doing a dry run..."); + settings.entry("dry_run").and_modify(|x| *x = true); + } + "--directory" | "-d" => { + println!("Running in directory mode..."); + settings.entry("directory").and_modify(|x| *x = true); + } + "--version" | "-v" => { + println!("movie_rename {}", VERSION); + exit(0); + } + other => { + if other.starts_with("-") { + eprintln!("Unknown argument passed: {}", other); + } else { + entries.push(arg); + } + } + } + } + (entries, settings) +} diff --git a/src/main.rs b/src/main.rs index 0a7e0b9..9b7bb37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,58 +1,11 @@ use load_file::{self, load_str}; -use std::{collections::HashMap, env, fmt, fs, path::Path, process::exit}; -use tmdb::{model::*, themoviedb::*}; -use torrent_name_parser::Metadata; -use youchoose; +use std::{env, fs, path::Path, process::exit}; +use tmdb::themoviedb::*; -// Get the version from Cargo.toml -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -// Struct for movie entries -struct MovieEntry { - title: String, - id: u64, - director: String, - year: String, - language: String, - overview: String, -} - -impl MovieEntry { - // Create movie entry from results - fn from(movie: SearchMovie) -> MovieEntry { - MovieEntry { - title: movie.title, - id: movie.id, - director: String::from("N/A"), - year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")), - language: movie.original_language, - overview: movie.overview.unwrap_or(String::from("N/A")), - } - } - - // Generate desired filename from movie entry - fn rename_format(&self, mut format: String) -> String { - format = format.replace("{title}", self.title.as_str()); - if self.year.as_str() != "N/A" { - format = format.replace("{year}", self.year.as_str()); - } else { - format = format.replace("{year}", ""); - } - if self.director.as_str() != "N/A" { - format = format.replace("{director}", self.director.as_str()); - } else { - format = format.replace("{director}", ""); - } - format - } -} - -// Implement display trait for movie entries -impl fmt::Display for MovieEntry { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} ({})", self.title, self.year) - } -} +// Import all the modules +mod functions; +use functions::{process_args, process_file}; +mod structs; fn main() { // Read arguments from commandline @@ -142,167 +95,3 @@ fn main() { } } } - -// Function to process movie entries -fn process_file(filename: &String, tmdb: &TMDb, pattern: &str, dry_run: bool) -> (String, bool) { - // Get the basename - let mut file_base = String::from(filename); - let mut parent = String::from(""); - match filename.rsplit_once("/") { - Some(parts) => { - parent = parts.0.to_string(); - file_base = parts.1.to_string(); - } - None => {} - } - - // Parse the filename for metadata - let metadata = Metadata::from(file_base.as_str()).expect("Could not parse filename!"); - // Search using the TMDb API - let mut search = tmdb.search(); - search.title(metadata.title()); - - // Check if year is present in filename - if let Some(year) = metadata.year() { - search.year(year as u64); - } - - let mut results = Vec::new(); - if let Ok(search_results) = search.execute() { - results = search_results.results; - } else { - eprintln!("There was an error while searching {}!", filename); - } - - let mut movie_list: Vec = Vec::new(); - // Create movie entry from the result - for result in results { - let mut movie_details = MovieEntry::from(result); - // Get director's name, if needed - if pattern.contains("{director}") { - let with_credits: Result = - tmdb.fetch().id(movie_details.id).append_credits().execute(); - if let Ok(movie) = with_credits { - if let Some(cre) = movie.credits { - let mut directors = cre.crew; - directors.retain(|x| x.job == "Director"); - for person in directors { - movie_details.director = person.name; - } - } - } - } - movie_list.push(movie_details); - } - - // If nothing is found, skip - if movie_list.len() == 0 { - eprintln!("Could not find any entries matching {}!", filename); - return ("".to_string(), true); - } - - // Choose from the possible entries - let mut menu = youchoose::Menu::new(movie_list.iter()) - .preview(display) - .preview_label(file_base.to_string()); - let choice = menu.show()[0]; - - let mut extension = metadata.extension().unwrap_or("").to_string(); - // Handle the case for subtitle files - let mut is_subtitle = false; - if ["srt", "ssa"].contains(&extension.as_str()) { - let languages = Vec::from(["en", "hi", "bn", "de", "fr", "sp", "ja", "n/a"]); - let mut lang_menu = youchoose::Menu::new(languages.iter()); - let lang_choice = lang_menu.show()[0]; - if languages[lang_choice] != "none" { - extension = format!("{}.{}", languages[lang_choice], extension); - } - is_subtitle = true; - } - - // Create the new name - let new_name_base = movie_list[choice].rename_format(pattern.to_string()); - let mut new_name = String::from(new_name_base.clone()); - if extension != "" { - new_name = format!("{}.{}", new_name, extension); - } - if parent != "".to_string() { - new_name = format!("{}/{}", parent, new_name); - } - - // Process the renaming - if *filename == new_name { - println!("[file] {} already has correct name.", filename); - } else { - println!("[file] {} -> {}", file_base, new_name); - // Only do the rename of --dry-run isn't passed - if dry_run == false { - fs::rename(filename, new_name.as_str()).expect("Unable to rename file!"); - } - } - (new_name_base, is_subtitle) -} - -// Display function for preview in menu -fn display(movie: &MovieEntry) -> String { - let mut buffer = String::new(); - buffer.push_str(&format!("Title: {}\n", movie.title)); - buffer.push_str(&format!("Release year: {}\n", movie.year)); - buffer.push_str(&format!("Language: {}\n", movie.language)); - buffer.push_str(&format!("Director: {}\n", movie.director)); - buffer.push_str(&format!("TMDb ID: {}\n", movie.id)); - buffer.push_str(&format!("Overview: {}\n", movie.overview)); - buffer -} - -// Function to process the passed arguments -fn process_args(mut args: Vec) -> (Vec, HashMap<&'static str, bool>) { - // Remove the entry corresponding to the running process - args.remove(0); - let mut entries = Vec::new(); - let mut settings = HashMap::from([("dry_run", false), ("directory", false)]); - for arg in args { - match arg.as_str() { - "--help" | "-h" => { - println!(" The expected syntax is:"); - println!( - " movie_rename [-n|--dry-run] [-d|--directory] [-v|--version]" - ); - println!( - " There needs to be a config file names movie_rename.conf in your $XDG_CONFIG_HOME." - ); - println!(" It should consist of two lines. The first line should have your TMDb API key."); - println!( - " The second line should have a pattern, that will be used for the rename." - ); - println!(" In the pattern, the variables need to be enclosed in {{}}, the supported variables are `title`, `year` and `director`."); - println!( - " Default pattern is `{{title}} ({{year}}) - {{director}}`. Extension is always kept." - ); - println!("Passing --directory assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles."); - println!(" Pass --help to get this again."); - exit(0); - } - "--dry-run" | "-n" => { - println!("Doing a dry run..."); - settings.entry("dry_run").and_modify(|x| *x = true); - } - "--directory" | "-d" => { - println!("Running in directory mode..."); - settings.entry("directory").and_modify(|x| *x = true); - } - "--version" | "-v" => { - println!("movie_rename {}", VERSION); - exit(0); - } - other => { - if other.starts_with("-") { - eprintln!("Unknown argument passed: {}", other); - } else { - entries.push(arg); - } - } - } - } - (entries, settings) -} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..e2d07fb --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,85 @@ +use std::fmt; +use tmdb::model::*; + +// Struct for movie entries +pub struct MovieEntry { + pub title: String, + pub id: u64, + pub director: String, + pub year: String, + pub language: String, + pub overview: String, +} + +impl MovieEntry { + // Create movie entry from results + pub fn from(movie: SearchMovie) -> MovieEntry { + MovieEntry { + title: movie.title, + id: movie.id, + director: String::from("N/A"), + year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")), + language: movie.original_language, + overview: movie.overview.unwrap_or(String::from("N/A")), + } + } + + // Generate desired filename from movie entry + pub fn rename_format(&self, mut format: String) -> String { + format = format.replace("{title}", self.title.as_str()); + if self.year.as_str() != "N/A" { + format = format.replace("{year}", self.year.as_str()); + } else { + format = format.replace("{year}", ""); + } + if self.director.as_str() != "N/A" { + format = format.replace("{director}", self.director.as_str()); + } else { + format = format.replace("{director}", ""); + } + format + } +} + +// Implement display trait for movie entries +impl fmt::Display for MovieEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} ({})", self.title, self.year) + } +} + +pub struct Language { + pub short: String, + pub long: String, +} + +impl Language { + // Create Language entries from &str pairs + fn from(short: &str, long: &str) -> Language { + Language { + short: short.to_string(), + long: long.to_string(), + } + } + + // Generate a vector of Language entries of all supported languages + pub fn generate_list() -> Vec { + let mut list = Vec::new(); + list.push(Language::from("en", "English")); + list.push(Language::from("hi", "Hindi")); + list.push(Language::from("bn", "Bengali")); + list.push(Language::from("fr", "French")); + list.push(Language::from("ja", "Japanese")); + list.push(Language::from("de", "German")); + list.push(Language::from("sp", "Spanish")); + list.push(Language::from("none", "None")); + list + } +} + +// Implement display trait for Language +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.long) + } +}