mirror of
https://github.com/SinTan1729/movie-rename.git
synced 2025-04-19 10:20:00 -05:00
Compare commits
80 commits
Author | SHA1 | Date | |
---|---|---|---|
49de0ce13c | |||
cd0dbcd47b | |||
3757dd0a47 | |||
cf8a9ee764 | |||
80fdc110ae | |||
c5b1f51233 | |||
|
3e7b8fbd5d | ||
|
f979559b50 | ||
443c8e6cc9 | |||
4f3f349292 | |||
51e0c84eb4 | |||
83bf1f7af6 | |||
b4073357f7 | |||
67dcd9e378 | |||
b62bffd340 | |||
|
4a69071380 | ||
|
6027b701f0 | ||
1b43b7a017 | |||
|
bc2c9c0135 | ||
|
324cae4fed | ||
64d1749450 | |||
bc0ccd8107 | |||
16118a8d5a | |||
790ec225ea | |||
|
e74c35472a | ||
|
0a851a47fd | ||
c2152264f9 | |||
|
3695c78282 | ||
|
f827e351c3 | ||
39835837c4 | |||
a2f0536642 | |||
fd98c5d355 | |||
cc59a03051 | |||
89d15e9fb7 | |||
d2fc60b709 | |||
6178c022d1 | |||
5e5ba7ea0a | |||
779064034a | |||
9456009b4d | |||
21b26cf4e1 | |||
d7172c78f6 | |||
591ae8f796 | |||
8b6e83a55a | |||
0e92c693f7 | |||
c7c1988b73 | |||
96fcf425b0 | |||
e9314ccdda | |||
4d04f51251 | |||
02fea4a71c | |||
7cb18202eb | |||
8a84f1b2b6 | |||
5a4d7d0e1d | |||
1a83f88c0b | |||
14e5899f36 | |||
c0760526fa | |||
|
d8a2b2d988 | ||
|
be019f221e | ||
|
64abb5b03b | ||
6d94a5429f | |||
7165812709 | |||
9d8e10f041 | |||
a1c4fb816e | |||
e1af92739b | |||
d1b2fcf8a0 | |||
6049f6beb6 | |||
62f47334bb | |||
4dff4ba511 | |||
4105e61829 | |||
4ee97eb9a5 | |||
|
0745539b88 | ||
15d3a57ba5 | |||
a138984bac | |||
5d294a4687 | |||
cbb6b2ab21 | |||
c2d8f05cff | |||
2d6e063a97 | |||
c52643033f | |||
17300e0362 | |||
26eb446de8 | |||
|
7197c88cfb |
12 changed files with 1840 additions and 1402 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
*.mp4
|
*.mp4
|
||||||
*.srt
|
*.srt
|
||||||
*.key
|
*.key
|
||||||
|
*.tar.gz
|
||||||
|
|
2475
Cargo.lock
generated
2475
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
31
Cargo.toml
31
Cargo.toml
|
@ -1,12 +1,35 @@
|
||||||
[package]
|
[package]
|
||||||
name = "movie-rename"
|
name = "movie-rename"
|
||||||
version = "1.2.0"
|
version = "2.3.2"
|
||||||
|
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.11.0"
|
torrent-name-parser = "0.12.1"
|
||||||
tmdb = "3.0.0"
|
tmdb-api = "0.8.0"
|
||||||
inquire = "0.5.2"
|
inquire = "0.7.5"
|
||||||
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.
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
movie-rename: A simple tool to rename movies, written in Rust.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) 2023 Sayantan Santra
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
movie-rename Copyright (C) 2023 Sayantan Santra
|
||||||
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
Normal file
24
Makefile
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
37
README.md
37
README.md
|
@ -1,23 +1,40 @@
|
||||||
 
|
[](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 rename movies, written in Rust.
|
||||||
|
|
||||||
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.
|
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 also because I wanted to try writing something useful in Rust.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Install from [AUR](https://aur.archlinux.org/packages/movie-rename), my personal [lure-repo](https://github.com/SinTan1729/lure-repo) or download the binary from the releases.
|
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).
|
||||||
|
|
||||||
## Notes
|
You can also install from source by using
|
||||||
|
```
|
||||||
|
git clone https://github.com/SinTan1729/movie-rename
|
||||||
|
cd movie-rename
|
||||||
|
sudo make install
|
||||||
|
```
|
||||||
|
|
||||||
- The expected syntax is:
|
## Usage
|
||||||
|
- The syntax is:
|
||||||
|
|
||||||
`movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]`
|
`movie-rename <filename(s)> [-n|--dry-run] [-d|--directory] [-h|--help] [-v|--version]`
|
||||||
- There needs to be a config file named movie-rename.conf in your $XDG_CONFIG_HOME.
|
- 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.
|
- 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` 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 `--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
|
||||||
|
- 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.
|
- 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
Normal file
21
build.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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,6 +1,6 @@
|
||||||
.\" Manpage for movie-rename.
|
.\" Manpage for movie-rename.
|
||||||
.\" Contact sayantan[dot]santra689[at]gmail[dot]com to correct errors or typos.
|
.\" Contact sayantan[dot]santra689[at]gmail[dot]com to correct errors or typos.
|
||||||
.TH man 1 "08 Dec 2022" "1.1.1" "movie-rename man page"
|
.TH man 1 "February 2023" "movie-rename"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
movie-rename
|
movie-rename
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
@ -23,7 +23,7 @@ Print help information.
|
||||||
-v, --version
|
-v, --version
|
||||||
Print version information.
|
Print version information.
|
||||||
.SH CONFIG
|
.SH CONFIG
|
||||||
There needs to be a config file named movie-rename.conf in your $XDG_CONFIG_HOME.
|
There needs to be a config file named config in the $XDG_CONFIG_HOME/movie-rename/ directory.
|
||||||
It should consist of two lines.
|
It should consist of two lines.
|
||||||
.sp
|
.sp
|
||||||
The first line should have your TMDb API key.
|
The first line should have your TMDb API key.
|
||||||
|
|
46
src/args.rs
Normal file
46
src/args.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
||||||
|
}
|
272
src/functions.rs
272
src/functions.rs
|
@ -1,186 +1,212 @@
|
||||||
use inquire::{
|
use inquire::{
|
||||||
ui::{Color, IndexPrefix, RenderConfig, Styled},
|
ui::{Color, IndexPrefix, RenderConfig, Styled},
|
||||||
Select,
|
InquireError, 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::{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 fn process_file(
|
pub async fn process_file(
|
||||||
filename: &String,
|
filename: &String,
|
||||||
tmdb: &TMDb,
|
tmdb: &Client<ReqwestExecutor>,
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
) -> (String, 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
|
// 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::from("");
|
let mut parent = String::new();
|
||||||
match filename.rsplit_once("/") {
|
if let Some(parts) = filename.rsplit_once('/') {
|
||||||
Some(parts) => {
|
{
|
||||||
parent = parts.0.to_string();
|
parent = String::from(parts.0);
|
||||||
file_base = parts.1.to_string();
|
file_base = String::from(parts.1);
|
||||||
}
|
}
|
||||||
None => {}
|
}
|
||||||
|
|
||||||
|
// 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!");
|
||||||
// Search using the TMDb API
|
|
||||||
let mut search = tmdb.search();
|
|
||||||
search.title(metadata.title());
|
|
||||||
|
|
||||||
// Check if year is present in filename
|
// Process only if it's a valid file format
|
||||||
if let Some(year) = metadata.year() {
|
let mut extension = metadata.extension().unwrap_or("").to_string();
|
||||||
search.year(year as u64);
|
if ["mp4", "avi", "mkv", "flv", "m4a", "srt", "ssa"].contains(&extension.as_str()) {
|
||||||
}
|
println!(" Processing {file_base}...");
|
||||||
|
|
||||||
let mut results = Vec::new();
|
|
||||||
if let Ok(search_results) = search.execute() {
|
|
||||||
results = search_results.results;
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("There was an error while searching {}!", filename);
|
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();
|
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
||||||
// Create movie entry from the result
|
// Create movie entry from the result
|
||||||
for result in results {
|
if results.is_ok() {
|
||||||
|
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 with_credits: Result<Movie, _> =
|
let credits_search = MovieCredits::new(movie_details.id);
|
||||||
tmdb.fetch().id(movie_details.id).append_credits().execute();
|
let credits_reply = credits_search.execute(tmdb).await;
|
||||||
if let Ok(movie) = with_credits {
|
if credits_reply.is_ok() {
|
||||||
if let Some(cre) = movie.credits {
|
let mut crew = credits_reply.unwrap().crew;
|
||||||
let mut directors = cre.crew;
|
// Only keep the director(s)
|
||||||
directors.retain(|x| x.job == "Director");
|
crew.retain(|x| x.job == *"Director");
|
||||||
for person in directors {
|
if !crew.is_empty() {
|
||||||
movie_details.director = person.name;
|
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);
|
movie_list.push(movie_details);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If nothing is found, skip
|
|
||||||
if movie_list.len() == 0 {
|
|
||||||
eprintln!("Could not find any entries matching {}!", filename);
|
|
||||||
return ("".to_string(), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Choose from the possible entries
|
||||||
let choice = Select::new(
|
choice = match 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,
|
||||||
let mut extension = metadata.extension().unwrap_or("").to_string();
|
Err(error) => {
|
||||||
// Handle the case for subtitle files
|
println!(" {error}");
|
||||||
let mut is_subtitle = false;
|
let flag = matches!(
|
||||||
if ["srt", "ssa"].contains(&extension.as_str()) {
|
error,
|
||||||
let lang_list = Language::generate_list();
|
InquireError::OperationCanceled | InquireError::OperationInterrupted
|
||||||
let lang_choice = Select::new("Choose the language for the subtitle file:", lang_list)
|
);
|
||||||
.prompt()
|
return (filename_without_ext, None, flag);
|
||||||
.expect("Invalid choice!");
|
|
||||||
if lang_choice.short != "none".to_string() {
|
|
||||||
extension = format!("{}.{}", lang_choice.short, extension);
|
|
||||||
}
|
|
||||||
is_subtitle = true;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Create the new name
|
// Create the new name
|
||||||
let new_name_base = choice.rename_format(pattern.to_string());
|
new_name_base = choice.rename_format(String::from(pattern));
|
||||||
let mut new_name_with_ext = new_name_base.clone();
|
} else {
|
||||||
if extension != "" {
|
println!(" Using previous choice for related files...");
|
||||||
new_name_with_ext = format!("{}.{}", new_name_with_ext, extension);
|
|
||||||
}
|
}
|
||||||
let mut new_name = String::from(new_name_with_ext.clone());
|
|
||||||
if parent != "".to_string() {
|
// Handle the case for subtitle files
|
||||||
new_name = format!("{}/{}", parent, new_name);
|
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
|
// Process the renaming
|
||||||
if *filename == new_name {
|
if *filename == new_name {
|
||||||
println!("[file] '{}' already has correct name.", file_base);
|
println!(" [file] '{file_base}' already has correct name.");
|
||||||
} 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 == false {
|
if !dry_run {
|
||||||
if Path::new(new_name.as_str()).is_file() == false {
|
if !Path::new(new_name.as_str()).is_file() {
|
||||||
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...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(new_name_base, is_subtitle)
|
(filename_without_ext, Some(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 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."
|
|
||||||
);
|
|
||||||
println!("Passing --directory assumes that the arguments are directory names, which contain exactly one movie and optionally subtitles.");
|
|
||||||
println!(" Pass --help to get this again.");
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
"--version" | "-v" => {
|
|
||||||
println!("movie-rename {}", VERSION);
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
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 {
|
fn get_render_config() -> RenderConfig<'static> {
|
||||||
let mut render_config = RenderConfig::default();
|
let mut render_config = RenderConfig::default();
|
||||||
render_config.option_index_prefix = IndexPrefix::Simple;
|
render_config.option_index_prefix = IndexPrefix::Simple;
|
||||||
|
|
||||||
|
|
130
src/main.rs
130
src/main.rs
|
@ -1,100 +1,136 @@
|
||||||
use load_file::{self, load_str};
|
use load_file::{self, load_str};
|
||||||
use std::{env, fs, path::Path, process::exit};
|
use std::{collections::HashMap, env, fs, path::Path, process::exit};
|
||||||
use tmdb::themoviedb::*;
|
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
|
||||||
|
|
||||||
// Import all the modules
|
// Import all the modules
|
||||||
mod functions;
|
mod functions;
|
||||||
use functions::{process_args, process_file};
|
use functions::process_file;
|
||||||
|
mod args;
|
||||||
mod structs;
|
mod structs;
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
// Read arguments from commandline
|
async fn main() {
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
// Process the passed arguments
|
// Process the passed arguments
|
||||||
let (entries, settings) = process_args(args);
|
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.
|
||||||
|
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...")
|
||||||
|
}
|
||||||
|
|
||||||
// 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("$HOME".to_string());
|
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME"));
|
||||||
if config_file == String::from("$HOME") {
|
if config_file == "$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.conf");
|
config_file.push_str("/movie-rename/config");
|
||||||
let mut config = load_str!(config_file.as_str()).lines();
|
|
||||||
let api_key = config.next().unwrap_or("");
|
|
||||||
let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
|
|
||||||
|
|
||||||
if api_key == "" {
|
if !Path::new(config_file.as_str()).is_file() {
|
||||||
eprintln!("Error reading the config file. Pass --help to see help.");
|
eprintln!("Error reading the config file. Pass --help to see help.");
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut config = load_str!(config_file.as_str()).lines();
|
||||||
|
let api_key = config.next().unwrap_or("");
|
||||||
|
let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
|
||||||
|
|
||||||
|
if api_key.is_empty() {
|
||||||
|
eprintln!("Could not read the API key. Pass --help to see help.");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
// Create TMDb object for API calls
|
// Create TMDb object for API calls
|
||||||
let tmdb = TMDb {
|
let tmdb = Client::<ReqwestExecutor>::new(String::from(api_key));
|
||||||
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
|
// Check if the file/directory exists on disk and run necessary commands
|
||||||
match settings["directory"] {
|
match flag_directory {
|
||||||
// Normal file
|
// Normal file
|
||||||
false => {
|
false => {
|
||||||
if Path::new(entry.as_str()).is_file() == true {
|
if Path::new(entry.as_str()).is_file() {
|
||||||
// Process the filename for movie entries
|
// Process the filename for movie entries
|
||||||
process_file(&entry, &tmdb, pattern, settings["dry_run"]);
|
process_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("The file {} wasn't found on disk, skipping...", entry);
|
eprintln!("The file {entry} wasn't found on disk, skipping...");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Directory
|
// Directory
|
||||||
true => {
|
true => {
|
||||||
if Path::new(entry.as_str()).is_dir() == 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_count = 0;
|
let mut movie_list = HashMap::new();
|
||||||
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 (movie_name_temp, is_subtitle) = process_file(
|
let filename = file.unwrap().path().display().to_string();
|
||||||
&format!("{}", file.unwrap().path().display()),
|
let (filename_without_ext, movie_name_temp, add_to_list) =
|
||||||
|
process_file(
|
||||||
|
&filename,
|
||||||
&tmdb,
|
&tmdb,
|
||||||
pattern,
|
pattern,
|
||||||
settings["dry_run"],
|
flag_dry_run,
|
||||||
);
|
flag_lucky,
|
||||||
if is_subtitle == false {
|
Some(&movie_list),
|
||||||
movie_count += 1;
|
)
|
||||||
movie_name = movie_name_temp;
|
.await;
|
||||||
|
|
||||||
|
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_count == 1 {
|
if movie_list.len() == 1 {
|
||||||
let entry_clean = entry.trim_end_matches("/");
|
let entry_clean = entry.trim_end_matches('/');
|
||||||
if entry_clean == movie_name {
|
let movie_name = movie_list.into_values().next().unwrap();
|
||||||
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, movie_name);
|
println!("[directory] '{entry_clean}' -> '{name}'",);
|
||||||
if settings["dry_run"] == false {
|
if !flag_dry_run {
|
||||||
if Path::new(movie_name.as_str()).is_dir() == false {
|
if !Path::new(name.as_str()).is_dir() {
|
||||||
fs::rename(entry, movie_name)
|
fs::rename(entry, name)
|
||||||
.expect("Unable to rename directory!");
|
.expect("Unable to rename directory!");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Destination directory already exists, skipping...");
|
eprintln!(
|
||||||
|
"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 {} wasn't found on disk, skipping...", entry);
|
eprintln!("The directory {entry} wasn't found on disk, skipping...");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
149
src/structs.rs
149
src/structs.rs
|
@ -1,40 +1,60 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use tmdb::model::*;
|
use tmdb_api::movie::MovieShort;
|
||||||
|
|
||||||
// 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: String,
|
pub director: Option<String>,
|
||||||
pub year: String,
|
pub year: Option<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: SearchMovie) -> MovieEntry {
|
pub fn from(movie: MovieShort) -> MovieEntry {
|
||||||
MovieEntry {
|
MovieEntry {
|
||||||
title: movie.title,
|
title: movie.inner.title,
|
||||||
id: movie.id,
|
id: movie.inner.id,
|
||||||
director: String::from("N/A"),
|
director: None,
|
||||||
year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")),
|
year: movie
|
||||||
language: get_long_lang(movie.original_language.as_str()),
|
.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
|
// Generate desired filename from movie entry
|
||||||
pub fn rename_format(&self, mut format: String) -> String {
|
pub fn rename_format(&self, mut format: String) -> String {
|
||||||
format = format.replace("{title}", self.title.as_str());
|
// Try to sanitize the title to avoid some characters
|
||||||
if self.year.as_str() != "N/A" {
|
let mut title = self.title.clone();
|
||||||
format = format.replace("{year}", self.year.as_str());
|
title = sanitize(title);
|
||||||
} else {
|
title.truncate(159);
|
||||||
format = format.replace("{year}", "");
|
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.director.as_str() != "N/A" {
|
None => format.replace("{director}", ""),
|
||||||
format = format.replace("{director}", self.director.as_str());
|
};
|
||||||
} else {
|
|
||||||
format = 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
|
format
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,12 +64,23 @@ 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));
|
|
||||||
buffer.push_str(&format!("Language: {}, ", self.language));
|
if let Some(year) = &self.year {
|
||||||
buffer.push_str(&format!("Directed by: {}, ", self.director));
|
buffer.push_str(&format!("({year}), "));
|
||||||
buffer.push_str(&format!("TMDb ID: {}", self.id));
|
}
|
||||||
|
|
||||||
|
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));
|
// buffer.push_str(&format!("Synopsis: {}", self.overview));
|
||||||
write!(f, "{}", buffer)
|
write!(f, "{buffer}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +95,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: lang.to_string(),
|
short: String::from(lang),
|
||||||
long: get_long_lang(lang),
|
long: get_long_lang(lang),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -80,17 +111,61 @@ impl fmt::Display for Language {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get long name of a language
|
// Get long name of a language
|
||||||
fn get_long_lang(short: &str) -> String {
|
pub fn get_long_lang(short: &str) -> String {
|
||||||
let long = match short {
|
// List used from https://gist.github.com/carlopires/1262033/c52ef0f7ce4f58108619508308372edd8d0bd518#file-gistfile1-txt
|
||||||
"en" => "English",
|
#[rustfmt::skip]
|
||||||
"hi" => "Hindi",
|
static LANG_LIST: [(&str, &str); 185] = [("ab", "Abkhaz"), ("aa", "Afar"), ("af", "Afrikaans"), ("ak", "Akan"), ("sq", "Albanian"),
|
||||||
"bn" => "Bengali",
|
("am", "Amharic"), ("ar", "Arabic"), ("an", "Aragonese"), ("hy", "Armenian"), ("as", "Assamese"), ("av", "Avaric"),
|
||||||
"fr" => "French",
|
("ae", "Avestan"), ("ay", "Aymara"), ("az", "Azerbaijani"), ("bm", "Bambara"), ("ba", "Bashkir"), ("eu", "Basque"),
|
||||||
"ja" => "Japanese",
|
("be", "Belarusian"), ("bn", "Bengali"), ("bh", "Bihari"), ("bi", "Bislama"), ("bs", "Bosnian"), ("br", "Breton"),
|
||||||
"de" => "German",
|
("bg", "Bulgarian"), ("my", "Burmese"), ("ca", "Catalan; Valencian"), ("ch", "Chamorro"), ("ce", "Chechen"),
|
||||||
"sp" => "Spanish",
|
("ny", "Chichewa; Chewa; Nyanja"), ("zh", "Chinese"), ("cv", "Chuvash"), ("kw", "Cornish"), ("co", "Corsican"),
|
||||||
"none" => "None",
|
("cr", "Cree"), ("hr", "Croatian"), ("cs", "Czech"), ("da", "Danish"), ("dv", "Divehi; Maldivian;"), ("nl", "Dutch"),
|
||||||
other => other,
|
("dz", "Dzongkha"), ("en", "English"), ("eo", "Esperanto"), ("et", "Estonian"), ("ee", "Ewe"), ("fo", "Faroese"),
|
||||||
};
|
("fj", "Fijian"), ("fi", "Finnish"), ("fr", "French"), ("ff", "Fula"), ("gl", "Galician"), ("ka", "Georgian"),
|
||||||
long.to_string()
|
("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