Compare commits
7 Commits
main
...
13bea20035
| Author | SHA1 | Date | |
|---|---|---|---|
| 13bea20035 | |||
| 5f4ad51f63 | |||
| 88b68a0e78 | |||
| 56a1bff810 | |||
| ea7d33b525 | |||
| 47296d32c6 | |||
| ee7e765181 |
@@ -1,81 +0,0 @@
|
||||
name: Release binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Install cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
|
||||
- name: Build x86_64
|
||||
run: cargo build --release
|
||||
|
||||
- name: Build arm64
|
||||
env:
|
||||
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
||||
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR: aarch64-linux-gnu-ar
|
||||
run: cargo build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-unknown}}"
|
||||
mkdir -p dist/pkg-x86_64 dist/pkg-arm64
|
||||
|
||||
cp target/release/mov-renamarr dist/pkg-x86_64/
|
||||
cp README.md LICENSE CHANGELOG.md dist/pkg-x86_64/
|
||||
tar -C dist/pkg-x86_64 -czf "dist/mov-renamarr-${TAG}-x86_64-linux-gnu.tar.gz" .
|
||||
|
||||
cp target/aarch64-unknown-linux-gnu/release/mov-renamarr dist/pkg-arm64/
|
||||
cp README.md LICENSE CHANGELOG.md dist/pkg-arm64/
|
||||
tar -C dist/pkg-arm64 -czf "dist/mov-renamarr-${TAG}-arm64-linux-gnu.tar.gz" .
|
||||
|
||||
- name: Upload release assets
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||
echo "RELEASE_TOKEN secret is required to upload release assets."
|
||||
exit 1
|
||||
fi
|
||||
TAG="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-unknown}}"
|
||||
API_URL="${GITHUB_API_URL:-${GITEA_API_URL:-}}"
|
||||
if [ -z "$API_URL" ]; then
|
||||
API_URL="${GITHUB_SERVER_URL}/api/v1"
|
||||
fi
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
|
||||
release_json=$(curl -sS -H "Authorization: token ${RELEASE_TOKEN}" \
|
||||
"${API_URL}/repos/${REPO}/releases/tags/${TAG}")
|
||||
release_id=$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' <<<"$release_json")
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
create_payload=$(python3 -c 'import json,os; print(json.dumps({"tag_name": os.environ["TAG"], "name": os.environ["TAG"], "body": ""}))')
|
||||
release_json=$(curl -sS -H "Authorization: token ${RELEASE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$create_payload" \
|
||||
"${API_URL}/repos/${REPO}/releases")
|
||||
release_id=$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' <<<"$release_json")
|
||||
fi
|
||||
|
||||
for file in dist/*.tar.gz; do
|
||||
name=$(basename "$file")
|
||||
curl -sS -H "Authorization: token ${RELEASE_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$file" \
|
||||
"${API_URL}/repos/${REPO}/releases/${release_id}/assets?name=${name}"
|
||||
done
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,10 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0 - 2025-12-30
|
||||
Initial release.
|
||||
|
||||
- Radarr-style naming for movies
|
||||
- TMDb/OMDb providers with caching
|
||||
- Optional Ollama LLM integration
|
||||
- Safe defaults (copy, skip on collision)
|
||||
- Reports (text/json/csv) + sidecars support
|
||||
145
Cargo.lock
generated
145
Cargo.lock
generated
@@ -446,15 +446,6 @@ dependencies = [
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
@@ -727,6 +718,21 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -999,6 +1005,19 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
@@ -1323,14 +1342,12 @@ dependencies = [
|
||||
"assert_cmd",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"csv",
|
||||
"directories",
|
||||
"httpmock",
|
||||
"is-terminal",
|
||||
"libc",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"predicates",
|
||||
"rayon",
|
||||
@@ -1346,6 +1363,23 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
@@ -1389,6 +1423,50 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -1658,10 +1736,12 @@ dependencies = [
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@@ -1673,6 +1753,7 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -1776,6 +1857,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -1792,6 +1882,29 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -2106,6 +2219,16 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
|
||||
@@ -3,27 +3,20 @@ name = "mov-renamarr"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Fast, safe CLI to rename movie files into Radarr-compatible names."
|
||||
readme = "README.md"
|
||||
repository = "https://git.44r0n.cc/44r0n7/mov-renamarr.git"
|
||||
keywords = ["media", "movies", "rename", "radarr", "cli"]
|
||||
categories = ["command-line-utilities", "filesystem"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
clap_complete = "4.5"
|
||||
csv = "1.3"
|
||||
directories = "5.0"
|
||||
is-terminal = "0.4"
|
||||
libc = "0.2"
|
||||
num_cpus = "1.16"
|
||||
owo-colors = "4.1"
|
||||
once_cell = "1.19"
|
||||
rayon = "1.10"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
96
PLAN.md
Normal file
96
PLAN.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Mov Renamarr CLI - Project Plan
|
||||
|
||||
## Goal
|
||||
Build a Linux CLI that scans a directory of movie files, queries online metadata (OMDb/TMDb), and writes Radarr-compatible folder and file names to an output directory. Project name: Mov Renamarr.
|
||||
|
||||
## Core Requirements
|
||||
- Input: directory tree containing video files.
|
||||
- Output: `Movie Title (Year)/Movie Title (Year) [Quality] [id].ext`.
|
||||
- Uses `ffprobe` for media info and filename parsing for hints.
|
||||
- Queries OMDb/TMDb, with caching to avoid repeat lookups.
|
||||
- Non-interactive by default: skip ambiguous/unmatched files, report them at end.
|
||||
- Optional `--interactive` to confirm matches.
|
||||
- Linux support only (for now).
|
||||
|
||||
## Non-Goals (for MVP)
|
||||
- No Radarr API integration.
|
||||
- No TV/series handling.
|
||||
- No transcoding or media repair.
|
||||
|
||||
## Decisions to Lock In
|
||||
- Default action: copy (safe default).
|
||||
- Optional flags: `--move` and `--rename-in-place`.
|
||||
- Config file support (XDG by default) + CLI overrides. Config format: TOML.
|
||||
- Provider selection: auto based on available API keys, with optional user preference. Default auto priority: TMDb.
|
||||
- Match scoring and minimum confidence threshold.
|
||||
- Cache storage format (SQLite vs JSON) + TTL + `--refresh-cache`.
|
||||
- Quality tags default to resolution only; configurable via CLI/config.
|
||||
- Optional local LLM integration (Ollama) for filename parsing and lookup assist, disabled by default.
|
||||
- Default report format: text.
|
||||
- Sidecar notes: off by default; opt-in only.
|
||||
- Include top-3 candidates in unresolved items by default.
|
||||
- Emphasize performance, broad Linux compatibility, and robust error handling.
|
||||
- UX: per-file status line (file/provider/result/new name), progress counts, color when TTY, `--verbose` for debug details.
|
||||
- Collision policy: default skip if destination exists; optional `--overwrite` or `--suffix` to avoid data loss.
|
||||
- Sidecar files: optionally move/copy all sidecar files with `--sidecars` flag (off by default).
|
||||
- Concurrency: default jobs = min(4, max(1, floor(cores/2))); default net-jobs = min(2, jobs); allow overrides.
|
||||
- ffprobe required (no native parsing fallback).
|
||||
- Reports: stdout by default; optional report file name pattern `mov-renamarr-report-YYYYMMDD-HHMMSS.txt` when `--report` is set without a path.
|
||||
- Config precedence: defaults -> config TOML -> env -> CLI flags.
|
||||
- Config path: `$XDG_CONFIG_HOME/mov-renamarr/config.toml` (fallback `~/.config/mov-renamarr/config.toml`).
|
||||
- Cache path: `$XDG_CACHE_HOME/mov-renamarr/cache.db` (fallback `~/.cache/mov-renamarr/cache.db`).
|
||||
- Report file default location: current working directory when `--report` is set without a path.
|
||||
- Provider base URLs configurable in config/env to support testing/mocking.
|
||||
- Create a commented default config file on first run and notify the user.
|
||||
|
||||
## Proposed CLI (Draft)
|
||||
- `mov-renamarr --input <dir> --output <dir>`
|
||||
- `--config <path>` (default: XDG config)
|
||||
- `--provider auto|omdb|tmdb|both`
|
||||
- `--api-key-omdb <key>` / `--api-key-tmdb <key>` (override config/env)
|
||||
- `--cache <path>` (default: `~/.cache/mov-renamarr.db`)
|
||||
- `--refresh-cache` (bypass cache)
|
||||
- `--dry-run`
|
||||
- `--move` / `--rename-in-place`
|
||||
- `--interactive`
|
||||
- `--report <path>` + `--report-format text|json|csv`
|
||||
- `--sidecar-notes` (write per-file skip notes)
|
||||
- `--min-score <0-100>`
|
||||
- `--include-id` (tmdb/omdb/imdb if available)
|
||||
- `--quality-tags resolution|resolution,codec,source`
|
||||
- `--color auto|always|never`
|
||||
- `--jobs <n|auto>`
|
||||
- `--net-jobs <n|auto>`
|
||||
- `--no-lookup` (skip external providers; use filename/LLM only)
|
||||
- `--llm-mode off|parse|assist` (default: off)
|
||||
- `--llm-endpoint <url>` (Ollama, default `http://localhost:11434`)
|
||||
- `--llm-model <name>` (Ollama model name)
|
||||
- `--llm-timeout <seconds>` / `--llm-max-tokens <n>`
|
||||
|
||||
## Matching Heuristics (Draft)
|
||||
- Parse filename for title/year hints; strip extra release metadata.
|
||||
- Use `ffprobe` for duration and resolution.
|
||||
- Prefer exact year match; allow +/- 1 year when missing.
|
||||
- Use string similarity + runtime delta to choose best match.
|
||||
|
||||
## Pipeline (Draft)
|
||||
1. Load config (XDG) + merge CLI overrides.
|
||||
2. Discover files and filter by extension; skip output subtree when output != input to avoid reprocessing.
|
||||
3. Parse filename hints (title/year) and strip release metadata (optionally via LLM parse).
|
||||
4. Run `ffprobe` for duration/resolution/codec.
|
||||
5. Select provider(s) based on available API keys and user preference.
|
||||
6. Query provider(s) with hints (LLM assist may propose candidates but must be verified).
|
||||
7. Score and select match; if below threshold, mark as unresolved.
|
||||
8. Build Radarr-compatible output path.
|
||||
9. Copy/move/rename-in-place file to output directory.
|
||||
10. Write summary report of successes and unresolved items.
|
||||
|
||||
## Milestones
|
||||
- M0: Project scaffold and plan (done).
|
||||
- M1: CLI skeleton and config parsing.
|
||||
- M2: `ffprobe` integration and media metadata model.
|
||||
- M3: OMDb/TMDb client + caching.
|
||||
- M4: Matching, naming, and file move/copy.
|
||||
- M5: Reporting, tests, and polish.
|
||||
- M6: Automated test harness and fixtures.
|
||||
- M7: Performance pass and profiling.
|
||||
22
README.md
22
README.md
@@ -31,8 +31,6 @@ cargo install --git <repo-url> --locked
|
||||
cargo install --path . --locked
|
||||
```
|
||||
|
||||
Download a prebuilt binary from the Releases page.
|
||||
|
||||
## Quick start :rocket:
|
||||
Create a default config (with comments) and see the config path:
|
||||
```bash
|
||||
@@ -63,26 +61,6 @@ Common flags:
|
||||
- `--quality-tags resolution,codec,source`
|
||||
- `--min-score 0-100` (match threshold)
|
||||
- `--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:
|
||||
Default config location:
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
#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
|
||||
@@ -1,126 +0,0 @@
|
||||
_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
|
||||
@@ -1,48 +0,0 @@
|
||||
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,7 +43,6 @@ refresh_cache = false
|
||||
report_format = "text" # text|json|csv
|
||||
sidecar_notes = false # write per-file notes for skipped/failed
|
||||
sidecars = false # move/copy sidecar files (srt, nfo, etc)
|
||||
dry_run_summary = false # suppress per-file output (use with --dry-run)
|
||||
```
|
||||
|
||||
## Matching and naming
|
||||
@@ -51,7 +50,6 @@ dry_run_summary = false # suppress per-file output (use with --dry-run)
|
||||
min_score = 80 # 0-100 match threshold
|
||||
include_id = false # include tmdb/imdb id in filenames
|
||||
no_lookup = false # skip external providers (filename/LLM only)
|
||||
explain = false # show top candidates when skipped
|
||||
```
|
||||
|
||||
## Quality tags
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Releasing
|
||||
|
||||
This repo includes a Gitea Actions workflow that builds Linux binaries for
|
||||
`x86_64` and `arm64` and uploads them to the release.
|
||||
|
||||
## One-time setup (Gitea Actions)
|
||||
1) Create a personal access token with repo write access.
|
||||
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
|
||||
1) Update `CHANGELOG.md`.
|
||||
2) Create and push a tag:
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "vX.Y.Z"
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
3) The workflow builds and uploads binaries to the release.
|
||||
|
||||
## Artifacts
|
||||
- `mov-renamarr-<tag>-x86_64-linux-gnu.tar.gz`
|
||||
- `mov-renamarr-<tag>-arm64-linux-gnu.tar.gz`
|
||||
33
src/cli.rs
33
src/cli.rs
@@ -2,13 +2,13 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "mov-renamarr", version, about = "Rename movie files into Radarr-compatible naming")]
|
||||
pub struct Cli {
|
||||
#[arg(long, value_name = "DIR", required_unless_present_any = ["completions"])]
|
||||
pub input: Option<PathBuf>,
|
||||
#[arg(long, value_name = "DIR")]
|
||||
pub input: PathBuf,
|
||||
|
||||
#[arg(long, value_name = "DIR")]
|
||||
pub output: Option<PathBuf>,
|
||||
@@ -34,9 +34,6 @@ pub struct Cli {
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
|
||||
#[arg(long)]
|
||||
pub dry_run_summary: bool,
|
||||
|
||||
#[arg(long = "move", conflicts_with = "rename_in_place")]
|
||||
pub move_files: bool,
|
||||
|
||||
@@ -105,20 +102,11 @@ pub struct Cli {
|
||||
#[arg(long, alias = "offline")]
|
||||
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)]
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProviderChoice {
|
||||
Auto,
|
||||
@@ -127,7 +115,7 @@ pub enum ProviderChoice {
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ReportFormat {
|
||||
Text,
|
||||
@@ -135,7 +123,7 @@ pub enum ReportFormat {
|
||||
Csv,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ColorMode {
|
||||
Auto,
|
||||
@@ -143,7 +131,7 @@ pub enum ColorMode {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LlmMode {
|
||||
Off,
|
||||
@@ -151,13 +139,6 @@ pub enum LlmMode {
|
||||
Assist,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)]
|
||||
pub enum CompletionShell {
|
||||
Bash,
|
||||
Zsh,
|
||||
Fish,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum JobsArg {
|
||||
Auto,
|
||||
|
||||
162
src/config.rs
162
src/config.rs
@@ -5,7 +5,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use directories::BaseDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::cli::{Cli, ColorMode, JobsArg, LlmMode, ProviderChoice, ReportFormat};
|
||||
|
||||
@@ -34,12 +34,10 @@ pub struct Settings {
|
||||
pub net_jobs: usize,
|
||||
pub no_lookup: bool,
|
||||
pub dry_run: bool,
|
||||
pub dry_run_summary: bool,
|
||||
pub move_files: bool,
|
||||
pub rename_in_place: bool,
|
||||
pub interactive: bool,
|
||||
pub verbose: bool,
|
||||
pub explain: bool,
|
||||
pub omdb_base_url: String,
|
||||
pub tmdb_base_url: String,
|
||||
}
|
||||
@@ -61,25 +59,6 @@ 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)]
|
||||
pub struct LlmSettings {
|
||||
pub mode: LlmMode,
|
||||
@@ -89,50 +68,6 @@ pub struct LlmSettings {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -168,8 +103,6 @@ struct FileConfig {
|
||||
omdb_base_url: Option<String>,
|
||||
tmdb_base_url: Option<String>,
|
||||
no_lookup: Option<bool>,
|
||||
dry_run_summary: Option<bool>,
|
||||
explain: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
@@ -206,11 +139,8 @@ pub fn build_settings(cli: &Cli) -> Result<Settings> {
|
||||
}
|
||||
let file_config = load_config_file(&config_path)?;
|
||||
|
||||
let input = cli
|
||||
.input
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("--input is required unless --completions is used"))?;
|
||||
let output = resolve_output(cli, &input)?;
|
||||
let input = cli.input.clone();
|
||||
let output = resolve_output(cli)?;
|
||||
|
||||
let mut settings = Settings {
|
||||
input,
|
||||
@@ -236,12 +166,10 @@ pub fn build_settings(cli: &Cli) -> Result<Settings> {
|
||||
net_jobs: default_net_jobs(default_jobs()),
|
||||
no_lookup: false,
|
||||
dry_run: cli.dry_run,
|
||||
dry_run_summary: false,
|
||||
move_files: cli.move_files,
|
||||
rename_in_place: cli.rename_in_place,
|
||||
interactive: cli.interactive,
|
||||
verbose: cli.verbose,
|
||||
explain: false,
|
||||
omdb_base_url: "https://www.omdbapi.com".to_string(),
|
||||
tmdb_base_url: "https://api.themoviedb.org/3".to_string(),
|
||||
};
|
||||
@@ -261,11 +189,11 @@ pub fn init_default_config() -> Result<PathBuf> {
|
||||
Ok(config_path)
|
||||
}
|
||||
|
||||
fn resolve_output(cli: &Cli, input: &Path) -> Result<PathBuf> {
|
||||
fn resolve_output(cli: &Cli) -> Result<PathBuf> {
|
||||
match (cli.rename_in_place, cli.output.as_ref()) {
|
||||
(true, None) => Ok(input.to_path_buf()),
|
||||
(true, None) => Ok(cli.input.clone()),
|
||||
(true, Some(out)) => {
|
||||
if out != input {
|
||||
if out != &cli.input {
|
||||
Err(anyhow!(
|
||||
"--rename-in-place requires output to be omitted or the same as input"
|
||||
))
|
||||
@@ -274,7 +202,7 @@ fn resolve_output(cli: &Cli, input: &Path) -> Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
(false, Some(out)) => {
|
||||
if out == input {
|
||||
if out == &cli.input {
|
||||
Err(anyhow!(
|
||||
"output directory must be different from input unless --rename-in-place is set"
|
||||
))
|
||||
@@ -359,16 +287,12 @@ fn default_config_template() -> String {
|
||||
"sidecar_notes = false",
|
||||
"# sidecars copies/moves subtitle/nfo/etc files with the movie file.",
|
||||
"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 = false",
|
||||
"suffix = false",
|
||||
"# Disable external lookups (use filename/LLM only).",
|
||||
"# When true, provider selection is ignored.",
|
||||
"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 = 80",
|
||||
"# include_id adds tmdb-XXXX or imdb-ttXXXX in the filename.",
|
||||
@@ -438,9 +362,6 @@ fn apply_file_config(settings: &mut Settings, file: &FileConfig) -> Result<()> {
|
||||
if let Some(sidecars) = file.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 {
|
||||
settings.overwrite = overwrite;
|
||||
}
|
||||
@@ -472,9 +393,6 @@ fn apply_file_config(settings: &mut Settings, file: &FileConfig) -> Result<()> {
|
||||
if let Some(no_lookup) = file.no_lookup {
|
||||
settings.no_lookup = no_lookup;
|
||||
}
|
||||
if let Some(explain) = file.explain {
|
||||
settings.explain = explain;
|
||||
}
|
||||
if let Some(llm) = &file.llm {
|
||||
apply_file_llm(settings, llm);
|
||||
}
|
||||
@@ -551,11 +469,9 @@ 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_SIDECARS", |value| settings.sidecars = 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_SUFFIX", |value| settings.suffix = 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| {
|
||||
if let Ok(tags) = parse_quality_tags(&split_list(&value)) {
|
||||
@@ -645,9 +561,6 @@ fn apply_cli_overrides(settings: &mut Settings, cli: &Cli) -> Result<()> {
|
||||
if cli.sidecars {
|
||||
settings.sidecars = true;
|
||||
}
|
||||
if cli.dry_run_summary {
|
||||
settings.dry_run_summary = true;
|
||||
}
|
||||
if cli.overwrite {
|
||||
settings.overwrite = true;
|
||||
}
|
||||
@@ -675,9 +588,6 @@ fn apply_cli_overrides(settings: &mut Settings, cli: &Cli) -> Result<()> {
|
||||
if cli.no_lookup {
|
||||
settings.no_lookup = true;
|
||||
}
|
||||
if cli.explain {
|
||||
settings.explain = true;
|
||||
}
|
||||
if let Some(mode) = &cli.llm_mode {
|
||||
settings.llm.mode = mode.clone();
|
||||
}
|
||||
@@ -706,9 +616,6 @@ fn validate_settings(settings: &mut Settings) -> Result<()> {
|
||||
if settings.min_score > 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 {
|
||||
settings.net_jobs = 1;
|
||||
}
|
||||
@@ -729,61 +636,6 @@ pub fn default_net_jobs(jobs: usize) -> usize {
|
||||
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> {
|
||||
if raw.eq_ignore_ascii_case("auto") {
|
||||
return Ok(fallback);
|
||||
|
||||
33
src/main.rs
33
src/main.rs
@@ -11,10 +11,9 @@ mod report;
|
||||
mod utils;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::{generate, Shell};
|
||||
use clap::Parser;
|
||||
|
||||
use crate::cli::{Cli, CompletionShell};
|
||||
use crate::cli::Cli;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if std::env::args_os().len() == 1 {
|
||||
@@ -24,38 +23,12 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
if cli.print_config {
|
||||
let rendered = config::format_settings(&settings)?;
|
||||
println!("{rendered}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let report_format = settings.report_format.clone();
|
||||
let report_path = settings.report_path.clone();
|
||||
let dry_run_summary = settings.dry_run_summary;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -16,12 +16,11 @@ pub enum StatusKind {
|
||||
pub struct Output {
|
||||
use_color: bool,
|
||||
verbose: bool,
|
||||
summary_only: bool,
|
||||
lock: Mutex<()>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn new(color_mode: &ColorMode, verbose: bool, summary_only: bool) -> Self {
|
||||
pub fn new(color_mode: &ColorMode, verbose: bool) -> Self {
|
||||
let use_color = match color_mode {
|
||||
ColorMode::Always => true,
|
||||
ColorMode::Never => false,
|
||||
@@ -30,7 +29,6 @@ impl Output {
|
||||
Self {
|
||||
use_color,
|
||||
verbose,
|
||||
summary_only,
|
||||
lock: Mutex::new(()),
|
||||
}
|
||||
}
|
||||
@@ -45,9 +43,6 @@ impl Output {
|
||||
result: &str,
|
||||
output_name: Option<&str>,
|
||||
) {
|
||||
if self.summary_only && !matches!(status, StatusKind::Failed) {
|
||||
return;
|
||||
}
|
||||
let _guard = self.lock.lock().unwrap();
|
||||
let prefix = format!("[{}/{}]", index, total);
|
||||
let status_label = match status {
|
||||
@@ -79,11 +74,6 @@ impl Output {
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
|
||||
pub fn detail(&self, message: &str) {
|
||||
let _guard = self.lock.lock().unwrap();
|
||||
println!("{message}");
|
||||
}
|
||||
|
||||
|
||||
pub fn info(&self, message: &str) {
|
||||
if self.verbose {
|
||||
|
||||
136
src/parse.rs
136
src/parse.rs
@@ -1,7 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::utils::{collapse_whitespace, normalize_title};
|
||||
@@ -14,24 +12,6 @@ pub struct FileHints {
|
||||
pub alt_titles: Vec<String>,
|
||||
}
|
||||
|
||||
static YEAR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(19|20)\d{2}").expect("year regex"));
|
||||
static BRACKET_SQUARE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\[[^\]]*\]").expect("square bracket regex"));
|
||||
static BRACKET_ROUND_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\([^\)]*\)").expect("round bracket regex"));
|
||||
static STOPWORDS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
[
|
||||
"1080p", "720p", "2160p", "480p", "360p", "4k", "uhd", "hdr", "dvdrip",
|
||||
"bdrip", "brrip", "bluray", "blu", "webdl", "web-dl", "webrip", "hdrip",
|
||||
"remux", "x264", "x265", "h264", "h265", "hevc", "aac", "dts", "ac3",
|
||||
"proper", "repack", "limited", "extended", "uncut", "remastered", "subbed",
|
||||
"subs", "multi", "dubbed", "dub", "yts", "yify", "rarbg", "web", "hd",
|
||||
"hq", "cam", "ts", "dvdscr", "r5", "r6",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
|
||||
pub fn parse_filename(path: &Path) -> FileHints {
|
||||
let stem = path
|
||||
.file_stem()
|
||||
@@ -40,9 +20,8 @@ pub fn parse_filename(path: &Path) -> FileHints {
|
||||
|
||||
let year = extract_year(&stem);
|
||||
let cleaned = strip_bracketed(&stem);
|
||||
let cleaned_for_tokens = strip_dash_suffix(&cleaned);
|
||||
let alt_titles = extract_alt_titles(&cleaned, year);
|
||||
let tokens = strip_noise_tokens(tokenize(&cleaned_for_tokens, year));
|
||||
let tokens = tokenize(&cleaned, year);
|
||||
|
||||
let title = if tokens.is_empty() {
|
||||
let mut fallback = cleaned.clone();
|
||||
@@ -65,8 +44,9 @@ pub fn parse_filename(path: &Path) -> FileHints {
|
||||
}
|
||||
|
||||
fn extract_year(raw: &str) -> Option<i32> {
|
||||
let re = Regex::new(r"(19|20)\d{2}").ok()?;
|
||||
let mut year: Option<i32> = None;
|
||||
for mat in YEAR_RE.find_iter(raw) {
|
||||
for mat in re.find_iter(raw) {
|
||||
if let Ok(parsed) = mat.as_str().parse::<i32>() {
|
||||
year = Some(parsed);
|
||||
}
|
||||
@@ -75,37 +55,13 @@ fn extract_year(raw: &str) -> Option<i32> {
|
||||
}
|
||||
|
||||
fn strip_bracketed(raw: &str) -> String {
|
||||
let without_square = BRACKET_SQUARE_RE.replace_all(raw, " ");
|
||||
let without_round = BRACKET_ROUND_RE.replace_all(&without_square, " ");
|
||||
let re_square = Regex::new(r"\[[^\]]*\]").unwrap();
|
||||
let re_round = Regex::new(r"\([^\)]*\)").unwrap();
|
||||
let without_square = re_square.replace_all(raw, " ");
|
||||
let without_round = re_round.replace_all(&without_square, " ");
|
||||
without_round.to_string()
|
||||
}
|
||||
|
||||
fn strip_dash_suffix(raw: &str) -> String {
|
||||
let Some((left, right)) = raw.split_once(" - ") else {
|
||||
return raw.to_string();
|
||||
};
|
||||
if should_strip_dash_suffix(right) {
|
||||
left.trim().to_string()
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn should_strip_dash_suffix(right: &str) -> bool {
|
||||
let trimmed = right.trim();
|
||||
if trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let lower = trimmed.to_ascii_lowercase();
|
||||
if lower.contains("http://") || lower.contains("https://") || lower.contains("www.") {
|
||||
return true;
|
||||
}
|
||||
if trimmed.contains('.') && !trimmed.contains(' ') {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn extract_alt_titles(raw: &str, year: Option<i32>) -> Vec<String> {
|
||||
let mut alt_titles = Vec::new();
|
||||
if let Some((left, right)) = raw.split_once(" - ") {
|
||||
@@ -127,6 +83,7 @@ fn clean_title_fragment(fragment: &str, year: Option<i32>) -> String {
|
||||
}
|
||||
|
||||
fn tokenize(raw: &str, year: Option<i32>) -> Vec<String> {
|
||||
let stopwords = stopwords();
|
||||
let mut tokens = Vec::new();
|
||||
for token in raw.split(|c: char| !c.is_alphanumeric()) {
|
||||
if token.is_empty() {
|
||||
@@ -138,7 +95,7 @@ fn tokenize(raw: &str, year: Option<i32>) -> Vec<String> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if STOPWORDS.contains(lower.as_str()) {
|
||||
if stopwords.contains(lower.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if token.chars().all(|c| c.is_ascii_uppercase()) && token.len() <= 8 {
|
||||
@@ -149,35 +106,17 @@ fn tokenize(raw: &str, year: Option<i32>) -> Vec<String> {
|
||||
tokens
|
||||
}
|
||||
|
||||
fn strip_noise_tokens(mut tokens: Vec<String>) -> Vec<String> {
|
||||
if tokens.is_empty() {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
if let Some(first) = tokens.first() {
|
||||
let lower = first.to_ascii_lowercase();
|
||||
if matches!(lower.as_str(), "watch" | "download") {
|
||||
tokens.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
if tokens.len() >= 2 {
|
||||
let last = tokens[tokens.len() - 1].to_ascii_lowercase();
|
||||
let prev = tokens[tokens.len() - 2].to_ascii_lowercase();
|
||||
if prev == "for" && last == "free" {
|
||||
tokens.pop();
|
||||
tokens.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last) = tokens.last() {
|
||||
let lower = last.to_ascii_lowercase();
|
||||
if matches!(lower.as_str(), "online" | "free" | "download") {
|
||||
tokens.pop();
|
||||
}
|
||||
}
|
||||
|
||||
tokens
|
||||
fn stopwords() -> std::collections::HashSet<&'static str> {
|
||||
[
|
||||
"1080p", "720p", "2160p", "480p", "360p", "4k", "uhd", "hdr", "dvdrip",
|
||||
"bdrip", "brrip", "bluray", "blu", "webdl", "web-dl", "webrip", "hdrip",
|
||||
"remux", "x264", "x265", "h264", "h265", "hevc", "aac", "dts", "ac3",
|
||||
"proper", "repack", "limited", "extended", "uncut", "remastered", "subbed",
|
||||
"subs", "multi", "dubbed", "dub", "yts", "yify", "rarbg", "web", "hd",
|
||||
"hq", "cam", "ts", "dvdscr", "r5", "r6",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -208,39 +147,4 @@ mod tests {
|
||||
assert_eq!(hints.title.as_deref(), Some("Zootopia Vlix"));
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_watch_for_free_domain_suffix() {
|
||||
let path = Path::new("Watch VideoName 2025 HD for free - website.tld.mp4");
|
||||
let hints = parse_filename(path);
|
||||
assert_eq!(hints.title.as_deref(), Some("VideoName"));
|
||||
assert_eq!(hints.year, Some(2025));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,7 @@ use crate::utils::{sanitize_filename, Semaphore};
|
||||
pub fn run(mut settings: Settings) -> Result<Report> {
|
||||
ensure_ffprobe()?;
|
||||
|
||||
let output = Arc::new(Output::new(
|
||||
&settings.color,
|
||||
settings.verbose,
|
||||
settings.dry_run_summary,
|
||||
));
|
||||
let output = Arc::new(Output::new(&settings.color, settings.verbose));
|
||||
if settings.no_lookup {
|
||||
output.warn("No-lookup mode enabled: using filename/LLM only (no external providers).");
|
||||
}
|
||||
@@ -249,18 +245,6 @@ fn process_file(
|
||||
"no match",
|
||||
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 {
|
||||
input: path.display().to_string(),
|
||||
status: "skipped".to_string(),
|
||||
|
||||
@@ -48,16 +48,6 @@ impl Report {
|
||||
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<()> {
|
||||
match format {
|
||||
ReportFormat::Text => self.write_text(path),
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::path::Path;
|
||||
use assert_cmd::Command;
|
||||
use httpmock::Method::{GET, POST};
|
||||
use httpmock::MockServer;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -297,117 +296,3 @@ fn rename_in_place_uses_input_as_output() {
|
||||
assert!(renamed.exists());
|
||||
assert!(!input.join("Alien.1979.1080p.mkv").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completions_generate_output() {
|
||||
let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr"));
|
||||
cmd.arg("--completions").arg("bash");
|
||||
|
||||
cmd.assert().success().stdout(contains("mov-renamarr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_config_outputs_toml() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let input = temp.path().join("input");
|
||||
let output = temp.path().join("output");
|
||||
fs::create_dir_all(&input).unwrap();
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
|
||||
let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr"));
|
||||
cmd.arg("--input").arg(&input)
|
||||
.arg("--output").arg(&output)
|
||||
.arg("--print-config")
|
||||
.env("XDG_CONFIG_HOME", temp.path().join("config"))
|
||||
.env("XDG_CACHE_HOME", temp.path().join("cache"));
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(contains("provider = \"auto\""))
|
||||
.stdout(contains("cache_ttl_days"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_summary_suppresses_per_file_output() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let input = temp.path().join("input");
|
||||
let output = temp.path().join("output");
|
||||
fs::create_dir_all(&input).unwrap();
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
fs::write(input.join("Film.2020.mkv"), b"stub").unwrap();
|
||||
|
||||
let ffprobe = make_ffprobe_stub(temp.path());
|
||||
|
||||
let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr"));
|
||||
cmd.arg("--input").arg(&input)
|
||||
.arg("--output").arg(&output)
|
||||
.arg("--dry-run")
|
||||
.arg("--dry-run-summary")
|
||||
.arg("--no-lookup")
|
||||
.arg("--color").arg("never")
|
||||
.env("XDG_CONFIG_HOME", temp.path().join("config"))
|
||||
.env("XDG_CACHE_HOME", temp.path().join("cache"))
|
||||
.env("PATH", prepend_path(ffprobe.parent().unwrap()));
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(contains("Processed: 1"))
|
||||
.stdout(predicates::str::contains("[1/1]").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explain_prints_candidates_on_skip() {
|
||||
let server = MockServer::start();
|
||||
|
||||
let search_mock = server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path("/search/movie")
|
||||
.query_param("api_key", "test")
|
||||
.query_param("query", "Zootopia")
|
||||
.query_param("year", "2016");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(r#"{"results":[{"id":55,"title":"Totally Different","release_date":"1990-01-01"}]}"#);
|
||||
});
|
||||
|
||||
let details_mock = server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path("/movie/55")
|
||||
.query_param("api_key", "test");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(r#"{"id":55,"title":"Totally Different","release_date":"1990-01-01","runtime":120,"imdb_id":"tt055"}"#);
|
||||
});
|
||||
|
||||
let temp = TempDir::new().unwrap();
|
||||
let input = temp.path().join("input");
|
||||
let output = temp.path().join("output");
|
||||
fs::create_dir_all(&input).unwrap();
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
fs::write(input.join("Zootopia.2016.mkv"), b"stub").unwrap();
|
||||
|
||||
let ffprobe = make_ffprobe_stub(temp.path());
|
||||
|
||||
let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr"));
|
||||
cmd.arg("--input").arg(&input)
|
||||
.arg("--output").arg(&output)
|
||||
.arg("--dry-run")
|
||||
.arg("--provider").arg("tmdb")
|
||||
.arg("--min-score").arg("99")
|
||||
.arg("--explain")
|
||||
.arg("--color").arg("never")
|
||||
.env("MOV_RENAMARR_TMDB_API_KEY", "test")
|
||||
.env("MOV_RENAMARR_TMDB_BASE_URL", server.url(""))
|
||||
.env("XDG_CONFIG_HOME", temp.path().join("config"))
|
||||
.env("XDG_CACHE_HOME", temp.path().join("cache"))
|
||||
.env("PATH", prepend_path(ffprobe.parent().unwrap()));
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(contains("skipped"))
|
||||
.stdout(contains("Candidates:"))
|
||||
.stdout(contains("Totally Different"));
|
||||
|
||||
search_mock.assert_hits(1);
|
||||
details_mock.assert_hits(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user