Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

12 changed files with 1249 additions and 2139 deletions

1
.gitignore vendored
View file

@ -2,4 +2,3 @@
*.mp4 *.mp4
*.srt *.srt
*.key *.key
*.tar.gz

2505
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,12 @@
[package] [package]
name = "movie-rename" name = "movie_rename"
version = "2.3.2" version = "0.1.0"
build = "build.rs"
edition = "2021" edition = "2021"
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
license = "GPL-3.0"
description = "A simple tool to rename movies, written in Rust."
homepage = "https://github.com/SinTan1729/movie-rename"
documentation = "https://docs.rs/movie-rename"
repository = "https://github.com/SinTan1729/movie-rename"
readme = "README.md"
keywords = ["rename", "movie", "media", "tmdb"]
categories = ["command-line-utilities"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
torrent-name-parser = "0.12.1" torrent-name-parser = "0.11.0"
tmdb-api = "0.8.0" tmdb = "3.0.0"
inquire = "0.7.5" youchoose = "0.1.1"
load_file = "1.0.1" load_file = "1.0.1"
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] }
clap = { version = "4.5.1", features = ["cargo"] }
[build-dependencies]
clap = { version = "4.5.1", features = ["cargo"] }
clap_complete = "4.5.1"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

View file

@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
movie-rename: A simple tool to rename movies, written in Rust. <one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2023 Sayantan Santra Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
movie-rename Copyright (C) 2023 Sayantan Santra <program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

View file

