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.3" have entirely different histories.
12 changed files with 1357 additions and 1743 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,4 +2,3 @@
|
||||||
*.mp4
|
*.mp4
|
||||||
*.srt
|
*.srt
|
||||||
*.key
|
*.key
|
||||||
*.tar.gz
|
|
||||||
|
|
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.3"
|
||||||
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.
|
||||||
|
|
5
Makefile
5
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
|
||||||
|
@ -18,7 +18,4 @@ uninstall:
|
||||||
rm -f "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
|
rm -f "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
|
||||||
rm -f "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
|
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
|
.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)
|
|
||||||
}
|
|
249
src/functions.rs
249
src/functions.rs
|
@ -1,165 +1,97 @@
|
||||||
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
|
|
||||||
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
|
// 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...");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let mut extension = metadata.extension().unwrap_or("").to_string();
|
||||||
// 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 +104,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;
|
||||||
|
|
||||||
|
|
123
src/main.rs
123
src/main.rs
|
@ -1,136 +1,101 @@
|
||||||
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),
|
if is_subtitle == false {
|
||||||
)
|
movie_count += 1;
|
||||||
.await;
|
movie_name = movie_name_temp;
|
||||||
|
|
||||||
if add_to_list {
|
|
||||||
movie_list.insert(filename_without_ext, movie_name_temp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("There was an error accessing the directory {entry}!");
|
eprintln!("There was an error accessing the directory {}!", entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if movie_list.len() == 1 {
|
if movie_count == 1 {
|
||||||
let entry_clean = entry.trim_end_matches('/');
|
let entry_clean = entry.trim_end_matches("/");
|
||||||
let movie_name = movie_list.into_values().next().unwrap();
|
if entry_clean == movie_name {
|
||||||
|
println!("[directory] '{}' already has correct name.", entry_clean);
|
||||||
// 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."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
println!("[directory] '{entry_clean}' -> '{name}'",);
|
println!("[directory] '{}' -> '{}'", entry_clean, movie_name);
|
||||||
if !flag_dry_run {
|
if settings["dry_run"] == false {
|
||||||
if !Path::new(name.as_str()).is_dir() {
|
if Path::new(movie_name.as_str()).is_dir() == false {
|
||||||
fs::rename(entry, name)
|
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