Compare commits

..

94 commits
1.1.1 ... main

Author SHA1 Message Date
49de0ce13c
build: Bumped version to 2.3.2 2025-04-07 21:26:27 -05:00
cd0dbcd47b
build: Updated deps to mitigate an issue in tokio 2025-04-07 21:25:19 -05:00
3757dd0a47
fix: Improved compatibility of built package 2025-03-08 16:37:23 -06:00
cf8a9ee764
build: Bumped version to 2.3.1 2025-03-08 16:06:18 -06:00
80fdc110ae
fix: Adapt to changes in deps 2025-03-08 16:05:53 -06:00
c5b1f51233
build: Updated deps 2025-03-08 15:53:54 -06:00
Sayantan Santra
3e7b8fbd5d
Merge pull request #7 from SinTan1729/dependabot/cargo/h2-0.3.26
build(deps): bump h2 from 0.3.24 to 0.3.26
2024-04-05 11:47:00 -05:00
dependabot[bot]
f979559b50
build(deps): bump h2 from 0.3.24 to 0.3.26
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:18:23 +00:00
443c8e6cc9
new: Print some message when flags are set 2024-03-11 18:23:16 -05:00
4f3f349292
chg: Slightly modified the manpage 2024-03-08 10:45:04 -06:00
51e0c84eb4
build: Bumped version to 2.3.0 2024-03-06 15:42:36 -06:00
83bf1f7af6
new: Autocomplete files generation 2024-03-06 15:41:45 -06:00
b4073357f7
fix: No need for the unwrap_or()s 2024-03-06 03:02:53 -06:00
67dcd9e378
new: Do argparsing using clap-rs instead of handwritten parser 2024-03-06 02:56:48 -06:00
b62bffd340
build: Bump version to 2.2.2 2024-03-04 17:09:59 -06:00
Sayantan Santra
4a69071380
Merge pull request #6 from SinTan1729/dependabot/cargo/mio-0.8.11
Bump mio from 0.8.8 to 0.8.11
2024-03-04 17:03:41 -06:00
dependabot[bot]
6027b701f0
Bump mio from 0.8.8 to 0.8.11
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.8 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.8...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:55:54 +00:00
1b43b7a017
build: Bump version to 2.2.1 2024-01-27 16:15:58 -06:00
Sayantan Santra
bc2c9c0135
Merge pull request #5 from SinTan1729/dependabot/cargo/h2-0.3.24
Bump h2 from 0.3.19 to 0.3.24
2024-01-27 16:14:23 -06:00
dependabot[bot]
324cae4fed
Bump h2 from 0.3.19 to 0.3.24
Bumps [h2](https://github.com/hyperium/h2) from 0.3.19 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.19...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 16:11:49 +00:00
64d1749450
build: Bump version to 2.2.0 2023-12-04 18:27:55 -06:00
bc0ccd8107
feat: Possible to merge the short flags 2023-12-04 18:27:22 -06:00
16118a8d5a
feat: Added an option to automatically choose the first option 2023-12-04 17:47:55 -06:00
790ec225ea
build: Bump version to 2.1.6 2023-10-02 22:29:40 -05:00
Sayantan Santra
e74c35472a
Merge pull request #4 from SinTan1729/dependabot/cargo/webpki-0.22.2
Bump webpki from 0.22.1 to 0.22.2
2023-10-03 03:27:59 +00:00
dependabot[bot]
0a851a47fd
Bump webpki from 0.22.1 to 0.22.2
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.1 to 0.22.2.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 21:53:19 +00:00
c2152264f9
fix: Version number 2023-09-13 18:24:47 -05:00
Sayantan Santra
3695c78282
Merge pull request #3 from SinTan1729/dependabot/cargo/webpki-0.22.1
Bump webpki from 0.22.0 to 0.22.1
2023-09-13 23:09:51 +00:00
dependabot[bot]
f827e351c3
Bump webpki from 0.22.0 to 0.22.1
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-13 23:09:15 +00:00
39835837c4
chg: Updated dependencies 2023-08-25 02:27:54 -05:00
a2f0536642
build: Bumped version to 2.1.4 2023-05-29 16:39:50 -05:00
fd98c5d355
new: Support all ISO639_1 codes 2023-05-29 16:38:11 -05:00
cc59a03051
build: Bumped version to 2.1.3 2023-05-29 00:14:54 -05:00
89d15e9fb7
fix: Properly output Option enums 2023-05-29 00:14:12 -05:00
d2fc60b709
build: Bumped version to 2.1.2 2023-05-28 20:23:10 -05:00
6178c022d1
change: Improved filename sanitization and some logic 2023-05-28 20:22:23 -05:00
5e5ba7ea0a
change: Do not dereference str when possible 2023-05-23 19:02:39 -05:00
779064034a
change: More consistency improvements 2023-05-23 19:02:35 -05:00
9456009b4d
change: Use String::new() everywhere 2023-05-23 19:02:26 -05:00
21b26cf4e1
build: Bumped version to 2.1.1 2023-05-22 00:36:58 -05:00
d7172c78f6
fix: Do not rename directory if only file was skipped 2023-05-21 22:42:30 -05:00
591ae8f796
fix: Remember choice if cancelled by user 2023-05-21 22:37:06 -05:00
8b6e83a55a
change: Put variable inside format string directly when possible 2023-05-21 21:10:47 -05:00
0e92c693f7
Bumped version to 2.1.0 2023-05-21 20:51:31 -05:00
c7c1988b73
fix: Still ask for language for preprocessed files 2023-05-21 20:50:16 -05:00
96fcf425b0
fix: Sanitization doesn't squash single hyphens 2023-05-21 20:44:33 -05:00
e9314ccdda
new: Reuse same name for subtitle files and such 2023-05-21 20:39:38 -05:00
4d04f51251
fix: Gracefully exit after user cancellation 2023-05-21 16:47:06 -05:00
02fea4a71c
new: Try to sanitize the filename 2023-05-21 16:10:53 -05:00
7cb18202eb
fix: Do not print both processing and ignoring 2023-05-21 16:03:24 -05:00
8a84f1b2b6 Bumped version to 2.0.0 2023-05-15 17:58:21 -05:00
5a4d7d0e1d Added support for multiple directors 2023-05-15 17:55:45 -05:00
1a83f88c0b Switched to tmdb_api 2023-05-15 17:10:51 -05:00
14e5899f36 Fixed clippy warnings 2023-04-26 17:27:39 -05:00
c0760526fa Updated LICENSE 2023-04-10 20:05:20 -05:00
Sayantan Santra
d8a2b2d988
Merge pull request #2 from SinTan1729/dependabot/cargo/openssl-0.10.48
Bump openssl from 0.10.45 to 0.10.48
2023-03-24 20:52:38 -05:00
dependabot[bot]
be019f221e
Bump openssl from 0.10.45 to 0.10.48
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.45 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.45...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-25 01:33:01 +00:00
Sayantan Santra
64abb5b03b
Update README.md 2023-03-09 09:37:37 -06:00
6d94a5429f Exit gracefully if config file doesn't exist 2023-02-28 23:18:56 -06:00
7165812709 Bumped version to 1.2.4 2023-02-26 14:07:13 -06:00
9d8e10f041 Added make option to create archive for aur 2023-02-26 14:07:04 -06:00
a1c4fb816e Skip irrelevant files 2023-02-26 14:06:29 -06:00
e1af92739b Bumped version to 1.2.3 2023-02-26 02:30:22 -06:00
d1b2fcf8a0 Optimizations for the release build profile 2023-02-26 02:27:28 -06:00
6049f6beb6 Updated README 2023-02-26 00:49:29 -06:00
62f47334bb Bumped version to 1.2.2 2023-02-25 22:47:53 -06:00
4dff4ba511 Created a Makefile and fixed manpage 2023-02-25 22:45:59 -06:00
4105e61829 Add link to releases badge 2023-02-07 20:55:49 -06:00
4ee97eb9a5 Updated README 2023-02-07 18:22:47 -06:00
Sayantan Santra
0745539b88
Merge pull request #1 from SinTan1729/fix-vulnerabilities
Add up to date dependencies
2023-02-07 18:18:16 -06:00
15d3a57ba5 Add up to date dependencies 2023-02-07 18:16:20 -06:00
a138984bac Added AUR badge 2023-02-01 11:32:39 -06:00
5d294a4687 Show full language name in selection menu 2022-12-11 00:15:18 -06:00
cbb6b2ab21 Add info for crates.io 2022-12-10 23:33:09 -06:00
c2d8f05cff Updated README.md 2022-12-10 22:39:07 -06:00
2d6e063a97 Visual improvements 2022-12-10 22:23:44 -06:00
c52643033f Detect subtitle language from the old filename 2022-12-10 21:00:07 -06:00
17300e0362 Merge branch 'main' of https://github.com/SinTan1729/movie-rename
I made some changes online that I forgot to pull before making local
changes.
2022-12-10 18:42:18 -06:00
26eb446de8 Improved arguments handling 2022-12-10 18:39:33 -06:00
SinTan1729
7197c88cfb
Fixed AUR link 2022-12-09 15:46:38 -06:00
3b76f470cd Change name in README 2022-12-09 15:03:10 -06:00
5f348cddcf Version bump and name change 2022-12-09 15:02:20 -06:00
1309ed34b4 Skip renaming if destination exists 2022-12-09 15:00:00 -06:00
29fd0c3646 Visual changes 2022-12-09 14:48:52 -06:00
159e9d49eb Bug fix for directory renaming 2022-12-09 14:16:54 -06:00
0b64b0eae6 Exit when unknown argument is passed 2022-12-09 14:13:35 -06:00
d2c6670901 Change the menu from youchoose to inquire 2022-12-09 14:11:27 -06:00
006c80a822 Refactor functions and structs into separate files 2022-12-09 13:44:17 -06:00
28e7972a15 Added badges to README 2022-12-09 02:39:21 -06:00
e5c73b5cea Fix config file location when XDG folders not set 2022-12-09 02:30:24 -06:00
81095222ff Get the version from Cargo.toml 2022-12-09 02:26:39 -06:00
b0b313ba23 Fixed typo 2022-12-09 00:32:38 -06:00
ace5bbbcd7 Fixed typo in manpage 2022-12-08 23:57:49 -06:00
5ce914bddb Bug fix for argument parsing 2022-12-08 22:56:14 -06:00
12 changed files with 2073 additions and 1336 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
*.mp4
*.srt
*.key
*.tar.gz

2473
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,35 @@
[package]
name = "movie_rename"
version = "0.1.0"
name = "movie-rename"
version = "2.3.2"
build = "build.rs"
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.11.0"
tmdb = "3.0.0"
youchoose = "0.1.1"
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"

View file

@ -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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
movie-rename: A simple tool to rename movies, written in Rust.
Copyright (C) 2023 Sayantan Santra
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:
<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 is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

24
Makefile Normal file
View 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

View file

@ -1,22 +1,40 @@
# movie-rename
[![latest-release](https://img.shields.io/github/v/release/SinTan1729/movie-rename?label=latest%20release)](https://github.com/SinTan1729/movie-rename/releases/latest/)
![commits-since-latest-release](https://img.shields.io/github/commits-since/SInTan1729/movie-rename/latest?label=commits%20since%20latest%20release)
[![AUR package](https://img.shields.io/aur/version/movie-rename-bin?label=AUR&logo=archlinux)](https://aur.archlinux.org/packages/movie-rename-bin/)
# `movie-rename`
### 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
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).
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.
## Notes
- 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.
- 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 Normal file
View 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(())
}

View file

@ -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 "08 Dec 2022" "1.1.1" "movie_rename man page"
.TH man 1 "February 2023" "movie-rename"
.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
@ -14,7 +14,7 @@ A list of filenames (or directory names, not both). -d or --directory must be pa
-n, --dry-run
Performs a dry run, without actually renaming anything.
.TP
-d. --directory
-d, --directory
Runs in directory mode. In this mode, it is assumed that the arguments are directory names, which contain exactly one movie and optionally subtitles.
.TP
-h, --help
@ -23,7 +23,7 @@ Print help information.
-v, --version
Print version information.
.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.
.sp
The first line should have your TMDb API key.

46
src/args.rs Normal file
View 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)
}

218
src/functions.rs Normal file
View file

@ -0,0 +1,218 @@
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
}

View file

@ -1,305 +1,139 @@
use load_file::{self, load_str};
use std::{collections::HashMap, env, fmt, fs, path::Path, process::exit};
use tmdb::{model::*, themoviedb::*};
use torrent_name_parser::Metadata;
use youchoose;
use std::{collections::HashMap, env, fs, path::Path, process::exit};
use tmdb_api::client::{reqwest::ReqwestExecutor, Client};
const VERSION: &str = "1.1.1";
// 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();
// Import all the modules
mod functions;
use functions::process_file;
mod args;
mod structs;
#[tokio::main]
async fn main() {
// 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
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or("$HOME".to_string());
if config_file == String::from("$HOME") {
let mut config_file = env::var("XDG_CONFIG_HOME").unwrap_or(String::from("$HOME"));
if config_file == "$HOME" {
config_file = env::var("$HOME").unwrap();
config_file.push_str("/.config");
}
config_file.push_str("/movie_rename.conf");
config_file.push_str("/movie-rename/config");
if !Path::new(config_file.as_str()).is_file() {
eprintln!("Error reading the config file. Pass --help to see help.");
exit(2);
}
let mut config = load_str!(config_file.as_str()).lines();
let api_key = config.next().unwrap_or("");
let pattern = config.next().unwrap_or("{title} ({year}) - {director}");
if api_key == "" {
eprintln!("Error reading the config file. Pass --help to see help.");
exit(1);
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
let tmdb = TMDb {
api_key: api_key,
language: "en",
};
let tmdb = Client::<ReqwestExecutor>::new(String::from(api_key));
// Iterate over entries
for entry in entries {
// Check if the file/directory exists on disk
match settings["directory"] {
// Check if the file/directory exists on disk and run necessary commands
match flag_directory {
// Normal file
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_file(&entry, &tmdb, pattern, settings["dry_run"]);
process_file(&entry, &tmdb, pattern, flag_dry_run, flag_lucky, None).await;
} else {
eprintln!("The file {} wasn't found on disk, skipping...", entry);
eprintln!("The file {entry} wasn't found on disk, skipping...");
continue;
}
}
// Directory
true => {
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 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()) {
for file in files_in_dir {
if file.is_ok() {
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;
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);
}
}
}
} else {
eprintln!("There was an error accessing the directory {}!", entry);
eprintln!("There was an error accessing the directory {entry}!");
continue;
}
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!");
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..."
);
}
}
}
}
}
} 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 {} wasn't found on disk, skipping...", entry);
eprintln!("The directory {entry} wasn't found on disk, skipping...");
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.contains("-") {
eprintln!("Unknown argument passed: {}", other);
} else {
entries.push(arg);
}
}
}
}
(entries, settings)
}

171
src/structs.rs Normal file
View file

@ -0,0 +1,171 @@
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
}