mirror of
https://github.com/SinTan1729/movie-rename.git
synced 2025-04-19 18:30:01 -05:00
Compare commits
No commits in common. "main" and "1.2.4" have entirely different histories.
11 changed files with 1365 additions and 1735 deletions
2442
Cargo.lock
generated
2442
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "movie-rename"
|
name = "movie-rename"
|
||||||
version = "2.3.2"
|
version = "1.2.4"
|
||||||
build = "build.rs"
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
|
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
@ -16,16 +15,10 @@ 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"
|
inquire = "0.5.3"
|
||||||
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]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|
6
LICENSE
6
LICENSE
|
@ -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.
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -2,7 +2,7 @@ PREFIX := /usr/local
|
||||||
PKGNAME := movie-rename
|
PKGNAME := movie-rename
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.34
|
cargo build --release
|
||||||
|
|
||||||
build-debug:
|
build-debug:
|
||||||
cargo build
|
cargo build
|
||||||
|
@ -19,6 +19,6 @@ uninstall:
|
||||||
rm -f "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
|
rm -f "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
|
||||||
|
|
||||||
aur: build
|
aur: build
|
||||||
tar --transform 's/.*\///g' -czf $(PKGNAME).tar.gz target/x86_64-unknown-linux-gnu/release/$(PKGNAME) target/autocomplete/* $(PKGNAME).1
|
tar --transform 's/.*\///g' -czf $(PKGNAME).tar.gz target/release/$(PKGNAME) $(PKGNAME).1
|
||||||
|
|
||||||
.PHONY: build build-debug install clean uninstall aur
|
.PHONY: build build-debug install clean uninstall aur
|
||||||
|
|
|
@ -26,12 +26,11 @@ sudo make install
|
||||||
- There needs to be a config file named `config` in the `$XDG_CONFIG_HOME/movie-rename/` directory.
|
- 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).
|
- 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 `--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 `--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.
|
- Passing `-nd` or `-dn` does a dry run in directory mode.
|
||||||
- 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 `--help` or `-h` shows help and exits.
|
||||||
- Passing `--version` or `-v` shows version and exits.
|
- Passing `--version` or `-v` shows version and exits.
|
||||||
|
|
||||||
|
|
21
build.rs
21
build.rs
|
@ -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(())
|
|
||||||
}
|
|
|
@ -17,6 +17,9 @@ Performs a dry run, without actually renaming anything.
|
||||||
-d, --directory
|
-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.
|
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
|
.TP
|
||||||
|
-dn, -nd
|
||||||
|
Performs a dry run in directory mode.
|
||||||
|
.TP
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help information.
|
Print help information.
|
||||||
.TP
|
.TP
|
||||||
|
|
46
src/args.rs
46
src/args.rs
|
@ -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)
|
|
||||||
}
|
|
246
src/functions.rs
246
src/functions.rs
|
@ -1,165 +1,104 @@
|
||||||
use inquire::{
|
use inquire::{
|
||||||
ui::{Color, IndexPrefix, RenderConfig, Styled},
|
ui::{Color, IndexPrefix, RenderConfig, Styled},
|
||||||
InquireError, Select,
|
Select,
|
||||||
};
|
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
|
||||||
use tmdb_api::{
|
|
||||||
client::{reqwest::ReqwestExecutor, Client},
|
|
||||||
movie::{credits::MovieCredits, search::MovieSearch},
|
|
||||||
prelude::Command,
|
|
||||||
};
|
};
|
||||||
|
use std::{collections::HashMap, fs, path::Path, process::exit};
|
||||||
|
use tmdb::{model::*, themoviedb::*};
|
||||||
use torrent_name_parser::Metadata;
|
use torrent_name_parser::Metadata;
|
||||||
|
|
||||||
use crate::structs::{get_long_lang, Language, MovieEntry};
|
use crate::structs::{get_long_lang, Language, MovieEntry};
|
||||||
|
|
||||||
|
// Get the version from Cargo.toml
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
// Function to process movie entries
|
// Function to process movie entries
|
||||||
pub async fn process_file(
|
pub fn process_file(
|
||||||
filename: &String,
|
filename: &String,
|
||||||
tmdb: &Client<ReqwestExecutor>,
|
tmdb: &TMDb,
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
lucky: bool,
|
) -> (String, 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
|
// Set RenderConfig for the menu items
|
||||||
inquire::set_global_render_config(get_render_config());
|
inquire::set_global_render_config(get_render_config());
|
||||||
|
|
||||||
// Get the basename
|
// Get the basename
|
||||||
let mut file_base = String::from(filename);
|
let mut file_base = String::from(filename);
|
||||||
let mut parent = String::new();
|
let mut parent = String::from("");
|
||||||
if let Some(parts) = filename.rsplit_once('/') {
|
match filename.rsplit_once("/") {
|
||||||
{
|
Some(parts) => {
|
||||||
parent = String::from(parts.0);
|
parent = parts.0.to_string();
|
||||||
file_base = String::from(parts.1);
|
file_base = parts.1.to_string();
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
|
println!(" Processing {}...", file_base);
|
||||||
// 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
|
// Parse the filename for metadata
|
||||||
let metadata = Metadata::from(file_base.as_str()).expect(" Could not parse filename!");
|
let metadata = Metadata::from(file_base.as_str()).expect(" Could not parse filename!");
|
||||||
|
|
||||||
// Process only if it's a valid file format
|
// Process only if it's a valid file format
|
||||||
let mut extension = metadata.extension().unwrap_or("").to_string();
|
let mut extension = metadata.extension().unwrap_or("").to_string();
|
||||||
if ["mp4", "avi", "mkv", "flv", "m4a", "srt", "ssa"].contains(&extension.as_str()) {
|
if !["mp4", "avi", "mkv", "flv", "m4a", "srt", "ssa"].contains(&extension.as_str()) {
|
||||||
println!(" Processing {file_base}...");
|
println!(" Ignoring {}...", file_base);
|
||||||
} else {
|
return ("n/a".to_string(), false);
|
||||||
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
|
// Search using the TMDb API
|
||||||
let year = metadata.year().map(|y| y as u16);
|
let mut search = tmdb.search();
|
||||||
let search = MovieSearch::new(metadata.title().to_string()).with_year(year);
|
search.title(metadata.title());
|
||||||
let reply = search.execute(tmdb).await;
|
|
||||||
|
|
||||||
let results = match reply {
|
// Check if year is present in filename
|
||||||
Ok(res) => Ok(res.results),
|
if let Some(year) = metadata.year() {
|
||||||
Err(e) => {
|
search.year(year as u64);
|
||||||
eprintln!(" There was an error while searching {file_base}!");
|
}
|
||||||
Err(e)
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
if let Ok(search_results) = search.execute() {
|
||||||
|
results = search_results.results;
|
||||||
|
} else {
|
||||||
|
eprintln!(" There was an error while searching {}!", file_base);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
||||||
// Create movie entry from the result
|
// Create movie entry from the result
|
||||||
if results.is_ok() {
|
for result in results {
|
||||||
for result in results.unwrap() {
|
|
||||||
let mut movie_details = MovieEntry::from(result);
|
let mut movie_details = MovieEntry::from(result);
|
||||||
// Get director's name, if needed
|
// Get director's name, if needed
|
||||||
if pattern.contains("{director}") {
|
if pattern.contains("{director}") {
|
||||||
let credits_search = MovieCredits::new(movie_details.id);
|
let with_credits: Result<Movie, _> =
|
||||||
let credits_reply = credits_search.execute(tmdb).await;
|
tmdb.fetch().id(movie_details.id).append_credits().execute();
|
||||||
if credits_reply.is_ok() {
|
if let Ok(movie) = with_credits {
|
||||||
let mut crew = credits_reply.unwrap().crew;
|
if let Some(cre) = movie.credits {
|
||||||
// Only keep the director(s)
|
let mut directors = cre.crew;
|
||||||
crew.retain(|x| x.job == *"Director");
|
directors.retain(|x| x.job == "Director");
|
||||||
if !crew.is_empty() {
|
for person in directors {
|
||||||
let directors: Vec<String> =
|
movie_details.director = person.name;
|
||||||
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);
|
movie_list.push(movie_details);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing is found, skip
|
// If nothing is found, skip
|
||||||
if movie_list.is_empty() {
|
if movie_list.len() == 0 {
|
||||||
eprintln!(" Could not find any entries matching {file_base}!");
|
eprintln!(" Could not find any entries matching {}!", file_base);
|
||||||
return (filename_without_ext, None, true);
|
return ("".to_string(), 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
|
// Choose from the possible entries
|
||||||
choice = match Select::new(
|
let choice = Select::new(
|
||||||
format!(" Possible choices for {file_base}:").as_str(),
|
format!(" Possible choices for {}:", file_base).as_str(),
|
||||||
movie_list,
|
movie_list,
|
||||||
)
|
)
|
||||||
.prompt()
|
.prompt()
|
||||||
{
|
.expect(" Invalid choice!");
|
||||||
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
|
// Handle the case for subtitle files
|
||||||
|
let mut is_subtitle = false;
|
||||||
if ["srt", "ssa"].contains(&extension.as_str()) {
|
if ["srt", "ssa"].contains(&extension.as_str()) {
|
||||||
// Try to detect if there's already language info in the filename, else ask user to choose
|
// Try to detect if there's already language info in the filename, else ask user to choose
|
||||||
|
let filename_parts: Vec<&str> = filename.rsplit(".").collect();
|
||||||
if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
|
if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
|
||||||
println!(
|
println!(
|
||||||
" Keeping language {} as detected in the subtitle file's extension...",
|
" Keeping language {} as detected in the subtitle file's extension...",
|
||||||
|
@ -172,41 +111,104 @@ pub async fn process_file(
|
||||||
Select::new(" Choose the language for the subtitle file:", lang_list)
|
Select::new(" Choose the language for the subtitle file:", lang_list)
|
||||||
.prompt()
|
.prompt()
|
||||||
.expect(" Invalid choice!");
|
.expect(" Invalid choice!");
|
||||||
if lang_choice.short != *"none" {
|
if lang_choice.short != "none".to_string() {
|
||||||
extension = format!("{}.{}", lang_choice.short, extension);
|
extension = format!("{}.{}", lang_choice.short, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is_subtitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add extension and stuff to the new name
|
// Create the new name
|
||||||
|
let new_name_base = choice.rename_format(pattern.to_string());
|
||||||
let mut new_name_with_ext = new_name_base.clone();
|
let mut new_name_with_ext = new_name_base.clone();
|
||||||
if !extension.is_empty() {
|
if extension != "" {
|
||||||
new_name_with_ext = format!("{new_name_with_ext}.{extension}");
|
new_name_with_ext = format!("{}.{}", new_name_with_ext, extension);
|
||||||
}
|
}
|
||||||
let mut new_name = new_name_with_ext.clone();
|
let mut new_name = String::from(new_name_with_ext.clone());
|
||||||
if !parent.is_empty() {
|
if parent != "".to_string() {
|
||||||
new_name = format!("{parent}/{new_name}");
|
new_name = format!("{}/{}", parent, new_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the renaming
|
// Process the renaming
|
||||||
if *filename == new_name {
|
if *filename == new_name {
|
||||||
println!(" [file] '{file_base}' already has correct name.");
|
println!(" [file] '{}' already has correct name.", file_base);
|
||||||
} else {
|
} else {
|
||||||
println!(" [file] '{file_base}' -> '{new_name_with_ext}'");
|
println!(" [file] '{}' -> '{}'", file_base, new_name_with_ext);
|
||||||
// Only do the rename of --dry-run isn't passed
|
// Only do the rename of --dry-run isn't passed
|
||||||
if !dry_run {
|
if dry_run == false {
|
||||||
if !Path::new(new_name.as_str()).is_file() {
|
if Path::new(new_name.as_str()).is_file() == false {
|
||||||
fs::rename(filename, new_name.as_str()).expect(" Unable to rename file!");
|
fs::rename(filename, new_name.as_str()).expect(" Unable to rename file!");
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" Destination file already exists, skipping...");
|
eprintln!(" Destination file already exists, skipping...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(filename_without_ext, Some(new_name_base), true)
|
(new_name_base, is_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to process the passed arguments
|
||||||
|
pub fn process_args(mut args: Vec<String>) -> (Vec<String>, 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 <filename(s)> [-n|--dry-run] [-d|--directory] [-v|--version]"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" There needs to be a config file named config in the $XDG_CONFIG_HOME/movie-rename/ directory."
|
||||||
|
);
|
||||||
|
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 or -d assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles.");
|
||||||
|
println!(" Passing --dry-run or -n does a dry tun and only prints out the new names, without actually doing anything.");
|
||||||
|
println!(" Passing -nd or -dn does a dry run in directory mode.");
|
||||||
|
println!(" Passing --version or -v shows version and exits.");
|
||||||
|
println!(" Pass --help to get this again.");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
"--version" | "-v" => {
|
||||||
|
println!("movie-rename {}", VERSION);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
"-nd" | "-dn" => {
|
||||||
|
println!("Doing a dry run in directory mode...");
|
||||||
|
settings.entry("dry_run").and_modify(|x| *x = true);
|
||||||
|
settings.entry("directory").and_modify(|x| *x = true);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
if other.starts_with("-") {
|
||||||
|
eprintln!("Unknown argument passed: {}", other);
|
||||||
|
exit(1);
|
||||||
|
} else {
|
||||||
|
entries.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(entries, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderConfig for the menu items
|
// RenderConfig for the menu items
|
||||||
fn get_render_config() -> RenderConfig<'static> {
|
fn get_render_config() -> RenderConfig {
|
||||||
let mut render_config = RenderConfig::default();
|
let mut render_config = RenderConfig::default();
|
||||||
render_config.option_index_prefix = IndexPrefix::Simple;
|
render_config.option_index_prefix = IndexPrefix::Simple;
|
||||||
|
|
||||||
|
|
130
src/main.rs
130
src/main.rs
|
@ -1,136 +1,106 @@
|
||||||
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, fs, path::Path, process::exit};
|
||||||
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
|
use tmdb::themoviedb::*;
|
||||||
|
|
||||||
// Import all the modules
|
// Import all the modules
|
||||||
mod functions;
|
mod functions;
|
||||||
use functions::process_file;
|
use functions::{process_args, process_file};
|
||||||
mod args;
|
|
||||||
mod structs;
|
mod structs;
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() {
|
// Read arguments from commandline
|
||||||
// Process the passed arguments
|
let args: Vec<String> = env::args().collect();
|
||||||
let (entries, settings) = args::process_args();
|
|
||||||
let flag_dry_run = settings["dry-run"];
|
|
||||||
let flag_directory = settings["directory"];
|
|
||||||
let flag_lucky = settings["i-feel-lucky"];
|
|
||||||
|
|
||||||
// Print some message when flags are set.
|
// Process the passed arguments
|
||||||
if flag_dry_run {
|
let (entries, settings) = process_args(args);
|
||||||
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...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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("/.config");
|
||||||
}
|
}
|
||||||
config_file.push_str("/movie-rename/config");
|
config_file.push_str("/movie-rename/config");
|
||||||
|
|
||||||
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(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Iterate over entries
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
// Check if the file/directory exists on disk and run necessary commands
|
// Check if the file/directory exists on disk and run necessary commands
|
||||||
match flag_directory {
|
// TODO: Detect subtitle files with same name/metadata and process them automatically without repeated input
|
||||||
|
match settings["directory"] {
|
||||||
// Normal file
|
// Normal file
|
||||||
false => {
|
false => {
|
||||||
if Path::new(entry.as_str()).is_file() {
|
if Path::new(entry.as_str()).is_file() == true {
|
||||||
// Process the filename for movie entries
|
// Process the filename for movie entries
|
||||||
process_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await;
|
process_file(&entry, &tmdb, pattern, settings["dry_run"]);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("The file {entry} wasn't found on disk, skipping...");
|
eprintln!("The file {} wasn't found on disk, skipping...", entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Directory
|
// Directory
|
||||||
true => {
|
true => {
|
||||||
if Path::new(entry.as_str()).is_dir() {
|
if Path::new(entry.as_str()).is_dir() == true {
|
||||||
println!("Processing files inside the directory {entry}...");
|
println!("Processing files inside the directory {}...", entry);
|
||||||
let mut movie_list = HashMap::new();
|
let mut movie_count = 0;
|
||||||
|
let mut movie_name = String::new();
|
||||||
if let Ok(files_in_dir) = fs::read_dir(entry.as_str()) {
|
if let Ok(files_in_dir) = fs::read_dir(entry.as_str()) {
|
||||||
for file in files_in_dir {
|
for file in files_in_dir {
|
||||||
if file.is_ok() {
|
if file.is_ok() {
|
||||||
let filename = file.unwrap().path().display().to_string();
|
let (movie_name_temp, is_subtitle) = process_file(
|
||||||
let (filename_without_ext, movie_name_temp, add_to_list) =
|
&format!("{}", file.unwrap().path().display()),
|
||||||
process_file(
|
|
||||||
&filename,
|
|
||||||
&tmdb,
|
&tmdb,
|
||||||
pattern,
|
pattern,
|
||||||
flag_dry_run,
|
settings["dry_run"],
|
||||||
flag_lucky,
|
);
|
||||||
Some(&movie_list),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if add_to_list {
|
if movie_name_temp == "n/a".to_string() {
|
||||||
movie_list.insert(filename_without_ext, movie_name_temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("There was an error accessing the directory {entry}!");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
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
|
if is_subtitle == false {
|
||||||
match movie_name {
|
movie_count += 1;
|
||||||
None => {
|
movie_name = movie_name_temp;
|
||||||
eprintln!("Not renaming directory as only movie was skipped.");
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(name) => {
|
|
||||||
if entry_clean == name {
|
|
||||||
println!(
|
|
||||||
"[directory] '{entry_clean}' already has correct name."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
println!("[directory] '{entry_clean}' -> '{name}'",);
|
eprintln!("There was an error accessing the directory {}!", entry);
|
||||||
if !flag_dry_run {
|
continue;
|
||||||
if !Path::new(name.as_str()).is_dir() {
|
}
|
||||||
fs::rename(entry, name)
|
if movie_count == 1 {
|
||||||
|
let entry_clean = entry.trim_end_matches("/");
|
||||||
|
if entry_clean == movie_name {
|
||||||
|
println!("[directory] '{}' already has correct name.", entry_clean);
|
||||||
|
} else {
|
||||||
|
println!("[directory] '{}' -> '{}'", entry_clean, movie_name);
|
||||||
|
if settings["dry_run"] == false {
|
||||||
|
if Path::new(movie_name.as_str()).is_dir() == false {
|
||||||
|
fs::rename(entry, movie_name)
|
||||||
.expect("Unable to rename directory!");
|
.expect("Unable to rename directory!");
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!("Destination directory already exists, skipping...");
|
||||||
"Destination directory already exists, skipping..."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Could not determine how to rename the directory {entry}!");
|
eprintln!("Could not determine how to rename the directory {}!", entry);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("The directory {entry} wasn't found on disk, skipping...");
|
eprintln!("The directory {} wasn't found on disk, skipping...", entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
142
src/structs.rs
142
src/structs.rs
|
@ -1,60 +1,40 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use tmdb_api::movie::MovieShort;
|
use tmdb::model::*;
|
||||||
|
|
||||||
// Struct for movie entries
|
// Struct for movie entries
|
||||||
pub struct MovieEntry {
|
pub struct MovieEntry {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub director: Option<String>,
|
pub director: String,
|
||||||
pub year: Option<String>,
|
pub year: String,
|
||||||
pub language: String,
|
pub language: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MovieEntry {
|
impl MovieEntry {
|
||||||
// Create movie entry from results
|
// Create movie entry from results
|
||||||
pub fn from(movie: MovieShort) -> MovieEntry {
|
pub fn from(movie: SearchMovie) -> MovieEntry {
|
||||||
MovieEntry {
|
MovieEntry {
|
||||||
title: movie.inner.title,
|
title: movie.title,
|
||||||
id: movie.inner.id,
|
id: movie.id,
|
||||||
director: None,
|
director: String::from("N/A"),
|
||||||
year: movie
|
year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")),
|
||||||
.inner
|
language: get_long_lang(movie.original_language.as_str()),
|
||||||
.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
|
// Generate desired filename from movie entry
|
||||||
pub fn rename_format(&self, mut format: String) -> String {
|
pub fn rename_format(&self, mut format: String) -> String {
|
||||||
// Try to sanitize the title to avoid some characters
|
format = format.replace("{title}", self.title.as_str());
|
||||||
let mut title = self.title.clone();
|
if self.year.as_str() != "N/A" {
|
||||||
title = sanitize(title);
|
format = format.replace("{year}", self.year.as_str());
|
||||||
title.truncate(159);
|
} else {
|
||||||
format = format.replace("{title}", title.as_str());
|
format = format.replace("{year}", "");
|
||||||
|
|
||||||
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}", ""),
|
if self.director.as_str() != "N/A" {
|
||||||
};
|
format = format.replace("{director}", self.director.as_str());
|
||||||
|
} else {
|
||||||
// Try to clean extra spaces and such
|
format = format.replace("{director}", "");
|
||||||
format = format.trim_matches(|c| "- ".contains(c)).to_string();
|
|
||||||
while format.contains("- -") {
|
|
||||||
format = format.replace("- -", "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
format
|
format
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,23 +44,15 @@ impl fmt::Display for MovieEntry {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
buffer.push_str(&format!("{} ", self.title));
|
buffer.push_str(&format!("{} ", self.title));
|
||||||
|
buffer.push_str(&format!("({}), ", self.year));
|
||||||
if let Some(year) = &self.year {
|
|
||||||
buffer.push_str(&format!("({year}), "));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push_str(&format!(
|
buffer.push_str(&format!(
|
||||||
"Language: {}, ",
|
"Language: {}, ",
|
||||||
get_long_lang(self.language.as_str())
|
get_long_lang(self.language.as_str())
|
||||||
));
|
));
|
||||||
|
buffer.push_str(&format!("Directed by: {}, ", self.director));
|
||||||
if let Some(director) = &self.director {
|
buffer.push_str(&format!("TMDb ID: {}", self.id));
|
||||||
buffer.push_str(&format!("Directed by: {director}, "));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push_str(&format!("TMDB ID: {}", self.id));
|
|
||||||
// buffer.push_str(&format!("Synopsis: {}", self.overview));
|
// buffer.push_str(&format!("Synopsis: {}", self.overview));
|
||||||
write!(f, "{buffer}")
|
write!(f, "{}", buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +67,7 @@ impl Language {
|
||||||
let mut list = Vec::new();
|
let mut list = Vec::new();
|
||||||
for lang in ["en", "hi", "bn", "fr", "ja", "de", "sp", "none"] {
|
for lang in ["en", "hi", "bn", "fr", "ja", "de", "sp", "none"] {
|
||||||
list.push(Language {
|
list.push(Language {
|
||||||
short: String::from(lang),
|
short: lang.to_string(),
|
||||||
long: get_long_lang(lang),
|
long: get_long_lang(lang),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -112,60 +84,16 @@ impl fmt::Display for Language {
|
||||||
|
|
||||||
// Get long name of a language
|
// Get long name of a language
|
||||||
pub fn get_long_lang(short: &str) -> String {
|
pub fn get_long_lang(short: &str) -> String {
|
||||||
// List used from https://gist.github.com/carlopires/1262033/c52ef0f7ce4f58108619508308372edd8d0bd518#file-gistfile1-txt
|
let long = match short {
|
||||||
#[rustfmt::skip]
|
"en" => "English",
|
||||||
static LANG_LIST: [(&str, &str); 185] = [("ab", "Abkhaz"), ("aa", "Afar"), ("af", "Afrikaans"), ("ak", "Akan"), ("sq", "Albanian"),
|
"hi" => "Hindi",
|
||||||
("am", "Amharic"), ("ar", "Arabic"), ("an", "Aragonese"), ("hy", "Armenian"), ("as", "Assamese"), ("av", "Avaric"),
|
"bn" => "Bengali",
|
||||||
("ae", "Avestan"), ("ay", "Aymara"), ("az", "Azerbaijani"), ("bm", "Bambara"), ("ba", "Bashkir"), ("eu", "Basque"),
|
"fr" => "French",
|
||||||
("be", "Belarusian"), ("bn", "Bengali"), ("bh", "Bihari"), ("bi", "Bislama"), ("bs", "Bosnian"), ("br", "Breton"),
|
"ja" => "Japanese",
|
||||||
("bg", "Bulgarian"), ("my", "Burmese"), ("ca", "Catalan; Valencian"), ("ch", "Chamorro"), ("ce", "Chechen"),
|
"de" => "German",
|
||||||
("ny", "Chichewa; Chewa; Nyanja"), ("zh", "Chinese"), ("cv", "Chuvash"), ("kw", "Cornish"), ("co", "Corsican"),
|
"sp" => "Spanish",
|
||||||
("cr", "Cree"), ("hr", "Croatian"), ("cs", "Czech"), ("da", "Danish"), ("dv", "Divehi; Maldivian;"), ("nl", "Dutch"),
|
"none" => "None",
|
||||||
("dz", "Dzongkha"), ("en", "English"), ("eo", "Esperanto"), ("et", "Estonian"), ("ee", "Ewe"), ("fo", "Faroese"),
|
other => other,
|
||||||
("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"),
|
long.to_string()
|
||||||
("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
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue