mirror of
https://github.com/SinTan1729/movie-rename.git
synced 2025-04-19 10:20:00 -05:00
Compare commits
No commits in common. "main" and "1.1.2" have entirely different histories.
12 changed files with 1351 additions and 2088 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
|||
/target
|
||||
*.mp4
|
||||
*.srt
|
||||
*.key
|
||||
*.tar.gz
|
||||
*.key
|
2505
Cargo.lock
generated
2505
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
35
Cargo.toml
35
Cargo.toml
|
@ -1,35 +1,12 @@
|
|||
[package]
|
||||
name = "movie-rename"
|
||||
version = "2.3.2"
|
||||
build = "build.rs"
|
||||
name = "movie_rename"
|
||||
version = "0.1.0"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
torrent-name-parser = "0.12.1"
|
||||
tmdb-api = "0.8.0"
|
||||
inquire = "0.7.5"
|
||||
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"
|
||||
torrent-name-parser = "0.11.0"
|
||||
tmdb = "3.0.0"
|
||||
youchoose = "0.1.1"
|
||||
load_file = "1.0.1"
|
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
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
movie-rename: A simple tool to rename movies, written in Rust.
|
||||
Copyright (C) 2023 Sayantan Santra
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
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 is free software, and you are welcome to redistribute it
|
||||
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
|
48
README.md
48
README.md
|
@ -1,40 +1,22 @@
|
|||
[](https://github.com/SinTan1729/movie-rename/releases/latest/)
|
||||

|
||||
[](https://aur.archlinux.org/packages/movie-rename-bin/)
|
||||
# `movie-rename`
|
||||
# movie-rename
|
||||
|
||||
### A simple tool to rename 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 also 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 partly because I wanted to try writing something useful in Rust.
|
||||
|
||||
## Installation
|
||||
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).
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- 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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
- The expected syntax is:
|
||||
|
||||
`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.
|
||||
- It should consist of two lines. The first line should have your TMDb API key.
|
||||
- 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`.
|
||||
- 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.
|
||||
|
||||
- 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,12 +1,12 @@
|
|||
.\" Manpage for movie-rename.
|
||||
.\" Manpage for movie_rename.
|
||||
.\" Contact sayantan[dot]santra689[at]gmail[dot]com to correct errors or typos.
|
||||
.TH man 1 "February 2023" "movie-rename"
|
||||
.TH man 1 "08 Dec 2022" "1.1.1" "movie_rename man page"
|
||||
.SH NAME
|
||||
movie-rename
|
||||
movie_rename
|
||||
.SH SYNOPSIS
|
||||
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]
|
||||
.SH DESCRIPTION
|
||||
movie-rename is a simple tool to rename movies, written in Rust.
|
||||
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
|
||||
|
@ -23,7 +23,7 @@ Print help information.
|
|||
-v, --version
|
||||
Print version information.
|
||||
.SH CONFIG
|
||||
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 movie_rename.conf in your $XDG_CONFIG_HOME.
|
||||
It should consist of two lines.
|
||||
.sp
|
||||
The first line should have your TMDb API key.
|
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
|
||||
}
|
350
src/main.rs
350
src/main.rs
|
@ -1,139 +1,305 @@
|
|||
use load_file::{self, load_str};
|
||||
use std::{collections::HashMap, env, fs, path::Path, process::exit};
|
||||
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
|
||||
use std::{collections::HashMap, env, fmt, fs, path::Path, process::exit};
|
||||
use tmdb::{model::*, themoviedb::*};
|
||||
use torrent_name_parser::Metadata;
|
||||
use youchoose;
|
||||
|
||||
// Import all the modules
|
||||
mod functions;
|
||||
use functions::process_file;
|
||||
mod args;
|
||||
mod structs;
|
||||
const VERSION: &str = "1.1.2";
|
||||
// Struct for movie entries
|
||||
struct MovieEntry {
|
||||
title: String,
|
||||
id: u64,
|
||||
director: String,
|
||||
year: String,
|
||||
language: String,
|
||||
overview: String,
|
||||
}
|
||||
|
||||
impl MovieEntry {
|
||||
// Create movie entry from results
|
||||
fn from(movie: SearchMovie) -> MovieEntry {
|
||||
MovieEntry {
|
||||
title: movie.title,
|
||||
id: movie.id,
|
||||
director: String::from("N/A"),
|
||||
year: String::from(movie.release_date.split('-').next().unwrap_or("N/A")),
|
||||
language: movie.original_language,
|
||||
overview: movie.overview.unwrap_or(String::from("N/A")),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 args: Vec<String> = env::args().collect();
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Process the passed arguments
|
||||
let (entries, settings) = args::process_args();
|
||||
let flag_dry_run = settings["dry-run"];
|
||||
let flag_directory = settings["directory"];
|
||||
let flag_lucky = settings["i-feel-lucky"];
|
||||
|
||||
// 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...")
|
||||
}
|
||||
let (entries, settings) = process_args(args);
|
||||
|
||||
// Try to read config file, or display error
|
||||
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME"));
|
||||
if config_file == "$HOME" {
|
||||
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or("$HOME".to_string());
|
||||
if config_file == String::from("$HOME") {
|
||||
config_file = env::var("$HOME").unwrap();
|
||||
config_file.push_str("/.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);
|
||||
}
|
||||
|
||||
config_file.push_str("/movie_rename.conf");
|
||||
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);
|
||||
if api_key == "" {
|
||||
eprintln!("Error reading the config file. Pass --help to see help.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 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
|
||||
for entry in entries {
|
||||
// Check if the file/directory exists on disk and run necessary commands
|
||||
match flag_directory {
|
||||
// Check if the file/directory exists on disk
|
||||
match settings["directory"] {
|
||||
// Normal file
|
||||
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_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await;
|
||||
process_file(&entry, &tmdb, pattern, settings["dry_run"]);
|
||||
} else {
|
||||
eprintln!("The file {entry} wasn't found on disk, skipping...");
|
||||
eprintln!("The file {} wasn't found on disk, skipping...", entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Directory
|
||||
true => {
|
||||
if Path::new(entry.as_str()).is_dir() {
|
||||
println!("Processing files inside the directory {entry}...");
|
||||
let mut movie_list = HashMap::new();
|
||||
|
||||
if Path::new(entry.as_str()).is_dir() == true {
|
||||
println!("Processing files inside the directory {}...", entry);
|
||||
let mut movie_count = 0;
|
||||
let mut movie_name = String::new();
|
||||
if let Ok(files_in_dir) = fs::read_dir(entry.as_str()) {
|
||||
for file in files_in_dir {
|
||||
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 {
|
||||
movie_list.insert(filename_without_ext, movie_name_temp);
|
||||
let (movie_name_temp, is_subtitle) = process_file(
|
||||
&format!("{}", file.unwrap().path().display()),
|
||||
&tmdb,
|
||||
pattern,
|
||||
settings["dry_run"],
|
||||
);
|
||||
if is_subtitle == false {
|
||||
movie_count += 1;
|
||||
movie_name = movie_name_temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("There was an error accessing the directory {entry}!");
|
||||
eprintln!("There was an error accessing the directory {}!", entry);
|
||||
continue;
|
||||
}
|
||||
if movie_list.len() == 1 {
|
||||
let entry_clean = entry.trim_end_matches('/');
|
||||
let movie_name = movie_list.into_values().next().unwrap();
|
||||
|
||||
// If the file was ignored, exit
|
||||
match movie_name {
|
||||
None => {
|
||||
eprintln!("Not renaming directory as only movie was skipped.");
|
||||
}
|
||||
|
||||
Some(name) => {
|
||||
if entry_clean == name {
|
||||
println!(
|
||||
"[directory] '{entry_clean}' already has correct name."
|
||||
);
|
||||
} else {
|
||||
println!("[directory] '{entry_clean}' -> '{name}'",);
|
||||
if !flag_dry_run {
|
||||
if !Path::new(name.as_str()).is_dir() {
|
||||
fs::rename(entry, name)
|
||||
.expect("Unable to rename directory!");
|
||||
} else {
|
||||
eprintln!(
|
||||
"Destination directory already exists, skipping..."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if movie_count == 1 {
|
||||
if entry == movie_name {
|
||||
println!("[directory] {} already has correct name.", entry);
|
||||
} else {
|
||||
println!("[directory] {} -> {}", entry, movie_name);
|
||||
if settings["dry_run"] == false {
|
||||
fs::rename(entry, movie_name).expect("Unable to rename directory!");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Could not determine how to rename the directory {entry}!");
|
||||
eprintln!("Could not determine how to rename the directory {}!", entry);
|
||||
}
|
||||
} else {
|
||||
eprintln!("The directory {entry} wasn't found on disk, skipping...");
|
||||
eprintln!("The directory {} wasn't found on disk, skipping...", entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process movie entries
|
||||
fn process_file(filename: &String, tmdb: &TMDb, pattern: &str, dry_run: bool) -> (String, bool) {
|
||||
// Get the basename
|
||||
let mut file_base = String::from(filename);
|
||||
let mut parent = String::from("");
|
||||
match filename.rsplit_once("/") {
|
||||
Some(parts) => {
|
||||
parent = parts.0.to_string();
|
||||
file_base = parts.1.to_string();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Parse the filename for metadata
|
||||
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
|
||||
if let Some(year) = metadata.year() {
|
||||
search.year(year as u64);
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
if let Ok(search_results) = search.execute() {
|
||||
results = search_results.results;
|
||||
} else {
|
||||
eprintln!("There was an error while searching {}!", filename);
|
||||
}
|
||||
|
||||
let mut movie_list: Vec<MovieEntry> = Vec::new();
|
||||
// Create movie entry from the result
|
||||
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 {
|
||||
if let Some(cre) = movie.credits {
|
||||
let mut directors = cre.crew;
|
||||
directors.retain(|x| x.job == "Director");
|
||||
for person in directors {
|
||||
movie_details.director = person.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Choose from the possible entries
|
||||
let mut menu = youchoose::Menu::new(movie_list.iter())
|
||||
.preview(display)
|
||||
.preview_label(file_base.to_string());
|
||||
let choice = menu.show()[0];
|
||||
|
||||
let mut extension = metadata.extension().unwrap_or("").to_string();
|
||||
// Handle the case for subtitle files
|
||||
let mut is_subtitle = false;
|
||||
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);
|
||||
}
|
||||
is_subtitle = true;
|
||||
}
|
||||
|
||||
// Create the new name
|
||||
let new_name_base = movie_list[choice].rename_format(pattern.to_string());
|
||||
let mut new_name = String::from(new_name_base.clone());
|
||||
if extension != "" {
|
||||
new_name = format!("{}.{}", new_name, extension);
|
||||
}
|
||||
if parent != "".to_string() {
|
||||
new_name = format!("{}/{}", parent, new_name);
|
||||
}
|
||||
|
||||
// Process the renaming
|
||||
if *filename == new_name {
|
||||
println!("[file] {} already has correct name.", filename);
|
||||
} else {
|
||||
println!("[file] {} -> {}", file_base, new_name);
|
||||
// Only do the rename of --dry-run isn't passed
|
||||
if dry_run == false {
|
||||
fs::rename(filename, new_name.as_str()).expect("Unable to rename file!");
|
||||
}
|
||||
}
|
||||
(new_name_base, is_subtitle)
|
||||
}
|
||||
|
||||
// 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.director));
|
||||
buffer.push_str(&format!("TMDb ID: {}\n", movie.id));
|
||||
buffer.push_str(&format!("Overview: {}\n", movie.overview));
|
||||
buffer
|
||||
}
|
||||
|
||||
// Function to process the passed arguments
|
||||
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);
|
||||
} else {
|
||||
entries.push(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(entries, settings)
|
||||
}
|
||||
|
|
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