Compare commits

..

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

10 changed files with 588 additions and 1438 deletions

1566
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
[package]
name = "movie-rename"
version = "2.3.2"
build = "build.rs"
version = "2.1.0"
edition = "2021"
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
license = "GPL-3.0"
@ -17,15 +16,10 @@ categories = ["command-line-utilities"]
[dependencies]
torrent-name-parser = "0.12.1"
tmdb-api = "0.8.0"
inquire = "0.7.5"
tmdb-api = "0.4.0"
inquire = "0.6.2"
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"
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"] }
[profile.release]
strip = true

View file

@ -2,7 +2,7 @@ PREFIX := /usr/local
PKGNAME := movie-rename
build:
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.34
cargo build --release
build-debug:
cargo build
@ -19,6 +19,6 @@ uninstall:
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
tar --transform 's/.*\///g' -czf $(PKGNAME).tar.gz target/release/$(PKGNAME) $(PKGNAME).1
.PHONY: build build-debug install clean uninstall aur

View file

@ -30,8 +30,7 @@ sudo make install
- 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 `-nd` or `-dn` does a dry run in directory mode.
- Passing `--help` or `-h` shows help and exits.
- Passing `--version` or `-v` shows version and exits.

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

@ -17,6 +17,9 @@ Performs a dry run, without actually renaming anything.
-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
-dn, -nd
Performs a dry run in directory mode.
.TP
-h, --help
Print help information.
.TP

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,39 +1,39 @@
use inquire::{
ui::{Color, IndexPrefix, RenderConfig, Styled},
InquireError, Select,
Select,
};
use std::{collections::HashMap, fs, path::Path};
use std::{collections::HashMap, fs, path::Path, process::exit};
use tmdb_api::{
client::{reqwest::ReqwestExecutor, Client},
movie::{credits::MovieCredits, search::MovieSearch},
prelude::Command,
Client,
};
use torrent_name_parser::Metadata;
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
pub async fn process_file(
filename: &String,
tmdb: &Client<ReqwestExecutor>,
tmdb: &Client,
pattern: &str,
dry_run: bool,
lucky: bool,
movie_list: Option<&HashMap<String, Option<String>>>,
movie_list: Option<&HashMap<String, String>>,
) -> (String, String, bool) {
// 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();
let mut parent = String::from("");
if let Some(parts) = filename.rsplit_once('/') {
{
parent = String::from(parts.0);
file_base = String::from(parts.1);
parent = parts.0.to_string();
file_base = parts.1.to_string();
}
}
@ -53,7 +53,7 @@ pub async fn process_file(
Some(list) => {
if list.contains_key(&filename_without_ext) {
preprocessed = true;
list[&filename_without_ext].clone().unwrap_or_default()
list[&filename_without_ext].clone()
} else {
String::new()
}
@ -62,8 +62,11 @@ pub async fn process_file(
// 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);
eprintln!(
" Ignoring {} as per previous choice for related files...",
file_base
);
return (filename_without_ext, "".to_string(), false);
}
// Parse the filename for metadata
@ -72,10 +75,10 @@ pub async fn process_file(
// 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}...");
println!(" Processing {}...", file_base);
} else {
println!(" Ignoring {file_base}...");
return (filename_without_ext, None, false);
println!(" Ignoring {}...", file_base);
return (filename_without_ext, "".to_string(), false);
}
// Only do the TMDb API stuff if it's not preprocessed
@ -88,7 +91,7 @@ pub async fn process_file(
let results = match reply {
Ok(res) => Ok(res.results),
Err(e) => {
eprintln!(" There was an error while searching {file_base}!");
eprintln!(" There was an error while searching {}!", file_base);
Err(e)
}
};
@ -113,7 +116,7 @@ pub async fn process_file(
if let Some(pos) = directors_text.rfind(',') {
directors_text.replace_range(pos..pos + 2, " and ");
}
movie_details.director = Some(directors_text);
movie_details.director = directors_text;
}
}
}
@ -123,18 +126,13 @@ pub async fn process_file(
// 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);
eprintln!(" Could not find any entries matching {}!", file_base);
return (filename_without_ext, "".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
choice = match Select::new(
format!(" Possible choices for {file_base}:").as_str(),
let choice = match Select::new(
format!(" Possible choices for {}:", file_base).as_str(),
movie_list,
)
.prompt()
@ -142,17 +140,12 @@ pub async fn process_file(
Ok(movie) => movie,
Err(error) => {
println!(" {error}");
let flag = matches!(
error,
InquireError::OperationCanceled | InquireError::OperationInterrupted
);
return (filename_without_ext, None, flag);
return (filename_without_ext, "".to_string(), false);
}
};
};
// Create the new name
new_name_base = choice.rename_format(String::from(pattern));
new_name_base = choice.rename_format(pattern.to_string());
} else {
println!(" Using previous choice for related files...");
}
@ -181,18 +174,18 @@ pub async fn process_file(
// 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}");
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}");
if parent != *"" {
new_name = format!("{}/{}", parent, new_name);
}
// Process the renaming
if *filename == new_name {
println!(" [file] '{file_base}' already has correct name.");
println!(" [file] '{}' already has correct name.", file_base);
} 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
if !dry_run {
if !Path::new(new_name.as_str()).is_file() {
@ -202,11 +195,72 @@ pub async fn process_file(
}
}
}
(filename_without_ext, Some(new_name_base), true)
(filename_without_ext, new_name_base, true)
}
// 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
fn get_render_config() -> RenderConfig<'static> {
fn get_render_config() -> RenderConfig {
let mut render_config = RenderConfig::default();
render_config.option_index_prefix = IndexPrefix::Simple;

View file

@ -1,35 +1,23 @@
use load_file::{self, load_str};
use std::{collections::HashMap, env, fs, path::Path, process::exit};
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
use tmdb_api::Client;
// Import all the modules
mod functions;
use functions::process_file;
mod args;
use functions::{process_args, process_file};
mod structs;
#[tokio::main]
async fn main() {
// Process the passed arguments
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"];
// Read arguments from commandline
let args: Vec<String> = env::args().collect();
// Print some message when flags are set.
if flag_dry_run {
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...")
}
// Process the passed arguments
let (entries, settings) = process_args(args);
// Try to read config file, or display error
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME"));
if config_file == "$HOME" {
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or("$HOME".to_string());
if config_file == *"$HOME" {
config_file = env::var("$HOME").unwrap();
config_file.push_str("/.config");
}
@ -50,26 +38,26 @@ async fn main() {
}
// Create TMDb object for API calls
let tmdb = Client::<ReqwestExecutor>::new(String::from(api_key));
let tmdb = Client::new(api_key.to_string());
// Iterate over entries
for entry in entries {
// Check if the file/directory exists on disk and run necessary commands
match flag_directory {
match settings["directory"] {
// Normal file
false => {
if Path::new(entry.as_str()).is_file() {
// 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"], None).await;
} else {
eprintln!("The file {entry} wasn't found on disk, skipping...");
eprintln!("The file {} wasn't found on disk, skipping...", entry);
continue;
}
}
// Directory
true => {
if Path::new(entry.as_str()).is_dir() {
println!("Processing files inside the directory {entry}...");
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()) {
@ -81,56 +69,46 @@ async fn main() {
&filename,
&tmdb,
pattern,
flag_dry_run,
flag_lucky,
settings["dry_run"],
Some(&movie_list),
)
.await;
// if movie_name_temp.is_empty() {
// continue;
// }
if add_to_list {
movie_list.insert(filename_without_ext, movie_name_temp);
}
}
}
} else {
eprintln!("There was an error accessing the directory {entry}!");
eprintln!("There was an error accessing the directory {}!", entry);
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
match movie_name {
None => {
eprintln!("Not renaming directory as only movie was skipped.");
}
Some(name) => {
if entry_clean == name {
println!(
"[directory] '{entry_clean}' already has correct name."
);
if entry_clean == movie_name {
println!("[directory] '{}' already has correct name.", entry_clean);
} else {
println!("[directory] '{entry_clean}' -> '{name}'",);
if !flag_dry_run {
if !Path::new(name.as_str()).is_dir() {
fs::rename(entry, name)
println!("[directory] '{}' -> '{}'", entry_clean, movie_name);
if !settings["dry_run"] {
if !Path::new(movie_name.as_str()).is_dir() {
fs::rename(entry, movie_name)
.expect("Unable to rename directory!");
} else {
eprintln!(
"Destination directory already exists, skipping..."
);
}
}
eprintln!("Destination directory already exists, skipping...");
}
}
}
} else {
eprintln!("Could not determine how to rename the directory {entry}!");
eprintln!("Could not determine how to rename the directory {}!", entry);
}
} else {
eprintln!("The directory {entry} wasn't found on disk, skipping...");
eprintln!("The directory {} wasn't found on disk, skipping...", entry);
continue;
}
}

View file

@ -5,8 +5,8 @@ use tmdb_api::movie::MovieShort;
pub struct MovieEntry {
pub title: String,
pub id: u64,
pub director: Option<String>,
pub year: Option<String>,
pub director: String,
pub year: String,
pub language: String,
}
@ -16,38 +16,39 @@ impl MovieEntry {
MovieEntry {
title: movie.inner.title,
id: movie.inner.id,
director: None,
year: movie
.inner
.release_date
.map(|date| date.format("%Y").to_string()),
director: String::from("N/A"),
year: match movie.inner.release_date {
Some(date) => date.format("%Y").to_string(),
_ => "N/A".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 {
const PATTERN: &str = "^~#%$*+={}?@'`/\\\"><|:&!";
// Try to sanitize the title to avoid some characters
let mut title = self.title.clone();
title = sanitize(title);
title.retain(|c| !PATTERN.contains(c));
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())
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" {
// Try to sanitize the director's name to avoid some characters
let mut director = self.director.clone();
director.retain(|c| !PATTERN.contains(c));
director.truncate(63);
format = format.replace("{director}", director.as_str());
} else {
format = format.replace("{director}", "");
}
None => format.replace("{director}", ""),
};
// Try to clean extra spaces and such
format = format.trim_matches(|c| "- ".contains(c)).to_string();
@ -64,23 +65,15 @@ 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!("({}), ", self.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!("Directed by: {}, ", self.director));
buffer.push_str(&format!("TMDB ID: {}", self.id));
// buffer.push_str(&format!("Synopsis: {}", self.overview));
write!(f, "{buffer}")
write!(f, "{}", buffer)
}
}
@ -95,7 +88,7 @@ impl Language {
let mut list = Vec::new();
for lang in ["en", "hi", "bn", "fr", "ja", "de", "sp", "none"] {
list.push(Language {
short: String::from(lang),
short: lang.to_string(),
long: get_long_lang(lang),
});
}
@ -112,60 +105,16 @@ impl fmt::Display for Language {
// 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
let long = match short {
"en" => "English",
"hi" => "Hindi",
"bn" => "Bengali",
"fr" => "French",
"ja" => "Japanese",
"de" => "German",
"sp" => "Spanish",
"none" => "None",
other => other,
};
long.to_string()
}