@ -1,24 +0,0 @@
PREFIX := /usr/local
PKGNAME := movie-rename
build:
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.34
build-debug:
cargo build
clean:
cargo clean
install: build
install -Dm755 target/release/$(PKGNAME) "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
install -Dm644 $(PKGNAME).1 "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
rm -f "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
aur: build
tar --transform 's/.*\///g' -czf $(PKGNAME).tar.gz target/x86_64-unknown-linux-gnu/release/$(PKGNAME) target/autocomplete/* $(PKGNAME).1
.PHONY: build build-debug install clean uninstall aur

View file

@ -1,40 +1,16 @@
[![latest-release](https://img.shields.io/github/v/release/SinTan1729/movie-rename?label=latest%20release)](https://github.com/SinTan1729/movie-rename/releases/latest/) # movie-rename
![commits-since-latest-release](https://img.shields.io/github/commits-since/SInTan1729/movie-rename/latest?label=commits%20since%20latest%20release)
[![AUR package](https://img.shields.io/aur/version/movie-rename-bin?label=AUR&logo=archlinux)](https://aur.archlinux.org/packages/movie-rename-bin/)
# `movie-rename`
### A simple tool to rename movies, written in Rust. ## A simple tool to reame movies, written in Rust.
It turns a file like `Apur.Sansar.HEVC.2160p.AC3.mkv` into `Apur Sansar (1959) - Satyajit Ray.mkv` using metadata pulled from [TMDB](https://www.themoviedb.org/). This is made mostly due to [mnamer](https://github.com/jkwill87/mnamer) not having support for director's name, and partly because I wanted to try writing something useful in Rust.
This is made mostly due to [mnamer](https://github.com/jkwill87/mnamer) not having support for director's name, and also because I wanted to try writing something useful in Rust. The expected syntax is:
## Installation `movie_rename <filename(s)> [--dry-run]`
Install from [AUR](https://aur.archlinux.org/packages/movie-rename-bin), my personal [lure-repo](https://github.com/SinTan1729/lure-repo) or download the binary from the releases. You can also get it from [crates.io](https://crates.io/crates/movie-rename). - There needs to be a config file names movie_rename.conf in your $XDG_CONFIG_HOME.
- It should consist of two lines. The first line should have your TMDb API key.
You can also install from source by using
```
git clone https://github.com/SinTan1729/movie-rename
cd movie-rename
sudo make install
```
## Usage
- The syntax is:
`movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]`
- There needs to be a config file named `config` in the `$XDG_CONFIG_HOME/movie-rename/` directory.
- It should consist of two lines. The first line should have your [TMDB API key](https://developers.themoviedb.org/3/getting-started/authentication).
- The second line should have a pattern, that will be used for the rename. - The second line should have a pattern, that will be used for the rename.
- In the pattern, the variables need to be enclosed in `{}`, the supported variables are `title`, `year` and `director`. - In the pattern, the variables need to be enclosed in {{}}, the supported variables are `title`, `year` and `director`.
- Default pattern is `{title} ({year}) - {director}`. Extension is always kept. - Default pattern is `{title} ({year}) - {director}`. Extension is always kept.
- Passing `--directory` or `-d` assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles.
- Passing `--dry-run` or `-n` does a dry tun and only prints out the new names, without actually doing anything.
- Passing `--i-feel-lucky` or `-l` automatically chooses the first option. Useful when you use the program as part of a script.
- You can join the short flags `-d`, `-n` and `-l` together (e.g. `-dn` or `-dln`).
- Passing `--help` or `-h` shows help and exits.
- Passing `--version` or `-v` shows version and exits.
## Notes I plan to add more variables in the future. Support for TV Shows will not be added, since [tvnamer](https://github.com/dbr/tvnamer) does that excellently.
- Currently, it only supports names in English. It should be easy to turn it into a configurable option. Since for movies in all the languages I know, English name is usually provided, it's a non-feature for me. If someone is willing to test it out for other languages, they're welcome. I'm open to accepting PRs.
- I plan to add more variables in the future. Support for TV Shows will not be added, since [tvnamer](https://github.com/dbr/tvnamer) does that excellently.

View file

@ -1,21 +0,0 @@
use clap_complete::generate_to;
use clap_complete::shells::{Bash, Fish, Zsh};
use std::env;
use std::ffi::OsString;
use std::fs::{create_dir, remove_dir_all};
use std::io::Error;
include!("src/args.rs");
fn main() -> Result<(), Error> {
let target = "./target/autocomplete";
remove_dir_all(target).ok();
create_dir(target)?;
let outdir = OsString::from(target);
let mut cmd = get_command();
generate_to(Bash, &mut cmd, "movie-rename", &outdir)?;
generate_to(Fish, &mut cmd, "movie-rename", &outdir)?;
generate_to(Zsh, &mut cmd, "movie-rename", &outdir)?;
Ok(())
}

View file

@ -1,37 +0,0 @@
.\" Manpage for movie-rename.
.\" Contact sayantan[dot]santra689[at]gmail[dot]com to correct errors or typos.
.TH man 1 "February 2023" "movie-rename"
.SH NAME
movie-rename
.SH SYNOPSIS
movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]
.SH DESCRIPTION
movie-rename is a simple tool to rename movies, written in Rust.
.SH ARGUMENTS
A list of filenames (or directory names, not both). -d or --directory must be passed to work with directories.
.SH OPTIONS
.TP
-n, --dry-run
Performs a dry run, without actually renaming anything.
.TP
-d, --directory
Runs in directory mode. In this mode, it is assumed that the arguments are directory names, which contain exactly one movie and optionally subtitles.
.TP
-h, --help
Print help information.
.TP
-v, --version
Print version information.
.SH CONFIG
There needs to be a config file named config in the $XDG_CONFIG_HOME/movie-rename/ directory.
It should consist of two lines.
.sp
The first line should have your TMDb API key.
.sp
The second line should have a pattern, that will be used for the rename.
.sp
In the pattern, the variables need to be enclosed in {}, the supported variables are `title`, `year` and `director`.
.sp
Default pattern is `{title} ({year}) - {director}`. Extension is always kept.
.SH AUTHOR
Sayantan Santra sayantan[dot]santra689[at]gmail[dot]com

View file

@ -1,46 +0,0 @@
use clap::{arg, command, ArgAction, Command, ValueHint};
use std::collections::HashMap;
// Bare command generation function to help with autocompletion
pub fn get_command() -> Command {
command!()
.name("movie-rename")
.author("Sayantan Santra <sayantan.santra@gmail.com>")
.about("A simple tool to rename movies, written in Rust.")
.arg(arg!(-d --directory "Run in directory mode").action(ArgAction::SetTrue))
.arg(arg!(-n --"dry-run" "Do a dry run").action(ArgAction::SetTrue))
.arg(arg!(-l --"i-feel-lucky" "Always choose the first option").action(ArgAction::SetTrue))
.arg(
arg!([entries] "The files/directories to be processed")
.trailing_var_arg(true)
.num_args(1..)
.value_hint(ValueHint::AnyPath)
.required(true),
)
// Use -v instead of -V for version
.disable_version_flag(true)
.arg(arg!(-v --version "Print version").action(ArgAction::Version))
.arg_required_else_help(true)
}
// Function to process the passed arguments
pub fn process_args() -> (Vec<String>, HashMap<String, bool>) {
let matches = get_command().get_matches();
// Generate the settings HashMap from read flags
let mut settings = HashMap::new();
for id in matches.ids().map(|x| x.as_str()) {
if id != "entries" {
settings.insert(id.to_string(), matches.get_flag(id));
}
}
// Every unmatched argument should be treated as a file entry
let entries: Vec<String> = matches
.get_many::<String>("entries")
.expect("No entries provided!")
.cloned()
.collect();
(entries, settings)
}

View file

@ -1,218 +0,0 @@
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
}

View file

@ -1,139 +1,195 @@
use load_file::{self, load_str}; use load_file::{self, load_str};
use std::{collections::HashMap, env, fs, path::Path, process::exit}; use std::{env, fmt, fs, process::exit};
use tmdb_api::client::{reqwest::ReqwestExecutor, Client}; use tmdb::{model::*, themoviedb::*};
use torrent_name_parser::Metadata;
use youchoose;
// Import all the modules // Struct for movie entries
mod functions; struct MovieEntry {
use functions::process_file; title: String,
mod args; id: u64,
mod structs; director: String,
year: String,
language: String,
overview: String,
}
#[tokio::main] impl MovieEntry {
async fn main() { // Create movie entry from results
// Process the passed arguments fn from(movie: SearchMovie) -> MovieEntry {
let (entries, settings) = args::process_args(); MovieEntry {
let flag_dry_run = settings["dry-run"]; title: movie.title,
let flag_directory = settings["directory"]; id: movie.id,
let flag_lucky = settings["i-feel-lucky"]; director: String::from("N/A"),
year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")),
// Print some message when flags are set. language: movie.original_language,
if flag_dry_run { overview: movie.overview.unwrap_or(String::from("N/A")),
println!("Doing a dry run. No files will be modified.")
} }
if flag_directory {
println!("Running in directory mode...")
} }
if flag_lucky {
println!("Automatically selecting the first entry...") // 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)
}
}
fn main() {
// Read arguments from commandline
let mut args = env::args();
args.next();
let filenames: Vec<String> = args.collect();
// If --help is passed, show help and exit
if filenames.contains(&"--help".to_string()) {
println!(" The expected syntax is:");
println!(" movie_rename <filename(s)> [--dry-run]");
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."
);
exit(0);
} }
// Try to read config file, or display error // Try to read config file, or display error
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME")); let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or("$HOME".to_string());
if config_file == "$HOME" { if config_file == String::from("$HOME") {
config_file = env::var("$HOME").unwrap(); config_file = env::var("$HOME").unwrap();
config_file.push_str("/.config");
} }
config_file.push_str("/movie-rename/config"); config_file.push_str("/movie_rename.conf");
if !Path::new(config_file.as_str()).is_file() {
eprintln!("Error reading the config file. Pass --help to see help.");
exit(2);
}
let mut config = load_str!(config_file.as_str()).lines(); let mut config = load_str!(config_file.as_str()).lines();
let api_key = config.next().unwrap_or(""); let api_key = config.next().unwrap_or("");
let pattern = config.next().unwrap_or("{title} ({year}) - {director}"); let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
if api_key.is_empty() { if api_key == "" {
eprintln!("Could not read the API key. Pass --help to see help."); eprintln!("Error reading the config file. Pass --help to see help.");
exit(2); exit(1);
} }
// Create TMDb object for API calls // Create TMDb object for API calls
let tmdb = Client::<ReqwestExecutor>::new(String::from(api_key)); let tmdb = TMDb {
api_key: api_key,
language: "en",
};
// Iterate over entries // Check if --dry-run is passed
for entry in entries { let mut dry_run = false;
// Check if the file/directory exists on disk and run necessary commands if filenames.contains(&"--dry-run".to_string()) {
match flag_directory { println!("Doing a dry run.");
// Normal file dry_run = true;
false => { }
if Path::new(entry.as_str()).is_file() {
// Process the filename for movie entries // Iterate over filenames
process_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await; for filename in filenames {
} else { // Skip if it's the --dry-run tag
eprintln!("The file {entry} wasn't found on disk, skipping..."); if filename == "--dry-run".to_string() {
continue; continue;
} }
}
// Directory
true => {
if Path::new(entry.as_str()).is_dir() {
println!("Processing files inside the directory {entry}...");
let mut movie_list = HashMap::new();
if let Ok(files_in_dir) = fs::read_dir(entry.as_str()) { // Parse the filename for metadata
for file in files_in_dir { let metadata = Metadata::from(filename.as_str()).expect("Could not parse filename");
if file.is_ok() {
let filename = file.unwrap().path().display().to_string();
let (filename_without_ext, movie_name_temp, add_to_list) =
process_file(
&filename,
&tmdb,
pattern,
flag_dry_run,
flag_lucky,
Some(&movie_list),
)
.await;
if add_to_list { // Search using the TMDb API
movie_list.insert(filename_without_ext, movie_name_temp); let results = tmdb
} .search()
} .title(metadata.title())
} .year(metadata.year().unwrap() as u64)
} else { .execute()
eprintln!("There was an error accessing the directory {entry}!"); .unwrap()
continue; .results;
}
if movie_list.len() == 1 {
let entry_clean = entry.trim_end_matches('/');
let movie_name = movie_list.into_values().next().unwrap();
// If the file was ignored, exit let mut movie_list: Vec<MovieEntry> = Vec::new();
match movie_name {
None => { // Create movie entry from the result
eprintln!("Not renaming directory as only movie was skipped."); 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<Movie, _> =
tmdb.fetch().id(movie_details.id).append_credits().execute();
if let Ok(movie) = with_credits {
match movie.credits {
Some(cre) => {
let mut directors = cre.crew;
directors.retain(|x| x.job == "Director");
for person in directors {
movie_details.director = person.name;
}
}
None => {}
}
}
}
movie_list.push(movie_details);
} }
Some(name) => { // Choose from the possible entries
if entry_clean == name { let mut menu = youchoose::Menu::new(movie_list.iter())
println!( .preview(display)
"[directory] '{entry_clean}' already has correct name." .preview_label(filename.to_string());
); let choice = menu.show()[0];
// Handle the case for subtitle files
let mut extension = metadata.extension().unwrap_or("").to_string();
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);
}
}
// Create the new name
let mut new_name_vec = vec![
movie_list[choice].rename_format(pattern.to_string()),
extension,
];
new_name_vec.retain(|x| !x.is_empty());
let new_name = new_name_vec.join(".");
if filename == new_name {
println!("{} already has correct name.", filename);
} else { } else {
println!("[directory] '{entry_clean}' -> '{name}'",); println!("{} -> {}", filename, new_name);
if !flag_dry_run { // Only do the rename of --dry-run isn't passed
if !Path::new(name.as_str()).is_dir() { if dry_run == false {
fs::rename(entry, name) println!("Renaming...");
.expect("Unable to rename directory!"); fs::rename(filename, new_name).expect("Unable to rename file.");
} else {
eprintln!(
"Destination directory already exists, skipping..."
);
}
}
}
}
}
} else {
eprintln!("Could not determine how to rename the directory {entry}!");
}
} else {
eprintln!("The directory {entry} wasn't found on disk, skipping...");
continue;
}
} }
} }
} }
} }
// 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.title));
buffer.push_str(&format!("TMDb ID: {}\n", movie.id));
buffer.push_str(&format!("Overview: {}\n", movie.overview));
buffer
}

View file

@ -1,171 +0,0 @@
use std::fmt;
use tmdb_api::movie::MovieShort;
// Struct for movie entries
pub struct MovieEntry {
pub title: String,
pub id: u64,
pub director: Option<String>,
pub year: Option<String>,
pub language: String,
}
impl MovieEntry {
// Create movie entry from results
pub fn from(movie: MovieShort) -> MovieEntry {
MovieEntry {
title: movie.inner.title,
id: movie.inner.id,
director: None,
year: movie
.inner
.release_date
.map(|date| date.format("%Y").to_string()),
language: get_long_lang(movie.inner.original_language.as_str()),
}
}
// Generate desired filename from movie entry
pub fn rename_format(&self, mut format: String) -> String {
// Try to sanitize the title to avoid some characters
let mut title = self.title.clone();
title = sanitize(title);
title.truncate(159);
format = format.replace("{title}", title.as_str());
format = match &self.year {
Some(year) => format.replace("{year}", year.as_str()),
None => format.replace("{year}", ""),
};
format = match &self.director {
Some(name) => {
// Try to sanitize the director's name to avoid some characters
let mut director = name.clone();
director = sanitize(director);
director.truncate(63);
format.replace("{director}", director.as_str())
}
None => format.replace("{director}", ""),
};
// Try to clean extra spaces and such
format = format.trim_matches(|c| "- ".contains(c)).to_string();
while format.contains("- -") {
format = format.replace("- -", "-");
}
format
}
}
// Implement display trait for movie entries
impl fmt::Display for MovieEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buffer = String::new();
buffer.push_str(&format!("{} ", self.title));
if let Some(year) = &self.year {
buffer.push_str(&format!("({year}), "));
}
buffer.push_str(&format!(
"Language: {}, ",
get_long_lang(self.language.as_str())
));
if let Some(director) = &self.director {
buffer.push_str(&format!("Directed by: {director}, "));
}
buffer.push_str(&format!("TMDB ID: {}", self.id));
// buffer.push_str(&format!("Synopsis: {}", self.overview));
write!(f, "{buffer}")
}
}
pub struct Language {
pub short: String,
pub long: String,
}
impl Language {
// Generate a vector of Language entries of all supported languages
pub fn generate_list() -> Vec<Language> {
let mut list = Vec::new();
for lang in ["en", "hi", "bn", "fr", "ja", "de", "sp", "none"] {
list.push(Language {
short: String::from(lang),
long: get_long_lang(lang),
});
}
list
}
}
// Implement display trait for Language
impl fmt::Display for Language {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.long)
}
}
// Get long name of a language
pub fn get_long_lang(short: &str) -> String {
// List used from https://gist.github.com/carlopires/1262033/c52ef0f7ce4f58108619508308372edd8d0bd518#file-gistfile1-txt
#[rustfmt::skip]
static LANG_LIST: [(&str, &str); 185] = [("ab", "Abkhaz"), ("aa", "Afar"), ("af", "Afrikaans"), ("ak", "Akan"), ("sq", "Albanian"),
("am", "Amharic"), ("ar", "Arabic"), ("an", "Aragonese"), ("hy", "Armenian"), ("as", "Assamese"), ("av", "Avaric"),
("ae", "Avestan"), ("ay", "Aymara"), ("az", "Azerbaijani"), ("bm", "Bambara"), ("ba", "Bashkir"), ("eu", "Basque"),
("be", "Belarusian"), ("bn", "Bengali"), ("bh", "Bihari"), ("bi", "Bislama"), ("bs", "Bosnian"), ("br", "Breton"),
("bg", "Bulgarian"), ("my", "Burmese"), ("ca", "Catalan; Valencian"), ("ch", "Chamorro"), ("ce", "Chechen"),
("ny", "Chichewa; Chewa; Nyanja"), ("zh", "Chinese"), ("cv", "Chuvash"), ("kw", "Cornish"), ("co", "Corsican"),
("cr", "Cree"), ("hr", "Croatian"), ("cs", "Czech"), ("da", "Danish"), ("dv", "Divehi; Maldivian;"), ("nl", "Dutch"),
("dz", "Dzongkha"), ("en", "English"), ("eo", "Esperanto"), ("et", "Estonian"), ("ee", "Ewe"), ("fo", "Faroese"),
("fj", "Fijian"), ("fi", "Finnish"), ("fr", "French"), ("ff", "Fula"), ("gl", "Galician"), ("ka", "Georgian"),
("de", "German"), ("el", "Greek, Modern"), ("gn", "Guaraní"), ("gu", "Gujarati"), ("ht", "Haitian"), ("ha", "Hausa"),
("he", "Hebrew (modern)"), ("hz", "Herero"), ("hi", "Hindi"), ("ho", "Hiri Motu"), ("hu", "Hungarian"), ("ia", "Interlingua"),
("id", "Indonesian"), ("ie", "Interlingue"), ("ga", "Irish"), ("ig", "Igbo"), ("ik", "Inupiaq"), ("io", "Ido"), ("is", "Icelandic"),
("it", "Italian"), ("iu", "Inuktitut"), ("ja", "Japanese"), ("jv", "Javanese"), ("kl", "Kalaallisut"), ("kn", "Kannada"),
("kr", "Kanuri"), ("ks", "Kashmiri"), ("kk", "Kazakh"), ("km", "Khmer"), ("ki", "Kikuyu, Gikuyu"), ("rw", "Kinyarwanda"),
("ky", "Kirghiz, Kyrgyz"), ("kv", "Komi"), ("kg", "Kongo"), ("ko", "Korean"), ("ku", "Kurdish"), ("kj", "Kwanyama, Kuanyama"),
("la", "Latin"), ("lb", "Luxembourgish"), ("lg", "Luganda"), ("li", "Limburgish"), ("ln", "Lingala"), ("lo", "Lao"), ("lt", "Lithuanian"),
("lu", "Luba-Katanga"), ("lv", "Latvian"), ("gv", "Manx"), ("mk", "Macedonian"), ("mg", "Malagasy"), ("ms", "Malay"), ("ml", "Malayalam"),
("mt", "Maltese"), ("mi", "Māori"), ("mr", "Marathi (Marāṭhī)"), ("mh", "Marshallese"), ("mn", "Mongolian"), ("na", "Nauru"),
("nv", "Navajo, Navaho"), ("nb", "Norwegian Bokmål"), ("nd", "North Ndebele"), ("ne", "Nepali"), ("ng", "Ndonga"),
("nn", "Norwegian Nynorsk"), ("no", "Norwegian"), ("ii", "Nuosu"), ("nr", "South Ndebele"), ("oc", "Occitan"), ("oj", "Ojibwe, Ojibwa"),
("cu", "Old Church Slavonic"), ("om", "Oromo"), ("or", "Oriya"), ("os", "Ossetian, Ossetic"), ("pa", "Panjabi, Punjabi"), ("pi", "Pāli"),
("fa", "Persian"), ("pl", "Polish"), ("ps", "Pashto, Pushto"), ("pt", "Portuguese"), ("qu", "Quechua"), ("rm", "Romansh"), ("rn", "Kirundi"),
("ro", "Romanian, Moldavan"), ("ru", "Russian"), ("sa", "Sanskrit (Saṁskṛta)"), ("sc", "Sardinian"), ("sd", "Sindhi"), ("se", "Northern Sami"),
("sm", "Samoan"), ("sg", "Sango"), ("sr", "Serbian"), ("gd", "Scottish Gaelic"), ("sn", "Shona"), ("si", "Sinhala, Sinhalese"), ("sk", "Slovak"),
("sl", "Slovene"), ("so", "Somali"), ("st", "Southern Sotho"), ("es", "Spanish; Castilian"), ("su", "Sundanese"), ("sw", "Swahili"),
("ss", "Swati"), ("sv", "Swedish"), ("ta", "Tamil"), ("te", "Telugu"), ("tg", "Tajik"), ("th", "Thai"), ("ti", "Tigrinya"), ("bo", "Tibetan"),
("tk", "Turkmen"), ("tl", "Tagalog"), ("tn", "Tswana"), ("to", "Tonga"), ("tr", "Turkish"), ("ts", "Tsonga"), ("tt", "Tatar"), ("tw", "Twi"),
("ty", "Tahitian"), ("ug", "Uighur, Uyghur"), ("uk", "Ukrainian"), ("ur", "Urdu"), ("uz", "Uzbek"), ("ve", "Venda"), ("vi", "Vietnamese"),
("vo", "Volapük"), ("wa", "Walloon"), ("cy", "Welsh"), ("wo", "Wolof"), ("fy", "Western Frisian"), ("xh", "Xhosa"), ("yi", "Yiddish"),
("yo", "Yoruba"), ("za", "Zhuang, Chuang"), ("zu", "Zulu"), ("none", "None")];
let long = LANG_LIST
.iter()
.filter(|x| x.0 == short)
.map(|x| x.1)
.next();
if let Some(longlang) = long {
String::from(longlang)
} else {
String::from(short)
}
}
// Sanitize filename so that there are no errors while
// creating a file/directory
fn sanitize(input: String) -> String {
const AVOID: &str = "^~*+=`/\\\"><|";
let mut out = input;
out.retain(|c| !AVOID.contains(c));
out = out.replace(':', "");
out = out.replace('?', "");
out
}