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.0.0" have entirely different histories.
12 changed files with 1249 additions and 2139 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,4 +2,3 @@
|
||||||
*.mp4
|
*.mp4
|
||||||
*.srt
|
*.srt
|
||||||
*.key
|
*.key
|
||||||
*.tar.gz
|
|
||||||
|
|
2505
Cargo.lock
generated
2505
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
33
Cargo.toml
33
Cargo.toml
|
@ -1,35 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "movie-rename"
|
name = "movie_rename"
|
||||||
version = "2.3.2"
|
version = "0.1.0"
|
||||||
build = "build.rs"
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
|
|
||||||
license = "GPL-3.0"
|
|
||||||
description = "A simple tool to rename movies, written in Rust."
|
|
||||||
homepage = "https://github.com/SinTan1729/movie-rename"
|
|
||||||
documentation = "https://docs.rs/movie-rename"
|
|
||||||
repository = "https://github.com/SinTan1729/movie-rename"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["rename", "movie", "media", "tmdb"]
|
|
||||||
categories = ["command-line-utilities"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
torrent-name-parser = "0.12.1"
|
torrent-name-parser = "0.11.0"
|
||||||
tmdb-api = "0.8.0"
|
tmdb = "3.0.0"
|
||||||
inquire = "0.7.5"
|
youchoose = "0.1.1"
|
||||||
load_file = "1.0.1"
|
load_file = "1.0.1"
|
||||||
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] }
|
|
||||||
clap = { version = "4.5.1", features = ["cargo"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
clap = { version = "4.5.1", features = ["cargo"] }
|
|
||||||
clap_complete = "4.5.1"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
strip = true
|
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
|
|
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.
|
||||||
|
|
24
Makefile
24
Makefile
|
@ -1,24 +0,0 @@
|
||||||
PREFIX := /usr/local
|
|
||||||
PKGNAME := movie-rename
|
|
||||||
|
|
||||||
build:
|
|
||||||
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.34
|
|
||||||
|
|
||||||
build-debug:
|
|
||||||
cargo build
|
|
||||||
|
|
||||||
clean:
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
install: build
|
|
||||||
install -Dm755 target/release/$(PKGNAME) "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
|
|
||||||
install -Dm644 $(PKGNAME).1 "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
rm -f "$(DESTDIR)$(PREFIX)/bin/$(PKGNAME)"
|
|
||||||
rm -f "$(DESTDIR)$(PREFIX)/man/man1/$(PKGNAME).1"
|
|
||||||
|
|
||||||
aur: build
|
|
||||||
tar --transform 's/.*\///g' -czf $(PKGNAME).tar.gz target/x86_64-unknown-linux-gnu/release/$(PKGNAME) target/autocomplete/* $(PKGNAME).1
|
|
||||||
|
|
||||||
.PHONY: build build-debug install clean uninstall aur
|
|
42
README.md
42
README.md
|
@ -1,40 +1,16 @@
|
||||||
[](https://github.com/SinTan1729/movie-rename/releases/latest/)
|
# movie-rename
|
||||||

|
|
||||||
[](https://aur.archlinux.org/packages/movie-rename-bin/)
|
|
||||||
# `movie-rename`
|
|
||||||
|
|
||||||
### A simple tool to rename movies, written in Rust.
|
## A simple tool to reame movies, written in Rust.
|
||||||
|
|
||||||
It turns a file like `Apur.Sansar.HEVC.2160p.AC3.mkv` into `Apur Sansar (1959) - Satyajit Ray.mkv` using metadata pulled from [TMDB](https://www.themoviedb.org/).
|
This is made mostly due to [mnamer](https://github.com/jkwill87/mnamer) not having support for director's name, and partly because I wanted to try writing something useful in Rust.
|
||||||
|
|
||||||
This is made mostly due to [mnamer](https://github.com/jkwill87/mnamer) not having support for director's name, and also because I wanted to try writing something useful in Rust.
|
The expected syntax is:
|
||||||
|
|
||||||
## Installation
|
`movie_rename <filename(s)> [--dry-run]`
|
||||||
Install from [AUR](https://aur.archlinux.org/packages/movie-rename-bin), my personal [lure-repo](https://github.com/SinTan1729/lure-repo) or download the binary from the releases. You can also get it from [crates.io](https://crates.io/crates/movie-rename).
|
- There needs to be a config file names movie_rename.conf in your $XDG_CONFIG_HOME.
|
||||||
|
- It should consist of two lines. The first line should have your TMDb API key.
|
||||||
You can also install from source by using
|
|
||||||
```
|
|
||||||
git clone https://github.com/SinTan1729/movie-rename
|
|
||||||
cd movie-rename
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
- The syntax is:
|
|
||||||
|
|
||||||
`movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]`
|
|
||||||
- There needs to be a config file named `config` in the `$XDG_CONFIG_HOME/movie-rename/` directory.
|
|
||||||
- It should consist of two lines. The first line should have your [TMDB API key](https://developers.themoviedb.org/3/getting-started/authentication).
|
|
||||||
- The second line should have a pattern, that will be used for the rename.
|
- The second line should have a pattern, that will be used for the rename.
|
||||||
- In the pattern, the variables need to be enclosed in `{}`, the supported variables are `title`, `year` and `director`.
|
- In the pattern, the variables need to be enclosed in {{}}, the supported variables are `title`, `year` and `director`.
|
||||||
- Default pattern is `{title} ({year}) - {director}`. Extension is always kept.
|
- Default pattern is `{title} ({year}) - {director}`. Extension is always kept.
|
||||||
- Passing `--directory` or `-d` assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles.
|
|
||||||
- Passing `--dry-run` or `-n` does a dry tun and only prints out the new names, without actually doing anything.
|
|
||||||
- Passing `--i-feel-lucky` or `-l` automatically chooses the first option. Useful when you use the program as part of a script.
|
|
||||||
- You can join the short flags `-d`, `-n` and `-l` together (e.g. `-dn` or `-dln`).
|
|
||||||
- Passing `--help` or `-h` shows help and exits.
|
|
||||||
- Passing `--version` or `-v` shows version and exits.
|
|
||||||
|
|
||||||
## Notes
|
I plan to add more variables in the future. Support for TV Shows will not be added, since [tvnamer](https://github.com/dbr/tvnamer) does that excellently.
|
||||||
- Currently, it only supports names in English. It should be easy to turn it into a configurable option. Since for movies in all the languages I know, English name is usually provided, it's a non-feature for me. If someone is willing to test it out for other languages, they're welcome. I'm open to accepting PRs.
|
|
||||||
- I plan to add more variables in the future. Support for TV Shows will not be added, since [tvnamer](https://github.com/dbr/tvnamer) does that excellently.
|
|
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(())
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
.\" Manpage for movie-rename.
|
|
||||||
.\" Contact sayantan[dot]santra689[at]gmail[dot]com to correct errors or typos.
|
|
||||||
.TH man 1 "February 2023" "movie-rename"
|
|
||||||
.SH NAME
|
|
||||||
movie-rename
|
|
||||||
.SH SYNOPSIS
|
|
||||||
movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]
|
|
||||||
.SH DESCRIPTION
|
|
||||||
movie-rename is a simple tool to rename movies, written in Rust.
|
|
||||||
.SH ARGUMENTS
|
|
||||||
A list of filenames (or directory names, not both). -d or --directory must be passed to work with directories.
|
|
||||||
.SH OPTIONS
|
|
||||||
.TP
|
|
||||||
-n, --dry-run
|
|
||||||
Performs a dry run, without actually renaming anything.
|
|
||||||
.TP
|
|
||||||
-d, --directory
|
|
||||||
Runs in directory mode. In this mode, it is assumed that the arguments are directory names, which contain exactly one movie and optionally subtitles.
|
|
||||||
.TP
|
|
||||||
-h, --help
|
|
||||||
Print help information.
|
|
||||||
.TP
|
|
||||||
-v, --version
|
|
||||||
Print version information.
|
|
||||||
.SH CONFIG
|
|
||||||
There needs to be a config file named config in the $XDG_CONFIG_HOME/movie-rename/ directory.
|
|
||||||
It should consist of two lines.
|
|
||||||
.sp
|
|
||||||
The first line should have your TMDb API key.
|
|
||||||
.sp
|
|
||||||
The second line should have a pattern, that will be used for the rename.
|
|
||||||
.sp
|
|
||||||
In the pattern, the variables need to be enclosed in {}, the supported variables are `title`, `year` and `director`.
|
|
||||||
.sp
|
|
||||||
Default pattern is `{title} ({year}) - {director}`. Extension is always kept.
|
|
||||||
.SH AUTHOR
|
|
||||||
Sayantan Santra sayantan[dot]santra689[at]gmail[dot]com
|
|
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)
|
|
||||||
}
|
|
218
src/functions.rs
218
src/functions.rs
|
@ -1,218 +0,0 @@
|
||||||
use inquire::{
|
|
||||||
ui::{Color, IndexPrefix, RenderConfig, Styled},
|
|
||||||
InquireError, Select,
|
|
||||||
};
|
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
|
||||||
use tmdb_api::{
|
|
||||||
client::{reqwest::ReqwestExecutor, Client},
|
|
||||||
movie::{credits::MovieCredits, search::MovieSearch},
|
|
||||||
prelude::Command,
|
|
||||||
};
|
|
||||||
use torrent_name_parser::Metadata;
|
|
||||||
|
|
||||||
use crate::structs::{get_long_lang, Language, MovieEntry};
|
|
||||||
|
|
||||||
// Function to process movie entries
|
|
||||||
pub async fn process_file(
|
|
||||||
filename: &String,
|
|
||||||
tmdb: &Client<ReqwestExecutor>,
|
|
||||||
pattern: &str,
|
|
||||||
dry_run: bool,
|
|
||||||
lucky: bool,
|
|
||||||
movie_list: Option<&HashMap<String, Option<String>>>,
|
|
||||||
// The last bool tells whether the entry should be added to the movie_list or not
|
|
||||||
// The first String is filename without extension, and the second String is
|
|
||||||
// new basename, if any.
|
|
||||||
) -> (String, Option<String>, bool) {
|
|
||||||
// Set RenderConfig for the menu items
|
|
||||||
inquire::set_global_render_config(get_render_config());
|
|
||||||
|
|
||||||
// Get the basename
|
|
||||||
let mut file_base = String::from(filename);
|
|
||||||
let mut parent = String::new();
|
|
||||||
if let Some(parts) = filename.rsplit_once('/') {
|
|
||||||
{
|
|
||||||
parent = String::from(parts.0);
|
|
||||||
file_base = String::from(parts.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the filename into parts for a couple of checks and some later use
|
|
||||||
let filename_parts: Vec<&str> = filename.rsplit('.').collect();
|
|
||||||
let filename_without_ext = if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
|
|
||||||
filename.rsplitn(3, '.').last().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
filename.rsplit_once('.').unwrap().0.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the filename (without extension) has already been processed
|
|
||||||
// If yes, we'll use the older results
|
|
||||||
let mut preprocessed = false;
|
|
||||||
let mut new_name_base = match movie_list {
|
|
||||||
None => String::new(),
|
|
||||||
Some(list) => {
|
|
||||||
if list.contains_key(&filename_without_ext) {
|
|
||||||
preprocessed = true;
|
|
||||||
list[&filename_without_ext].clone().unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if it should be ignored
|
|
||||||
if preprocessed && new_name_base.is_empty() {
|
|
||||||
eprintln!(" Ignoring {file_base} as per previous choice for related files...");
|
|
||||||
return (filename_without_ext, None, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the filename for metadata
|
|
||||||
let metadata = Metadata::from(file_base.as_str()).expect(" Could not parse filename!");
|
|
||||||
|
|
||||||
// Process only if it's a valid file format
|
|
||||||
let mut extension = metadata.extension().unwrap_or("").to_string();
|
|
||||||
if ["mp4", "avi", "mkv", "flv", "m4a", "srt", "ssa"].contains(&extension.as_str()) {
|
|
||||||
println!(" Processing {file_base}...");
|
|
||||||
} else {
|
|
||||||
println!(" Ignoring {file_base}...");
|
|
||||||
return (filename_without_ext, None, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only do the TMDb API stuff if it's not preprocessed
|
|
||||||
if !preprocessed {
|
|
||||||
// Search using the TMDb API
|
|
||||||
let year = metadata.year().map(|y| y as u16);
|
|
||||||
let search = MovieSearch::new(metadata.title().to_string()).with_year(year);
|
|
||||||
let reply = search.execute(tmdb).await;
|
|
||||||
|
|
||||||
let results = match reply {
|
|
||||||
Ok(res) => Ok(res.results),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(" There was an error while searching {file_base}!");
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
|
||||||
// Create movie entry from the result
|
|
||||||
if results.is_ok() {
|
|
||||||
for result in results.unwrap() {
|
|
||||||
let mut movie_details = MovieEntry::from(result);
|
|
||||||
// Get director's name, if needed
|
|
||||||
if pattern.contains("{director}") {
|
|
||||||
let credits_search = MovieCredits::new(movie_details.id);
|
|
||||||
let credits_reply = credits_search.execute(tmdb).await;
|
|
||||||
if credits_reply.is_ok() {
|
|
||||||
let mut crew = credits_reply.unwrap().crew;
|
|
||||||
// Only keep the director(s)
|
|
||||||
crew.retain(|x| x.job == *"Director");
|
|
||||||
if !crew.is_empty() {
|
|
||||||
let directors: Vec<String> =
|
|
||||||
crew.iter().map(|x| x.person.name.clone()).collect();
|
|
||||||
let mut directors_text = directors.join(", ");
|
|
||||||
if let Some(pos) = directors_text.rfind(',') {
|
|
||||||
directors_text.replace_range(pos..pos + 2, " and ");
|
|
||||||
}
|
|
||||||
movie_details.director = Some(directors_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
movie_list.push(movie_details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing is found, skip
|
|
||||||
if movie_list.is_empty() {
|
|
||||||
eprintln!(" Could not find any entries matching {file_base}!");
|
|
||||||
return (filename_without_ext, None, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let choice;
|
|
||||||
if lucky {
|
|
||||||
// Take first choice if in lucky mode
|
|
||||||
choice = movie_list.into_iter().next().unwrap();
|
|
||||||
} else {
|
|
||||||
// Choose from the possible entries
|
|
||||||
choice = match Select::new(
|
|
||||||
format!(" Possible choices for {file_base}:").as_str(),
|
|
||||||
movie_list,
|
|
||||||
)
|
|
||||||
.prompt()
|
|
||||||
{
|
|
||||||
Ok(movie) => movie,
|
|
||||||
Err(error) => {
|
|
||||||
println!(" {error}");
|
|
||||||
let flag = matches!(
|
|
||||||
error,
|
|
||||||
InquireError::OperationCanceled | InquireError::OperationInterrupted
|
|
||||||
);
|
|
||||||
return (filename_without_ext, None, flag);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the new name
|
|
||||||
new_name_base = choice.rename_format(String::from(pattern));
|
|
||||||
} else {
|
|
||||||
println!(" Using previous choice for related files...");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the case for subtitle files
|
|
||||||
if ["srt", "ssa"].contains(&extension.as_str()) {
|
|
||||||
// Try to detect if there's already language info in the filename, else ask user to choose
|
|
||||||
if filename_parts.len() >= 3 && filename_parts[1].len() == 2 {
|
|
||||||
println!(
|
|
||||||
" Keeping language {} as detected in the subtitle file's extension...",
|
|
||||||
get_long_lang(filename_parts[1])
|
|
||||||
);
|
|
||||||
extension = format!("{}.{}", filename_parts[1], extension);
|
|
||||||
} else {
|
|
||||||
let lang_list = Language::generate_list();
|
|
||||||
let lang_choice =
|
|
||||||
Select::new(" Choose the language for the subtitle file:", lang_list)
|
|
||||||
.prompt()
|
|
||||||
.expect(" Invalid choice!");
|
|
||||||
if lang_choice.short != *"none" {
|
|
||||||
extension = format!("{}.{}", lang_choice.short, extension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add extension and stuff to the new name
|
|
||||||
let mut new_name_with_ext = new_name_base.clone();
|
|
||||||
if !extension.is_empty() {
|
|
||||||
new_name_with_ext = format!("{new_name_with_ext}.{extension}");
|
|
||||||
}
|
|
||||||
let mut new_name = new_name_with_ext.clone();
|
|
||||||
if !parent.is_empty() {
|
|
||||||
new_name = format!("{parent}/{new_name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the renaming
|
|
||||||
if *filename == new_name {
|
|
||||||
println!(" [file] '{file_base}' already has correct name.");
|
|
||||||
} else {
|
|
||||||
println!(" [file] '{file_base}' -> '{new_name_with_ext}'");
|
|
||||||
// Only do the rename of --dry-run isn't passed
|
|
||||||
if !dry_run {
|
|
||||||
if !Path::new(new_name.as_str()).is_file() {
|
|
||||||
fs::rename(filename, new_name.as_str()).expect(" Unable to rename file!");
|
|
||||||
} else {
|
|
||||||
eprintln!(" Destination file already exists, skipping...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(filename_without_ext, Some(new_name_base), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderConfig for the menu items
|
|
||||||
fn get_render_config() -> RenderConfig<'static> {
|
|
||||||
let mut render_config = RenderConfig::default();
|
|
||||||
render_config.option_index_prefix = IndexPrefix::Simple;
|
|
||||||
|
|
||||||
render_config.error_message = render_config
|
|
||||||
.error_message
|
|
||||||
.with_prefix(Styled::new("❌").with_fg(Color::LightRed));
|
|
||||||
|
|
||||||
render_config
|
|
||||||
}
|
|
274
src/main.rs
274
src/main.rs
|
@ -1,139 +1,195 @@
|
||||||
use load_file::{self, load_str};
|
use load_file::{self, load_str};
|
||||||
use std::{collections::HashMap, env, fs, path::Path, process::exit};
|
use std::{env, fmt, fs, process::exit};
|
||||||
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
|
use tmdb::{model::*, themoviedb::*};
|
||||||
|
use torrent_name_parser::Metadata;
|
||||||
|
use youchoose;
|
||||||
|
|
||||||
// Import all the modules
|
// Struct for movie entries
|
||||||
mod functions;
|
struct MovieEntry {
|
||||||
use functions::process_file;
|
title: String,
|
||||||
mod args;
|
id: u64,
|
||||||
mod structs;
|
director: String,
|
||||||
|
year: String,
|
||||||
|
language: String,
|
||||||
|
overview: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
impl MovieEntry {
|
||||||
async fn main() {
|
// Create movie entry from results
|
||||||
// Process the passed arguments
|
fn from(movie: SearchMovie) -> MovieEntry {
|
||||||
let (entries, settings) = args::process_args();
|
MovieEntry {
|
||||||
let flag_dry_run = settings["dry-run"];
|
title: movie.title,
|
||||||
let flag_directory = settings["directory"];
|
id: movie.id,
|
||||||
let flag_lucky = settings["i-feel-lucky"];
|
director: String::from("N/A"),
|
||||||
|
year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")),
|
||||||
// Print some message when flags are set.
|
language: movie.original_language,
|
||||||
if flag_dry_run {
|
overview: movie.overview.unwrap_or(String::from("N/A")),
|
||||||
println!("Doing a dry run. No files will be modified.")
|
|
||||||
}
|
}
|
||||||
if flag_directory {
|
|
||||||
println!("Running in directory mode...")
|
|
||||||
}
|
}
|
||||||
if flag_lucky {
|
|
||||||
println!("Automatically selecting the first entry...")
|
// Generate desired filename from movie entry
|
||||||
|
fn rename_format(&self, mut format: String) -> String {
|
||||||
|
format = format.replace("{title}", self.title.as_str());
|
||||||
|
if self.year.as_str() != "N/A" {
|
||||||
|
format = format.replace("{year}", self.year.as_str());
|
||||||
|
} else {
|
||||||
|
format = format.replace("{year}", "");
|
||||||
|
}
|
||||||
|
if self.director.as_str() != "N/A" {
|
||||||
|
format = format.replace("{director}", self.director.as_str());
|
||||||
|
} else {
|
||||||
|
format = format.replace("{director}", "");
|
||||||
|
}
|
||||||
|
format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement display trait for movie entries
|
||||||
|
impl fmt::Display for MovieEntry {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{} ({})", self.title, self.year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Read arguments from commandline
|
||||||
|
let mut args = env::args();
|
||||||
|
args.next();
|
||||||
|
let filenames: Vec<String> = args.collect();
|
||||||
|
|
||||||
|
// If --help is passed, show help and exit
|
||||||
|
if filenames.contains(&"--help".to_string()) {
|
||||||
|
println!(" The expected syntax is:");
|
||||||
|
println!(" movie_rename <filename(s)> [--dry-run]");
|
||||||
|
println!(
|
||||||
|
" There needs to be a config file names movie_rename.conf in your $XDG_CONFIG_HOME."
|
||||||
|
);
|
||||||
|
println!(" It should consist of two lines. The first line should have your TMDb API key.");
|
||||||
|
println!(" The second line should have a pattern, that will be used for the rename.");
|
||||||
|
println!(" In the pattern, the variables need to be enclosed in {{}}, the supported variables are `title`, `year` and `director`.");
|
||||||
|
println!(
|
||||||
|
" Default pattern is `{{title}} ({{year}}) - {{director}}`. Extension is always kept."
|
||||||
|
);
|
||||||
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read config file, or display error
|
// Try to read config file, or display error
|
||||||
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME"));
|
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or("$HOME".to_string());
|
||||||
if config_file == "$HOME" {
|
if config_file == String::from("$HOME") {
|
||||||
config_file = env::var("$HOME").unwrap();
|
config_file = env::var("$HOME").unwrap();
|
||||||
config_file.push_str("/.config");
|
|
||||||
}
|
}
|
||||||
config_file.push_str("/movie-rename/config");
|
config_file.push_str("/movie_rename.conf");
|
||||||
|
|
||||||
if !Path::new(config_file.as_str()).is_file() {
|
|
||||||
eprintln!("Error reading the config file. Pass --help to see help.");
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut config = load_str!(config_file.as_str()).lines();
|
let mut config = load_str!(config_file.as_str()).lines();
|
||||||
let api_key = config.next().unwrap_or("");
|
let api_key = config.next().unwrap_or("");
|
||||||
let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
|
let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
|
||||||
|
|
||||||
if api_key.is_empty() {
|
if api_key == "" {
|
||||||
eprintln!("Could not read the API key. Pass --help to see help.");
|
eprintln!("Error reading the config file. Pass --help to see help.");
|
||||||
exit(2);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create TMDb object for API calls
|
// Create TMDb object for API calls
|
||||||
let tmdb = Client::<ReqwestExecutor>::new(String::from(api_key));
|
let tmdb = TMDb {
|
||||||
|
api_key: api_key,
|
||||||
|
language: "en",
|
||||||
|
};
|
||||||
|
|
||||||
// Iterate over entries
|
// Check if --dry-run is passed
|
||||||
for entry in entries {
|
let mut dry_run = false;
|
||||||
// Check if the file/directory exists on disk and run necessary commands
|
if filenames.contains(&"--dry-run".to_string()) {
|
||||||
match flag_directory {
|
println!("Doing a dry run.");
|
||||||
// Normal file
|
dry_run = true;
|
||||||
false => {
|
}
|
||||||
if Path::new(entry.as_str()).is_file() {
|
|
||||||
// Process the filename for movie entries
|
// Iterate over filenames
|
||||||
process_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await;
|
for filename in filenames {
|
||||||
} else {
|
// Skip if it's the --dry-run tag
|
||||||
eprintln!("The file {entry} wasn't found on disk, skipping...");
|
if filename == "--dry-run".to_string() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Directory
|
|
||||||
true => {
|
|
||||||
if Path::new(entry.as_str()).is_dir() {
|
|
||||||
println!("Processing files inside the directory {entry}...");
|
|
||||||
let mut movie_list = HashMap::new();
|
|
||||||
|
|
||||||
if let Ok(files_in_dir) = fs::read_dir(entry.as_str()) {
|
// Parse the filename for metadata
|
||||||
for file in files_in_dir {
|
let metadata = Metadata::from(filename.as_str()).expect("Could not parse filename");
|
||||||
if file.is_ok() {
|
|
||||||
let filename = file.unwrap().path().display().to_string();
|
|
||||||
let (filename_without_ext, movie_name_temp, add_to_list) =
|
|
||||||
process_file(
|
|
||||||
&filename,
|
|
||||||
&tmdb,
|
|
||||||
pattern,
|
|
||||||
flag_dry_run,
|
|
||||||
flag_lucky,
|
|
||||||
Some(&movie_list),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if add_to_list {
|
// Search using the TMDb API
|
||||||
movie_list.insert(filename_without_ext, movie_name_temp);
|
let results = tmdb
|
||||||
}
|
.search()
|
||||||
}
|
.title(metadata.title())
|
||||||
}
|
.year(metadata.year().unwrap() as u64)
|
||||||
} else {
|
.execute()
|
||||||
eprintln!("There was an error accessing the directory {entry}!");
|
.unwrap()
|
||||||
continue;
|
.results;
|
||||||
}
|
|
||||||
if movie_list.len() == 1 {
|
|
||||||
let entry_clean = entry.trim_end_matches('/');
|
|
||||||
let movie_name = movie_list.into_values().next().unwrap();
|
|
||||||
|
|
||||||
// If the file was ignored, exit
|
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
||||||
match movie_name {
|
|
||||||
None => {
|
// Create movie entry from the result
|
||||||
eprintln!("Not renaming directory as only movie was skipped.");
|
for result in results {
|
||||||
|
let mut movie_details = MovieEntry::from(result);
|
||||||
|
// Get director's name, if needed
|
||||||
|
if pattern.contains("{director}") {
|
||||||
|
let with_credits: Result<Movie, _> =
|
||||||
|
tmdb.fetch().id(movie_details.id).append_credits().execute();
|
||||||
|
if let Ok(movie) = with_credits {
|
||||||
|
match movie.credits {
|
||||||
|
Some(cre) => {
|
||||||
|
let mut directors = cre.crew;
|
||||||
|
directors.retain(|x| x.job == "Director");
|
||||||
|
for person in directors {
|
||||||
|
movie_details.director = person.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
movie_list.push(movie_details);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(name) => {
|
// Choose from the possible entries
|
||||||
if entry_clean == name {
|
let mut menu = youchoose::Menu::new(movie_list.iter())
|
||||||
println!(
|
.preview(display)
|
||||||
"[directory] '{entry_clean}' already has correct name."
|
.preview_label(filename.to_string());
|
||||||
);
|
let choice = menu.show()[0];
|
||||||
|
|
||||||
|
// Handle the case for subtitle files
|
||||||
|
let mut extension = metadata.extension().unwrap_or("").to_string();
|
||||||
|
if ["srt", "ssa"].contains(&extension.as_str()) {
|
||||||
|
let languages = Vec::from(["en", "hi", "bn", "de", "fr", "sp", "ja", "n/a"]);
|
||||||
|
let mut lang_menu = youchoose::Menu::new(languages.iter());
|
||||||
|
let lang_choice = lang_menu.show()[0];
|
||||||
|
if languages[lang_choice] != "none" {
|
||||||
|
extension = format!("{}.{}", languages[lang_choice], extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new name
|
||||||
|
let mut new_name_vec = vec![
|
||||||
|
movie_list[choice].rename_format(pattern.to_string()),
|
||||||
|
extension,
|
||||||
|
];
|
||||||
|
new_name_vec.retain(|x| !x.is_empty());
|
||||||
|
let new_name = new_name_vec.join(".");
|
||||||
|
if filename == new_name {
|
||||||
|
println!("{} already has correct name.", filename);
|
||||||
} else {
|
} else {
|
||||||
println!("[directory] '{entry_clean}' -> '{name}'",);
|
println!("{} -> {}", filename, new_name);
|
||||||
if !flag_dry_run {
|
// Only do the rename of --dry-run isn't passed
|
||||||
if !Path::new(name.as_str()).is_dir() {
|
if dry_run == false {
|
||||||
fs::rename(entry, name)
|
println!("Renaming...");
|
||||||
.expect("Unable to rename directory!");
|
fs::rename(filename, new_name).expect("Unable to rename file.");
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"Destination directory already exists, skipping..."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("Could not determine how to rename the directory {entry}!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("The directory {entry} wasn't found on disk, skipping...");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display function for preview in menu
|
||||||
|
fn display(movie: &MovieEntry) -> String {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
buffer.push_str(&format!("Title: {}\n", movie.title));
|
||||||
|
buffer.push_str(&format!("Release year: {}\n", movie.year));
|
||||||
|
buffer.push_str(&format!("Language: {}\n", movie.language));
|
||||||
|
buffer.push_str(&format!("Director: {}\n", movie.title));
|
||||||
|
buffer.push_str(&format!("TMDb ID: {}\n", movie.id));
|
||||||
|
buffer.push_str(&format!("Overview: {}\n", movie.overview));
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
171
src/structs.rs
171
src/structs.rs
|
@ -1,171 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use tmdb_api::movie::MovieShort;
|
|
||||||
|
|
||||||
// Struct for movie entries
|
|
||||||
pub struct MovieEntry {
|
|
||||||
pub title: String,
|
|
||||||
pub id: u64,
|
|
||||||
pub director: Option<String>,
|
|
||||||
pub year: Option<String>,
|
|
||||||
pub language: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MovieEntry {
|
|
||||||
// Create movie entry from results
|
|
||||||
pub fn from(movie: MovieShort) -> MovieEntry {
|
|
||||||
MovieEntry {
|
|
||||||
title: movie.inner.title,
|
|
||||||
id: movie.inner.id,
|
|
||||||
director: None,
|
|
||||||
year: movie
|
|
||||||
.inner
|
|
||||||
.release_date
|
|
||||||
.map(|date| date.format("%Y").to_string()),
|
|
||||||
language: get_long_lang(movie.inner.original_language.as_str()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate desired filename from movie entry
|
|
||||||
pub fn rename_format(&self, mut format: String) -> String {
|
|
||||||
// Try to sanitize the title to avoid some characters
|
|
||||||
let mut title = self.title.clone();
|
|
||||||
title = sanitize(title);
|
|
||||||
title.truncate(159);
|
|
||||||
format = format.replace("{title}", title.as_str());
|
|
||||||
|
|
||||||
format = match &self.year {
|
|
||||||
Some(year) => format.replace("{year}", year.as_str()),
|
|
||||||
None => format.replace("{year}", ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
format = match &self.director {
|
|
||||||
Some(name) => {
|
|
||||||
// Try to sanitize the director's name to avoid some characters
|
|
||||||
let mut director = name.clone();
|
|
||||||
director = sanitize(director);
|
|
||||||
director.truncate(63);
|
|
||||||
format.replace("{director}", director.as_str())
|
|
||||||
}
|
|
||||||
None => format.replace("{director}", ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to clean extra spaces and such
|
|
||||||
format = format.trim_matches(|c| "- ".contains(c)).to_string();
|
|
||||||
while format.contains("- -") {
|
|
||||||
format = format.replace("- -", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement display trait for movie entries
|
|
||||||
impl fmt::Display for MovieEntry {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let mut buffer = String::new();
|
|
||||||
buffer.push_str(&format!("{} ", self.title));
|
|
||||||
|
|
||||||
if let Some(year) = &self.year {
|
|
||||||
buffer.push_str(&format!("({year}), "));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push_str(&format!(
|
|
||||||
"Language: {}, ",
|
|
||||||
get_long_lang(self.language.as_str())
|
|
||||||
));
|
|
||||||
|
|
||||||
if let Some(director) = &self.director {
|
|
||||||
buffer.push_str(&format!("Directed by: {director}, "));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push_str(&format!("TMDB ID: {}", self.id));
|
|
||||||
// buffer.push_str(&format!("Synopsis: {}", self.overview));
|
|
||||||
write!(f, "{buffer}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Language {
|
|
||||||
pub short: String,
|
|
||||||
pub long: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Language {
|
|
||||||
// Generate a vector of Language entries of all supported languages
|
|
||||||
pub fn generate_list() -> Vec<Language> {
|
|
||||||
let mut list = Vec::new();
|
|
||||||
for lang in ["en", "hi", "bn", "fr", "ja", "de", "sp", "none"] {
|
|
||||||
list.push(Language {
|
|
||||||
short: String::from(lang),
|
|
||||||
long: get_long_lang(lang),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement display trait for Language
|
|
||||||
impl fmt::Display for Language {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.long)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get long name of a language
|
|
||||||
pub fn get_long_lang(short: &str) -> String {
|
|
||||||
// List used from https://gist.github.com/carlopires/1262033/c52ef0f7ce4f58108619508308372edd8d0bd518#file-gistfile1-txt
|
|
||||||
#[rustfmt::skip]
|
|
||||||
static LANG_LIST: [(&str, &str); 185] = [("ab", "Abkhaz"), ("aa", "Afar"), ("af", "Afrikaans"), ("ak", "Akan"), ("sq", "Albanian"),
|
|
||||||
("am", "Amharic"), ("ar", "Arabic"), ("an", "Aragonese"), ("hy", "Armenian"), ("as", "Assamese"), ("av", "Avaric"),
|
|
||||||
("ae", "Avestan"), ("ay", "Aymara"), ("az", "Azerbaijani"), ("bm", "Bambara"), ("ba", "Bashkir"), ("eu", "Basque"),
|
|
||||||
("be", "Belarusian"), ("bn", "Bengali"), ("bh", "Bihari"), ("bi", "Bislama"), ("bs", "Bosnian"), ("br", "Breton"),
|
|
||||||
("bg", "Bulgarian"), ("my", "Burmese"), ("ca", "Catalan; Valencian"), ("ch", "Chamorro"), ("ce", "Chechen"),
|
|
||||||
("ny", "Chichewa; Chewa; Nyanja"), ("zh", "Chinese"), ("cv", "Chuvash"), ("kw", "Cornish"), ("co", "Corsican"),
|
|
||||||
("cr", "Cree"), ("hr", "Croatian"), ("cs", "Czech"), ("da", "Danish"), ("dv", "Divehi; Maldivian;"), ("nl", "Dutch"),
|
|
||||||
("dz", "Dzongkha"), ("en", "English"), ("eo", "Esperanto"), ("et", "Estonian"), ("ee", "Ewe"), ("fo", "Faroese"),
|
|
||||||
("fj", "Fijian"), ("fi", "Finnish"), ("fr", "French"), ("ff", "Fula"), ("gl", "Galician"), ("ka", "Georgian"),
|
|
||||||
("de", "German"), ("el", "Greek, Modern"), ("gn", "Guaraní"), ("gu", "Gujarati"), ("ht", "Haitian"), ("ha", "Hausa"),
|
|
||||||
("he", "Hebrew (modern)"), ("hz", "Herero"), ("hi", "Hindi"), ("ho", "Hiri Motu"), ("hu", "Hungarian"), ("ia", "Interlingua"),
|
|
||||||
("id", "Indonesian"), ("ie", "Interlingue"), ("ga", "Irish"), ("ig", "Igbo"), ("ik", "Inupiaq"), ("io", "Ido"), ("is", "Icelandic"),
|
|
||||||
("it", "Italian"), ("iu", "Inuktitut"), ("ja", "Japanese"), ("jv", "Javanese"), ("kl", "Kalaallisut"), ("kn", "Kannada"),
|
|
||||||
("kr", "Kanuri"), ("ks", "Kashmiri"), ("kk", "Kazakh"), ("km", "Khmer"), ("ki", "Kikuyu, Gikuyu"), ("rw", "Kinyarwanda"),
|
|
||||||
("ky", "Kirghiz, Kyrgyz"), ("kv", "Komi"), ("kg", "Kongo"), ("ko", "Korean"), ("ku", "Kurdish"), ("kj", "Kwanyama, Kuanyama"),
|
|
||||||
("la", "Latin"), ("lb", "Luxembourgish"), ("lg", "Luganda"), ("li", "Limburgish"), ("ln", "Lingala"), ("lo", "Lao"), ("lt", "Lithuanian"),
|
|
||||||
("lu", "Luba-Katanga"), ("lv", "Latvian"), ("gv", "Manx"), ("mk", "Macedonian"), ("mg", "Malagasy"), ("ms", "Malay"), ("ml", "Malayalam"),
|
|
||||||
("mt", "Maltese"), ("mi", "Māori"), ("mr", "Marathi (Marāṭhī)"), ("mh", "Marshallese"), ("mn", "Mongolian"), ("na", "Nauru"),
|
|
||||||
("nv", "Navajo, Navaho"), ("nb", "Norwegian Bokmål"), ("nd", "North Ndebele"), ("ne", "Nepali"), ("ng", "Ndonga"),
|
|
||||||
("nn", "Norwegian Nynorsk"), ("no", "Norwegian"), ("ii", "Nuosu"), ("nr", "South Ndebele"), ("oc", "Occitan"), ("oj", "Ojibwe, Ojibwa"),
|
|
||||||
("cu", "Old Church Slavonic"), ("om", "Oromo"), ("or", "Oriya"), ("os", "Ossetian, Ossetic"), ("pa", "Panjabi, Punjabi"), ("pi", "Pāli"),
|
|
||||||
("fa", "Persian"), ("pl", "Polish"), ("ps", "Pashto, Pushto"), ("pt", "Portuguese"), ("qu", "Quechua"), ("rm", "Romansh"), ("rn", "Kirundi"),
|
|
||||||
("ro", "Romanian, Moldavan"), ("ru", "Russian"), ("sa", "Sanskrit (Saṁskṛta)"), ("sc", "Sardinian"), ("sd", "Sindhi"), ("se", "Northern Sami"),
|
|
||||||
("sm", "Samoan"), ("sg", "Sango"), ("sr", "Serbian"), ("gd", "Scottish Gaelic"), ("sn", "Shona"), ("si", "Sinhala, Sinhalese"), ("sk", "Slovak"),
|
|
||||||
("sl", "Slovene"), ("so", "Somali"), ("st", "Southern Sotho"), ("es", "Spanish; Castilian"), ("su", "Sundanese"), ("sw", "Swahili"),
|
|
||||||
("ss", "Swati"), ("sv", "Swedish"), ("ta", "Tamil"), ("te", "Telugu"), ("tg", "Tajik"), ("th", "Thai"), ("ti", "Tigrinya"), ("bo", "Tibetan"),
|
|
||||||
("tk", "Turkmen"), ("tl", "Tagalog"), ("tn", "Tswana"), ("to", "Tonga"), ("tr", "Turkish"), ("ts", "Tsonga"), ("tt", "Tatar"), ("tw", "Twi"),
|
|
||||||
("ty", "Tahitian"), ("ug", "Uighur, Uyghur"), ("uk", "Ukrainian"), ("ur", "Urdu"), ("uz", "Uzbek"), ("ve", "Venda"), ("vi", "Vietnamese"),
|
|
||||||
("vo", "Volapük"), ("wa", "Walloon"), ("cy", "Welsh"), ("wo", "Wolof"), ("fy", "Western Frisian"), ("xh", "Xhosa"), ("yi", "Yiddish"),
|
|
||||||
("yo", "Yoruba"), ("za", "Zhuang, Chuang"), ("zu", "Zulu"), ("none", "None")];
|
|
||||||
|
|
||||||
let long = LANG_LIST
|
|
||||||
.iter()
|
|
||||||
.filter(|x| x.0 == short)
|
|
||||||
.map(|x| x.1)
|
|
||||||
.next();
|
|
||||||
|
|
||||||
if let Some(longlang) = long {
|
|
||||||
String::from(longlang)
|
|
||||||
} else {
|
|
||||||
String::from(short)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize filename so that there are no errors while
|
|
||||||
// creating a file/directory
|
|
||||||
fn sanitize(input: String) -> String {
|
|
||||||
const AVOID: &str = "^~*+=`/\\\"><|";
|
|
||||||
|
|
||||||
let mut out = input;
|
|
||||||
out.retain(|c| !AVOID.contains(c));
|
|
||||||
out = out.replace(':', "∶");
|
|
||||||
out = out.replace('?', "﹖");
|
|
||||||
out
|
|
||||||
}
|
|
Loading…
Reference in a new issue