Add config printing, explain mode, completions, and docs
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -446,6 +446,15 @@ dependencies = [
|
|||||||
"strsim",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "4.5.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.49"
|
version = "4.5.49"
|
||||||
@@ -1314,6 +1323,7 @@ dependencies = [
|
|||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
"csv",
|
"csv",
|
||||||
"directories",
|
"directories",
|
||||||
"httpmock",
|
"httpmock",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ categories = ["command-line-utilities", "filesystem"]
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
clap_complete = "4.5"
|
||||||
csv = "1.3"
|
csv = "1.3"
|
||||||
directories = "5.0"
|
directories = "5.0"
|
||||||
is-terminal = "0.4"
|
is-terminal = "0.4"
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -63,6 +63,26 @@ Common flags:
|
|||||||
- `--quality-tags resolution,codec,source`
|
- `--quality-tags resolution,codec,source`
|
||||||
- `--min-score 0-100` (match threshold)
|
- `--min-score 0-100` (match threshold)
|
||||||
- `--jobs auto|N` and `--net-jobs auto|N`
|
- `--jobs auto|N` and `--net-jobs auto|N`
|
||||||
|
- `--dry-run-summary` (suppress per-file output in dry-run)
|
||||||
|
- `--explain` (show top candidates when skipped)
|
||||||
|
- `--print-config` (print effective config and exit)
|
||||||
|
- `--completions bash|zsh|fish`
|
||||||
|
|
||||||
|
## Help & version :information_source:
|
||||||
|
```bash
|
||||||
|
mov-renamarr --help
|
||||||
|
mov-renamarr --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shell completions :shell:
|
||||||
|
Generate completions:
|
||||||
|
```bash
|
||||||
|
mov-renamarr --completions bash
|
||||||
|
mov-renamarr --completions zsh
|
||||||
|
mov-renamarr --completions fish
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-generated scripts are also available in `completions/`.
|
||||||
|
|
||||||
## Configuration :gear:
|
## Configuration :gear:
|
||||||
Default config location:
|
Default config location:
|
||||||
|
|||||||
70
completions/_mov-renamarr
Normal file
70
completions/_mov-renamarr
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#compdef mov-renamarr
|
||||||
|
|
||||||
|
autoload -U is-at-least
|
||||||
|
|
||||||
|
_mov-renamarr() {
|
||||||
|
typeset -A opt_args
|
||||||
|
typeset -a _arguments_options
|
||||||
|
local ret=1
|
||||||
|
|
||||||
|
if is-at-least 5.2; then
|
||||||
|
_arguments_options=(-s -S -C)
|
||||||
|
else
|
||||||
|
_arguments_options=(-s -C)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local context curcontext="$curcontext" state line
|
||||||
|
_arguments "${_arguments_options[@]}" : \
|
||||||
|
'--input=[]:DIR:_files' \
|
||||||
|
'--output=[]:DIR:_files' \
|
||||||
|
'--config=[]:PATH:_files' \
|
||||||
|
'--provider=[]:PROVIDER:(auto omdb tmdb both)' \
|
||||||
|
'--api-key-omdb=[]:API_KEY_OMDB:_default' \
|
||||||
|
'--api-key-tmdb=[]:API_KEY_TMDB:_default' \
|
||||||
|
'--cache=[]:PATH:_files' \
|
||||||
|
'--report=[]' \
|
||||||
|
'--report-format=[]:REPORT_FORMAT:(text json csv)' \
|
||||||
|
'--min-score=[]:MIN_SCORE:_default' \
|
||||||
|
'--quality-tags=[]:LIST:_default' \
|
||||||
|
'--color=[]:COLOR:(auto always never)' \
|
||||||
|
'--llm-mode=[]:LLM_MODE:(off parse assist)' \
|
||||||
|
'--llm-endpoint=[]:URL:_default' \
|
||||||
|
'--llm-model=[]:NAME:_default' \
|
||||||
|
'--llm-timeout=[]:SECONDS:_default' \
|
||||||
|
'--llm-max-tokens=[]:N:_default' \
|
||||||
|
'--jobs=[]:JOBS:_default' \
|
||||||
|
'--net-jobs=[]:NET_JOBS:_default' \
|
||||||
|
'--completions=[]:COMPLETIONS:(bash zsh fish)' \
|
||||||
|
'--refresh-cache[]' \
|
||||||
|
'--dry-run[]' \
|
||||||
|
'--dry-run-summary[]' \
|
||||||
|
'(--rename-in-place)--move[]' \
|
||||||
|
'(--move)--rename-in-place[]' \
|
||||||
|
'--interactive[]' \
|
||||||
|
'--sidecar-notes[]' \
|
||||||
|
'--sidecars[]' \
|
||||||
|
'--overwrite[]' \
|
||||||
|
'--suffix[]' \
|
||||||
|
'--include-id[]' \
|
||||||
|
'--no-lookup[]' \
|
||||||
|
'--explain[]' \
|
||||||
|
'--print-config[]' \
|
||||||
|
'--verbose[]' \
|
||||||
|
'-h[Print help]' \
|
||||||
|
'--help[Print help]' \
|
||||||
|
'-V[Print version]' \
|
||||||
|
'--version[Print version]' \
|
||||||
|
&& ret=0
|
||||||
|
}
|
||||||
|
|
||||||
|
(( $+functions[_mov-renamarr_commands] )) ||
|
||||||
|
_mov-renamarr_commands() {
|
||||||
|
local commands; commands=()
|
||||||
|
_describe -t commands 'mov-renamarr commands' commands "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$funcstack[1]" = "_mov-renamarr" ]; then
|
||||||
|
_mov-renamarr "$@"
|
||||||
|
else
|
||||||
|
compdef _mov-renamarr mov-renamarr
|
||||||
|
fi
|
||||||
126
completions/mov-renamarr.bash
Normal file
126
completions/mov-renamarr.bash
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
_mov-renamarr() {
|
||||||
|
local i cur prev opts cmd
|
||||||
|
COMPREPLY=()
|
||||||
|
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||||
|
cur="$2"
|
||||||
|
else
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
fi
|
||||||
|
prev="$3"
|
||||||
|
cmd=""
|
||||||
|
opts=""
|
||||||
|
|
||||||
|
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
|
||||||
|
do
|
||||||
|
case "${cmd},${i}" in
|
||||||
|
",$1")
|
||||||
|
cmd="mov__renamarr"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "${cmd}" in
|
||||||
|
mov__renamarr)
|
||||||
|
opts="-h -V --input --output --config --provider --api-key-omdb --api-key-tmdb --cache --refresh-cache --dry-run --dry-run-summary --move --rename-in-place --interactive --report --report-format --sidecar-notes --sidecars --overwrite --suffix --min-score --include-id --quality-tags --color --llm-mode --llm-endpoint --llm-model --llm-timeout --llm-max-tokens --jobs --net-jobs --no-lookup --explain --print-config --completions --verbose --help --version"
|
||||||
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "${prev}" in
|
||||||
|
--input)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--config)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "auto omdb tmdb both" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--api-key-omdb)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--api-key-tmdb)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--cache)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--report)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--report-format)
|
||||||
|
COMPREPLY=($(compgen -W "text json csv" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--min-score)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--quality-tags)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--color)
|
||||||
|
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--llm-mode)
|
||||||
|
COMPREPLY=($(compgen -W "off parse assist" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--llm-endpoint)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--llm-model)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--llm-timeout)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--llm-max-tokens)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--jobs)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--net-jobs)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--completions)
|
||||||
|
COMPREPLY=($(compgen -W "bash zsh fish" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
COMPREPLY=()
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
|
||||||
|
complete -F _mov-renamarr -o nosort -o bashdefault -o default mov-renamarr
|
||||||
|
else
|
||||||
|
complete -F _mov-renamarr -o bashdefault -o default mov-renamarr
|
||||||
|
fi
|
||||||
48
completions/mov-renamarr.fish
Normal file
48
completions/mov-renamarr.fish
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
complete -c mov-renamarr -l input -r -F
|
||||||
|
complete -c mov-renamarr -l output -r -F
|
||||||
|
complete -c mov-renamarr -l config -r -F
|
||||||
|
complete -c mov-renamarr -l provider -r -f -a "auto\t''
|
||||||
|
omdb\t''
|
||||||
|
tmdb\t''
|
||||||
|
both\t''"
|
||||||
|
complete -c mov-renamarr -l api-key-omdb -r
|
||||||
|
complete -c mov-renamarr -l api-key-tmdb -r
|
||||||
|
complete -c mov-renamarr -l cache -r -F
|
||||||
|
complete -c mov-renamarr -l report -r -F
|
||||||
|
complete -c mov-renamarr -l report-format -r -f -a "text\t''
|
||||||
|
json\t''
|
||||||
|
csv\t''"
|
||||||
|
complete -c mov-renamarr -l min-score -r
|
||||||
|
complete -c mov-renamarr -l quality-tags -r
|
||||||
|
complete -c mov-renamarr -l color -r -f -a "auto\t''
|
||||||
|
always\t''
|
||||||
|
never\t''"
|
||||||
|
complete -c mov-renamarr -l llm-mode -r -f -a "off\t''
|
||||||
|
parse\t''
|
||||||
|
assist\t''"
|
||||||
|
complete -c mov-renamarr -l llm-endpoint -r
|
||||||
|
complete -c mov-renamarr -l llm-model -r
|
||||||
|
complete -c mov-renamarr -l llm-timeout -r
|
||||||
|
complete -c mov-renamarr -l llm-max-tokens -r
|
||||||
|
complete -c mov-renamarr -l jobs -r
|
||||||
|
complete -c mov-renamarr -l net-jobs -r
|
||||||
|
complete -c mov-renamarr -l completions -r -f -a "bash\t''
|
||||||
|
zsh\t''
|
||||||
|
fish\t''"
|
||||||
|
complete -c mov-renamarr -l refresh-cache
|
||||||
|
complete -c mov-renamarr -l dry-run
|
||||||
|
complete -c mov-renamarr -l dry-run-summary
|
||||||
|
complete -c mov-renamarr -l move
|
||||||
|
complete -c mov-renamarr -l rename-in-place
|
||||||
|
complete -c mov-renamarr -l interactive
|
||||||
|
complete -c mov-renamarr -l sidecar-notes
|
||||||
|
complete -c mov-renamarr -l sidecars
|
||||||
|
complete -c mov-renamarr -l overwrite
|
||||||
|
complete -c mov-renamarr -l suffix
|
||||||
|
complete -c mov-renamarr -l include-id
|
||||||
|
complete -c mov-renamarr -l no-lookup
|
||||||
|
complete -c mov-renamarr -l explain
|
||||||
|
complete -c mov-renamarr -l print-config
|
||||||
|
complete -c mov-renamarr -l verbose
|
||||||
|
complete -c mov-renamarr -s h -l help -d 'Print help'
|
||||||
|
complete -c mov-renamarr -s V -l version -d 'Print version'
|
||||||
@@ -43,6 +43,7 @@ refresh_cache = false
|
|||||||
report_format = "text" # text|json|csv
|
report_format = "text" # text|json|csv
|
||||||
sidecar_notes = false # write per-file notes for skipped/failed
|
sidecar_notes = false # write per-file notes for skipped/failed
|
||||||
sidecars = false # move/copy sidecar files (srt, nfo, etc)
|
sidecars = false # move/copy sidecar files (srt, nfo, etc)
|
||||||
|
dry_run_summary = false # suppress per-file output (use with --dry-run)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Matching and naming
|
## Matching and naming
|
||||||
@@ -50,6 +51,7 @@ sidecars = false # move/copy sidecar files (srt, nfo, etc)
|
|||||||
min_score = 80 # 0-100 match threshold
|
min_score = 80 # 0-100 match threshold
|
||||||
include_id = false # include tmdb/imdb id in filenames
|
include_id = false # include tmdb/imdb id in filenames
|
||||||
no_lookup = false # skip external providers (filename/LLM only)
|
no_lookup = false # skip external providers (filename/LLM only)
|
||||||
|
explain = false # show top candidates when skipped
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quality tags
|
## Quality tags
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ This repo includes a Gitea Actions workflow that builds Linux binaries for
|
|||||||
## One-time setup (Gitea Actions)
|
## One-time setup (Gitea Actions)
|
||||||
1) Create a personal access token with repo write access.
|
1) Create a personal access token with repo write access.
|
||||||
2) Add it to the repo secrets as `RELEASE_TOKEN`.
|
2) Add it to the repo secrets as `RELEASE_TOKEN`.
|
||||||
|
3) Ensure at least one Gitea Actions runner is online.
|
||||||
|
4) If uploads fail with `413 Request Entity Too Large`, increase your reverse proxy upload limit (e.g., nginx `client_max_body_size`).
|
||||||
|
|
||||||
## Release steps
|
## Release steps
|
||||||
1) Update `CHANGELOG.md`.
|
1) Update `CHANGELOG.md`.
|
||||||
|
|||||||
33
src/cli.rs
33
src/cli.rs
@@ -2,13 +2,13 @@ use std::path::PathBuf;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "mov-renamarr", version, about = "Rename movie files into Radarr-compatible naming")]
|
#[command(name = "mov-renamarr", version, about = "Rename movie files into Radarr-compatible naming")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[arg(long, value_name = "DIR")]
|
#[arg(long, value_name = "DIR", required_unless_present_any = ["completions"])]
|
||||||
pub input: PathBuf,
|
pub input: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(long, value_name = "DIR")]
|
#[arg(long, value_name = "DIR")]
|
||||||
pub output: Option<PathBuf>,
|
pub output: Option<PathBuf>,
|
||||||
@@ -34,6 +34,9 @@ pub struct Cli {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub dry_run_summary: bool,
|
||||||
|
|
||||||
#[arg(long = "move", conflicts_with = "rename_in_place")]
|
#[arg(long = "move", conflicts_with = "rename_in_place")]
|
||||||
pub move_files: bool,
|
pub move_files: bool,
|
||||||
|
|
||||||
@@ -102,11 +105,20 @@ pub struct Cli {
|
|||||||
#[arg(long, alias = "offline")]
|
#[arg(long, alias = "offline")]
|
||||||
pub no_lookup: bool,
|
pub no_lookup: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub explain: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub print_config: bool,
|
||||||
|
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
pub completions: Option<CompletionShell>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ProviderChoice {
|
pub enum ProviderChoice {
|
||||||
Auto,
|
Auto,
|
||||||
@@ -115,7 +127,7 @@ pub enum ProviderChoice {
|
|||||||
Both,
|
Both,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ReportFormat {
|
pub enum ReportFormat {
|
||||||
Text,
|
Text,
|
||||||
@@ -123,7 +135,7 @@ pub enum ReportFormat {
|
|||||||
Csv,
|
Csv,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
Auto,
|
Auto,
|
||||||
@@ -131,7 +143,7 @@ pub enum ColorMode {
|
|||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum LlmMode {
|
pub enum LlmMode {
|
||||||
Off,
|
Off,
|
||||||
@@ -139,6 +151,13 @@ pub enum LlmMode {
|
|||||||
Assist,
|
Assist,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)]
|
||||||
|
pub enum CompletionShell {
|
||||||
|
Bash,
|
||||||
|
Zsh,
|
||||||
|
Fish,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum JobsArg {
|
pub enum JobsArg {
|
||||||
Auto,
|
Auto,
|
||||||
|
|||||||
162
src/config.rs
162
src/config.rs
@@ -5,7 +5,7 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cli::{Cli, ColorMode, JobsArg, LlmMode, ProviderChoice, ReportFormat};
|
use crate::cli::{Cli, ColorMode, JobsArg, LlmMode, ProviderChoice, ReportFormat};
|
||||||
|
|
||||||
@@ -34,10 +34,12 @@ pub struct Settings {
|
|||||||
pub net_jobs: usize,
|
pub net_jobs: usize,
|
||||||
pub no_lookup: bool,
|
pub no_lookup: bool,
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
pub dry_run_summary: bool,
|
||||||
pub move_files: bool,
|
pub move_files: bool,
|
||||||
pub rename_in_place: bool,
|
pub rename_in_place: bool,
|
||||||
pub interactive: bool,
|
pub interactive: bool,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
pub explain: bool,
|
||||||
pub omdb_base_url: String,
|
pub omdb_base_url: String,
|
||||||
pub tmdb_base_url: String,
|
pub tmdb_base_url: String,
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,25 @@ impl Default for QualityTags {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl QualityTags {
|
||||||
|
pub fn to_list(&self) -> Vec<String> {
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
if self.resolution {
|
||||||
|
tags.push("resolution".to_string());
|
||||||
|
}
|
||||||
|
if self.codec {
|
||||||
|
tags.push("codec".to_string());
|
||||||
|
}
|
||||||
|
if self.source {
|
||||||
|
tags.push("source".to_string());
|
||||||
|
}
|
||||||
|
if tags.is_empty() {
|
||||||
|
tags.push("none".to_string());
|
||||||
|
}
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct LlmSettings {
|
pub struct LlmSettings {
|
||||||
pub mode: LlmMode,
|
pub mode: LlmMode,
|
||||||
@@ -68,6 +89,50 @@ pub struct LlmSettings {
|
|||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PrintableSettings {
|
||||||
|
input: String,
|
||||||
|
output: String,
|
||||||
|
provider: ProviderChoice,
|
||||||
|
api_key_omdb: Option<String>,
|
||||||
|
api_key_tmdb: Option<String>,
|
||||||
|
cache_path: String,
|
||||||
|
cache_ttl_days: u32,
|
||||||
|
refresh_cache: bool,
|
||||||
|
report_format: ReportFormat,
|
||||||
|
report_path: Option<String>,
|
||||||
|
sidecar_notes: bool,
|
||||||
|
sidecars: bool,
|
||||||
|
dry_run_summary: bool,
|
||||||
|
overwrite: bool,
|
||||||
|
suffix: bool,
|
||||||
|
min_score: u8,
|
||||||
|
include_id: bool,
|
||||||
|
quality_tags: Vec<String>,
|
||||||
|
color: ColorMode,
|
||||||
|
jobs: usize,
|
||||||
|
net_jobs: usize,
|
||||||
|
no_lookup: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
move_files: bool,
|
||||||
|
rename_in_place: bool,
|
||||||
|
interactive: bool,
|
||||||
|
verbose: bool,
|
||||||
|
explain: bool,
|
||||||
|
omdb_base_url: String,
|
||||||
|
tmdb_base_url: String,
|
||||||
|
llm: PrintableLlm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PrintableLlm {
|
||||||
|
mode: LlmMode,
|
||||||
|
endpoint: String,
|
||||||
|
model: Option<String>,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
max_tokens: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for LlmSettings {
|
impl Default for LlmSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -103,6 +168,8 @@ struct FileConfig {
|
|||||||
omdb_base_url: Option<String>,
|
omdb_base_url: Option<String>,
|
||||||
tmdb_base_url: Option<String>,
|
tmdb_base_url: Option<String>,
|
||||||
no_lookup: Option<bool>,
|
no_lookup: Option<bool>,
|
||||||
|
dry_run_summary: Option<bool>,
|
||||||
|
explain: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default)]
|
#[derive(Debug, Deserialize, Default)]
|
||||||
@@ -139,8 +206,11 @@ pub fn build_settings(cli: &Cli) -> Result<Settings> {
|
|||||||
}
|
}
|
||||||
let file_config = load_config_file(&config_path)?;
|
let file_config = load_config_file(&config_path)?;
|
||||||
|
|
||||||
let input = cli.input.clone();
|
let input = cli
|
||||||
let output = resolve_output(cli)?;
|
.input
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow!("--input is required unless --completions is used"))?;
|
||||||
|
let output = resolve_output(cli, &input)?;
|
||||||
|
|
||||||
let mut settings = Settings {
|
let mut settings = Settings {
|
||||||
input,
|
input,
|
||||||
@@ -166,10 +236,12 @@ pub fn build_settings(cli: &Cli) -> Result<Settings> {
|
|||||||
net_jobs: default_net_jobs(default_jobs()),
|
net_jobs: default_net_jobs(default_jobs()),
|
||||||
no_lookup: false,
|
no_lookup: false,
|
||||||
dry_run: cli.dry_run,
|
dry_run: cli.dry_run,
|
||||||
|
dry_run_summary: false,
|
||||||
move_files: cli.move_files,
|
move_files: cli.move_files,
|
||||||
rename_in_place: cli.rename_in_place,
|
rename_in_place: cli.rename_in_place,
|
||||||
interactive: cli.interactive,
|
interactive: cli.interactive,
|
||||||
verbose: cli.verbose,
|
verbose: cli.verbose,
|
||||||
|
explain: false,
|
||||||
omdb_base_url: "https://www.omdbapi.com".to_string(),
|
omdb_base_url: "https://www.omdbapi.com".to_string(),
|
||||||
tmdb_base_url: "https://api.themoviedb.org/3".to_string(),
|
tmdb_base_url: "https://api.themoviedb.org/3".to_string(),
|
||||||
};
|
};
|
||||||
@@ -189,11 +261,11 @@ pub fn init_default_config() -> Result<PathBuf> {
|
|||||||
Ok(config_path)
|
Ok(config_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_output(cli: &Cli) -> Result<PathBuf> {
|
fn resolve_output(cli: &Cli, input: &Path) -> Result<PathBuf> {
|
||||||
match (cli.rename_in_place, cli.output.as_ref()) {
|
match (cli.rename_in_place, cli.output.as_ref()) {
|
||||||
(true, None) => Ok(cli.input.clone()),
|
(true, None) => Ok(input.to_path_buf()),
|
||||||
(true, Some(out)) => {
|
(true, Some(out)) => {
|
||||||
if out != &cli.input {
|
if out != input {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"--rename-in-place requires output to be omitted or the same as input"
|
"--rename-in-place requires output to be omitted or the same as input"
|
||||||
))
|
))
|
||||||
@@ -202,7 +274,7 @@ fn resolve_output(cli: &Cli) -> Result<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(false, Some(out)) => {
|
(false, Some(out)) => {
|
||||||
if out == &cli.input {
|
if out == input {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"output directory must be different from input unless --rename-in-place is set"
|
"output directory must be different from input unless --rename-in-place is set"
|
||||||
))
|
))
|
||||||
@@ -287,12 +359,16 @@ fn default_config_template() -> String {
|
|||||||
"sidecar_notes = false",
|
"sidecar_notes = false",
|
||||||
"# sidecars copies/moves subtitle/nfo/etc files with the movie file.",
|
"# sidecars copies/moves subtitle/nfo/etc files with the movie file.",
|
||||||
"sidecars = false",
|
"sidecars = false",
|
||||||
|
"# dry_run_summary suppresses per-file output (use with --dry-run for large batches).",
|
||||||
|
"dry_run_summary = false",
|
||||||
"# overwrite replaces existing files; suffix adds \" (1)\", \" (2)\", etc.",
|
"# overwrite replaces existing files; suffix adds \" (1)\", \" (2)\", etc.",
|
||||||
"overwrite = false",
|
"overwrite = false",
|
||||||
"suffix = false",
|
"suffix = false",
|
||||||
"# Disable external lookups (use filename/LLM only).",
|
"# Disable external lookups (use filename/LLM only).",
|
||||||
"# When true, provider selection is ignored.",
|
"# When true, provider selection is ignored.",
|
||||||
"no_lookup = false",
|
"no_lookup = false",
|
||||||
|
"# explain prints top candidate matches when a file is skipped.",
|
||||||
|
"explain = false",
|
||||||
"# min_score is 0-100 (match confidence threshold).",
|
"# min_score is 0-100 (match confidence threshold).",
|
||||||
"min_score = 80",
|
"min_score = 80",
|
||||||
"# include_id adds tmdb-XXXX or imdb-ttXXXX in the filename.",
|
"# include_id adds tmdb-XXXX or imdb-ttXXXX in the filename.",
|
||||||
@@ -362,6 +438,9 @@ fn apply_file_config(settings: &mut Settings, file: &FileConfig) -> Result<()> {
|
|||||||
if let Some(sidecars) = file.sidecars {
|
if let Some(sidecars) = file.sidecars {
|
||||||
settings.sidecars = sidecars;
|
settings.sidecars = sidecars;
|
||||||
}
|
}
|
||||||
|
if let Some(dry_run_summary) = file.dry_run_summary {
|
||||||
|
settings.dry_run_summary = dry_run_summary;
|
||||||
|
}
|
||||||
if let Some(overwrite) = file.overwrite {
|
if let Some(overwrite) = file.overwrite {
|
||||||
settings.overwrite = overwrite;
|
settings.overwrite = overwrite;
|
||||||
}
|
}
|
||||||
@@ -393,6 +472,9 @@ fn apply_file_config(settings: &mut Settings, file: &FileConfig) -> Result<()> {
|
|||||||
if let Some(no_lookup) = file.no_lookup {
|
if let Some(no_lookup) = file.no_lookup {
|
||||||
settings.no_lookup = no_lookup;
|
settings.no_lookup = no_lookup;
|
||||||
}
|
}
|
||||||
|
if let Some(explain) = file.explain {
|
||||||
|
settings.explain = explain;
|
||||||
|
}
|
||||||
if let Some(llm) = &file.llm {
|
if let Some(llm) = &file.llm {
|
||||||
apply_file_llm(settings, llm);
|
apply_file_llm(settings, llm);
|
||||||
}
|
}
|
||||||
@@ -469,9 +551,11 @@ fn apply_env_overrides(settings: &mut Settings) -> Result<()> {
|
|||||||
apply_env_bool("MOV_RENAMARR_INCLUDE_ID", |value| settings.include_id = value);
|
apply_env_bool("MOV_RENAMARR_INCLUDE_ID", |value| settings.include_id = value);
|
||||||
apply_env_bool("MOV_RENAMARR_SIDECARS", |value| settings.sidecars = value);
|
apply_env_bool("MOV_RENAMARR_SIDECARS", |value| settings.sidecars = value);
|
||||||
apply_env_bool("MOV_RENAMARR_SIDECAR_NOTES", |value| settings.sidecar_notes = value);
|
apply_env_bool("MOV_RENAMARR_SIDECAR_NOTES", |value| settings.sidecar_notes = value);
|
||||||
|
apply_env_bool("MOV_RENAMARR_DRY_RUN_SUMMARY", |value| settings.dry_run_summary = value);
|
||||||
apply_env_bool("MOV_RENAMARR_OVERWRITE", |value| settings.overwrite = value);
|
apply_env_bool("MOV_RENAMARR_OVERWRITE", |value| settings.overwrite = value);
|
||||||
apply_env_bool("MOV_RENAMARR_SUFFIX", |value| settings.suffix = value);
|
apply_env_bool("MOV_RENAMARR_SUFFIX", |value| settings.suffix = value);
|
||||||
apply_env_bool("MOV_RENAMARR_NO_LOOKUP", |value| settings.no_lookup = value);
|
apply_env_bool("MOV_RENAMARR_NO_LOOKUP", |value| settings.no_lookup = value);
|
||||||
|
apply_env_bool("MOV_RENAMARR_EXPLAIN", |value| settings.explain = value);
|
||||||
|
|
||||||
apply_env_string("MOV_RENAMARR_QUALITY_TAGS", |value| {
|
apply_env_string("MOV_RENAMARR_QUALITY_TAGS", |value| {
|
||||||
if let Ok(tags) = parse_quality_tags(&split_list(&value)) {
|
if let Ok(tags) = parse_quality_tags(&split_list(&value)) {
|
||||||
@@ -561,6 +645,9 @@ fn apply_cli_overrides(settings: &mut Settings, cli: &Cli) -> Result<()> {
|
|||||||
if cli.sidecars {
|
if cli.sidecars {
|
||||||
settings.sidecars = true;
|
settings.sidecars = true;
|
||||||
}
|
}
|
||||||
|
if cli.dry_run_summary {
|
||||||
|
settings.dry_run_summary = true;
|
||||||
|
}
|
||||||
if cli.overwrite {
|
if cli.overwrite {
|
||||||
settings.overwrite = true;
|
settings.overwrite = true;
|
||||||
}
|
}
|
||||||
@@ -588,6 +675,9 @@ fn apply_cli_overrides(settings: &mut Settings, cli: &Cli) -> Result<()> {
|
|||||||
if cli.no_lookup {
|
if cli.no_lookup {
|
||||||
settings.no_lookup = true;
|
settings.no_lookup = true;
|
||||||
}
|
}
|
||||||
|
if cli.explain {
|
||||||
|
settings.explain = true;
|
||||||
|
}
|
||||||
if let Some(mode) = &cli.llm_mode {
|
if let Some(mode) = &cli.llm_mode {
|
||||||
settings.llm.mode = mode.clone();
|
settings.llm.mode = mode.clone();
|
||||||
}
|
}
|
||||||
@@ -616,6 +706,9 @@ fn validate_settings(settings: &mut Settings) -> Result<()> {
|
|||||||
if settings.min_score > 100 {
|
if settings.min_score > 100 {
|
||||||
return Err(anyhow!("min-score must be between 0 and 100"));
|
return Err(anyhow!("min-score must be between 0 and 100"));
|
||||||
}
|
}
|
||||||
|
if settings.dry_run_summary && !settings.dry_run {
|
||||||
|
return Err(anyhow!("--dry-run-summary requires --dry-run"));
|
||||||
|
}
|
||||||
if settings.net_jobs == 0 {
|
if settings.net_jobs == 0 {
|
||||||
settings.net_jobs = 1;
|
settings.net_jobs = 1;
|
||||||
}
|
}
|
||||||
@@ -636,6 +729,61 @@ pub fn default_net_jobs(jobs: usize) -> usize {
|
|||||||
std::cmp::max(1, std::cmp::min(2, jobs))
|
std::cmp::max(1, std::cmp::min(2, jobs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_settings(settings: &Settings) -> Result<String> {
|
||||||
|
let printable = PrintableSettings::from(settings);
|
||||||
|
toml::to_string_pretty(&printable).context("failed to serialize settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Settings> for PrintableSettings {
|
||||||
|
fn from(settings: &Settings) -> Self {
|
||||||
|
Self {
|
||||||
|
input: settings.input.display().to_string(),
|
||||||
|
output: settings.output.display().to_string(),
|
||||||
|
provider: settings.provider.clone(),
|
||||||
|
api_key_omdb: settings.api_key_omdb.clone(),
|
||||||
|
api_key_tmdb: settings.api_key_tmdb.clone(),
|
||||||
|
cache_path: settings.cache_path.display().to_string(),
|
||||||
|
cache_ttl_days: settings.cache_ttl_days,
|
||||||
|
refresh_cache: settings.refresh_cache,
|
||||||
|
report_format: settings.report_format.clone(),
|
||||||
|
report_path: settings.report_path.as_ref().map(|p| p.display().to_string()),
|
||||||
|
sidecar_notes: settings.sidecar_notes,
|
||||||
|
sidecars: settings.sidecars,
|
||||||
|
dry_run_summary: settings.dry_run_summary,
|
||||||
|
overwrite: settings.overwrite,
|
||||||
|
suffix: settings.suffix,
|
||||||
|
min_score: settings.min_score,
|
||||||
|
include_id: settings.include_id,
|
||||||
|
quality_tags: settings.quality_tags.to_list(),
|
||||||
|
color: settings.color.clone(),
|
||||||
|
jobs: settings.jobs,
|
||||||
|
net_jobs: settings.net_jobs,
|
||||||
|
no_lookup: settings.no_lookup,
|
||||||
|
dry_run: settings.dry_run,
|
||||||
|
move_files: settings.move_files,
|
||||||
|
rename_in_place: settings.rename_in_place,
|
||||||
|
interactive: settings.interactive,
|
||||||
|
verbose: settings.verbose,
|
||||||
|
explain: settings.explain,
|
||||||
|
omdb_base_url: settings.omdb_base_url.clone(),
|
||||||
|
tmdb_base_url: settings.tmdb_base_url.clone(),
|
||||||
|
llm: PrintableLlm::from(&settings.llm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&LlmSettings> for PrintableLlm {
|
||||||
|
fn from(settings: &LlmSettings) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: settings.mode.clone(),
|
||||||
|
endpoint: settings.endpoint.clone(),
|
||||||
|
model: settings.model.clone(),
|
||||||
|
timeout_seconds: settings.timeout_seconds,
|
||||||
|
max_tokens: settings.max_tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_jobs_setting(raw: &str, fallback: usize) -> Result<usize> {
|
fn parse_jobs_setting(raw: &str, fallback: usize) -> Result<usize> {
|
||||||
if raw.eq_ignore_ascii_case("auto") {
|
if raw.eq_ignore_ascii_case("auto") {
|
||||||
return Ok(fallback);
|
return Ok(fallback);
|
||||||
|
|||||||
31
src/main.rs
31
src/main.rs
@@ -11,9 +11,10 @@ mod report;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::{CommandFactory, Parser};
|
||||||
|
use clap_complete::{generate, Shell};
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::{Cli, CompletionShell};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
if std::env::args_os().len() == 1 {
|
if std::env::args_os().len() == 1 {
|
||||||
@@ -23,12 +24,38 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
if let Some(shell) = cli.completions.clone() {
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
let shell = match shell {
|
||||||
|
CompletionShell::Bash => Shell::Bash,
|
||||||
|
CompletionShell::Zsh => Shell::Zsh,
|
||||||
|
CompletionShell::Fish => Shell::Fish,
|
||||||
|
};
|
||||||
|
generate(shell, &mut cmd, "mov-renamarr", &mut std::io::stdout());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let settings = config::build_settings(&cli)?;
|
let settings = config::build_settings(&cli)?;
|
||||||
|
|
||||||
|
if cli.print_config {
|
||||||
|
let rendered = config::format_settings(&settings)?;
|
||||||
|
println!("{rendered}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let report_format = settings.report_format.clone();
|
let report_format = settings.report_format.clone();
|
||||||
let report_path = settings.report_path.clone();
|
let report_path = settings.report_path.clone();
|
||||||
|
let dry_run_summary = settings.dry_run_summary;
|
||||||
|
|
||||||
let report = pipeline::run(settings)?;
|
let report = pipeline::run(settings)?;
|
||||||
|
if dry_run_summary
|
||||||
|
&& report_path.is_none()
|
||||||
|
&& matches!(report_format, crate::cli::ReportFormat::Text)
|
||||||
|
{
|
||||||
|
report.write_summary(None)?;
|
||||||
|
} else {
|
||||||
report.write(&report_format, report_path.as_deref())?;
|
report.write(&report_format, report_path.as_deref())?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ pub enum StatusKind {
|
|||||||
pub struct Output {
|
pub struct Output {
|
||||||
use_color: bool,
|
use_color: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
summary_only: bool,
|
||||||
lock: Mutex<()>,
|
lock: Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
pub fn new(color_mode: &ColorMode, verbose: bool) -> Self {
|
pub fn new(color_mode: &ColorMode, verbose: bool, summary_only: bool) -> Self {
|
||||||
let use_color = match color_mode {
|
let use_color = match color_mode {
|
||||||
ColorMode::Always => true,
|
ColorMode::Always => true,
|
||||||
ColorMode::Never => false,
|
ColorMode::Never => false,
|
||||||
@@ -29,6 +30,7 @@ impl Output {
|
|||||||
Self {
|
Self {
|
||||||
use_color,
|
use_color,
|
||||||
verbose,
|
verbose,
|
||||||
|
summary_only,
|
||||||
lock: Mutex::new(()),
|
lock: Mutex::new(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,9 @@ impl Output {
|
|||||||
result: &str,
|
result: &str,
|
||||||
output_name: Option<&str>,
|
output_name: Option<&str>,
|
||||||
) {
|
) {
|
||||||
|
if self.summary_only && !matches!(status, StatusKind::Failed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _guard = self.lock.lock().unwrap();
|
let _guard = self.lock.lock().unwrap();
|
||||||
let prefix = format!("[{}/{}]", index, total);
|
let prefix = format!("[{}/{}]", index, total);
|
||||||
let status_label = match status {
|
let status_label = match status {
|
||||||
@@ -74,6 +79,11 @@ impl Output {
|
|||||||
eprintln!("{msg}");
|
eprintln!("{msg}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn detail(&self, message: &str) {
|
||||||
|
let _guard = self.lock.lock().unwrap();
|
||||||
|
println!("{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn info(&self, message: &str) {
|
pub fn info(&self, message: &str) {
|
||||||
if self.verbose {
|
if self.verbose {
|
||||||
|
|||||||
27
src/parse.rs
27
src/parse.rs
@@ -150,4 +150,31 @@ mod tests {
|
|||||||
assert_eq!(hints.title.as_deref(), Some("Zootopia Vlix"));
|
assert_eq!(hints.title.as_deref(), Some("Zootopia Vlix"));
|
||||||
assert!(hints.alt_titles.iter().any(|t| t == "Zootopia"));
|
assert!(hints.alt_titles.iter().any(|t| t == "Zootopia"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_foreign_title_ascii() {
|
||||||
|
let path = Path::new("Cidade.de.Deus.2002.1080p.BluRay.x264.mkv");
|
||||||
|
let hints = parse_filename(path);
|
||||||
|
assert_eq!(hints.title.as_deref(), Some("Cidade de Deus"));
|
||||||
|
assert_eq!(hints.year, Some(2002));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_release_tags() {
|
||||||
|
let path = Path::new("Movie.Title.2019.1080p.HDRip.x264.AAC.mkv");
|
||||||
|
let hints = parse_filename(path);
|
||||||
|
assert_eq!(hints.title.as_deref(), Some("Movie Title"));
|
||||||
|
assert_eq!(hints.year, Some(2019));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_subtitle_separator() {
|
||||||
|
let path = Path::new("Doctor.Strange.In.The.Multiverse.of.Madness.2022.2160p.mkv");
|
||||||
|
let hints = parse_filename(path);
|
||||||
|
assert_eq!(
|
||||||
|
hints.title.as_deref(),
|
||||||
|
Some("Doctor Strange In The Multiverse of Madness")
|
||||||
|
);
|
||||||
|
assert_eq!(hints.year, Some(2022));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ use crate::utils::{sanitize_filename, Semaphore};
|
|||||||
pub fn run(mut settings: Settings) -> Result<Report> {
|
pub fn run(mut settings: Settings) -> Result<Report> {
|
||||||
ensure_ffprobe()?;
|
ensure_ffprobe()?;
|
||||||
|
|
||||||
let output = Arc::new(Output::new(&settings.color, settings.verbose));
|
let output = Arc::new(Output::new(
|
||||||
|
&settings.color,
|
||||||
|
settings.verbose,
|
||||||
|
settings.dry_run_summary,
|
||||||
|
));
|
||||||
if settings.no_lookup {
|
if settings.no_lookup {
|
||||||
output.warn("No-lookup mode enabled: using filename/LLM only (no external providers).");
|
output.warn("No-lookup mode enabled: using filename/LLM only (no external providers).");
|
||||||
}
|
}
|
||||||
@@ -245,6 +249,18 @@ fn process_file(
|
|||||||
"no match",
|
"no match",
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
if settings.explain && !outcome.candidates.is_empty() {
|
||||||
|
output.detail(" Candidates:");
|
||||||
|
for candidate in summarize_candidates(&outcome.candidates, 5) {
|
||||||
|
output.detail(&format!(
|
||||||
|
" - {} ({}) [{}] score {:.1}",
|
||||||
|
candidate.title,
|
||||||
|
candidate.year.map(|y| y.to_string()).unwrap_or_else(|| "?".into()),
|
||||||
|
candidate.provider,
|
||||||
|
candidate.score * 100.0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
let entry = ReportEntry {
|
let entry = ReportEntry {
|
||||||
input: path.display().to_string(),
|
input: path.display().to_string(),
|
||||||
status: "skipped".to_string(),
|
status: "skipped".to_string(),
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ impl Report {
|
|||||||
self.entries.push(entry);
|
self.entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_summary(&self, path: Option<&Path>) -> Result<()> {
|
||||||
|
let mut writer = open_writer(path)?;
|
||||||
|
writeln!(
|
||||||
|
writer,
|
||||||
|
"Processed: {} | Renamed: {} | Skipped: {} | Failed: {}",
|
||||||
|
self.processed, self.renamed, self.skipped, self.failed
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write(&self, format: &ReportFormat, path: Option<&Path>) -> Result<()> {
|
pub fn write(&self, format: &ReportFormat, path: Option<&Path>) -> Result<()> {
|
||||||
match format {
|
match format {
|
||||||
ReportFormat::Text => self.write_text(path),
|
ReportFormat::Text => self.write_text(path),
|
||||||
|
|||||||
Reference in New Issue
Block a user