Add config printing, explain mode, completions, and docs
This commit is contained in:
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;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cli::{Cli, ColorMode, JobsArg, LlmMode, ProviderChoice, ReportFormat};
|
||||
|
||||
@@ -34,10 +34,12 @@ 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,
|
||||
}
|
||||
@@ -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)]
|
||||
pub struct LlmSettings {
|
||||
pub mode: LlmMode,
|
||||
@@ -68,6 +89,50 @@ 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 {
|
||||
@@ -103,6 +168,8 @@ 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)]
|
||||
@@ -139,8 +206,11 @@ pub fn build_settings(cli: &Cli) -> Result<Settings> {
|
||||
}
|
||||
let file_config = load_config_file(&config_path)?;
|
||||
|
||||
let input = cli.input.clone();
|
||||
let output = resolve_output(cli)?;
|
||||
let input = cli
|
||||
.input
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("--input is required unless --completions is used"))?;
|
||||
let output = resolve_output(cli, &input)?;
|
||||
|
||||
let mut settings = Settings {
|
||||
input,
|
||||
@@ -166,10 +236,12 @@ 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(),
|
||||
};
|
||||
@@ -189,11 +261,11 @@ pub fn init_default_config() -> Result<PathBuf> {
|
||||
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()) {
|
||||
(true, None) => Ok(cli.input.clone()),
|
||||
(true, None) => Ok(input.to_path_buf()),
|
||||
(true, Some(out)) => {
|
||||
if out != &cli.input {
|
||||
if out != input {
|
||||
Err(anyhow!(
|
||||
"--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)) => {
|
||||
if out == &cli.input {
|
||||
if out == input {
|
||||
Err(anyhow!(
|
||||
"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",
|
||||
"# 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.",
|
||||
@@ -362,6 +438,9 @@ 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;
|
||||
}
|
||||
@@ -393,6 +472,9 @@ 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);
|
||||
}
|
||||
@@ -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_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)) {
|
||||
@@ -561,6 +645,9 @@ 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;
|
||||
}
|
||||
@@ -588,6 +675,9 @@ 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();
|
||||
}
|
||||
@@ -616,6 +706,9 @@ 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;
|
||||
}
|
||||
@@ -636,6 +729,61 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user