pub mod encoder_detect;
pub mod types;
pub use encoder_detect::Encoder;
pub use types::*;
use crate::error::AppError;
pub use crate::i18n::Language;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::{info, warn};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub language: Language,
pub encoder: Encoder,
#[serde(default = "default_quality_preset")]
pub quality_preset: QualityPreset,
pub quality: QualityConfig,
pub performance: PerformanceConfig,
pub presets: EncodingPresetsConfig,
pub output: OutputConfig,
pub tracks: TrackPresetConfig,
#[serde(default)]
pub daemon: DaemonConfig,
}
fn default_quality_preset() -> QualityPreset {
QualityPreset::Custom
}
impl AppConfig {
pub fn load() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
match Self::load_from_file(&config_path) {
Ok(mut config) => {
config.sanitize();
info!("Loaded config from {}", config_path.display());
return config;
}
Err(e) => {
warn!("Failed to load config: {e:?}. Using defaults.");
}
}
}
let config = Self::default();
if let Err(e) = config.save() {
warn!("Failed to save default config: {e:?}");
}
config
}
pub fn save(&self) -> Result<(), AppError> {
let config_path = Self::config_path();
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| AppError::Config(format!("Failed to create config directory: {e}")))?;
}
let toml_string = toml::to_string_pretty(self)?;
std::fs::write(&config_path, toml_string)
.map_err(|e| AppError::Config(format!("Failed to write config file: {e}")))?;
info!("Saved config to {}", config_path.display());
Ok(())
}
fn load_from_file(path: &std::path::Path) -> Result<Self, AppError> {
let content = std::fs::read_to_string(path)
.map_err(|e| AppError::Config(format!("Failed to read config file: {e}")))?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
pub fn config_path() -> PathBuf {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.or_else(|| std::env::var_os("APPDATA").map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("."))
.join("av1converter")
.join("config.toml")
}
pub fn sanitize(&mut self) {
self.quality.vmaf_threshold = self.quality.vmaf_threshold.clamp(0.0, 100.0);
self.performance.svt_preset = self.performance.svt_preset.min(13);
for preset in self.presets.all_mut() {
preset.crf = preset.crf.min(Encoder::SvtAv1.max_quality());
let hw_max = Encoder::Nvenc.max_quality();
preset.nvenc_cq = preset.nvenc_cq.min(hw_max);
preset.qsv_quality = preset.qsv_quality.min(hw_max);
preset.amf_quality = preset.amf_quality.min(hw_max);
preset.film_grain = preset.film_grain.min(50);
}
if self.daemon.port == 0 {
self.daemon.port = DaemonConfig::default().port;
}
if self.daemon.bind_address.parse::<std::net::IpAddr>().is_err() {
self.daemon.bind_address = DaemonConfig::default().bind_address;
}
}
pub fn preset_for(
&self,
tier: crate::analyzer::ResolutionTier,
hdr_type: crate::analyzer::HdrType,
) -> &EncodingPreset {
use crate::analyzer::{HdrType, ResolutionTier};
match tier {
ResolutionTier::SD => &self.presets.sd,
ResolutionTier::HD => &self.presets.hd,
ResolutionTier::FullHD => match hdr_type {
HdrType::DolbyVision => &self.presets.full_hd_dv,
HdrType::Sdr => &self.presets.full_hd,
_ => &self.presets.full_hd_hdr,
},
ResolutionTier::Uhd | ResolutionTier::Above4K => match hdr_type {
HdrType::DolbyVision => &self.presets.uhd_dv,
HdrType::Sdr => &self.presets.uhd,
_ => &self.presets.uhd_hdr,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn language_defaults_and_round_trips() {
let mut cfg = AppConfig::default();
let full = toml::to_string_pretty(&cfg).unwrap();
let legacy: String = full
.lines()
.filter(|l| !l.starts_with("language"))
.collect::<Vec<_>>()
.join("\n");
let loaded: AppConfig = toml::from_str(&legacy).unwrap();
assert_eq!(loaded.language, Language::English);
cfg.language = Language::Chinese;
let s = toml::to_string_pretty(&cfg).unwrap();
assert!(s.contains("language = \"zh\""));
assert_eq!(
toml::from_str::<AppConfig>(&s).unwrap().language,
Language::Chinese
);
}
#[test]
fn daemon_defaults_on_legacy_config() {
let full = toml::to_string_pretty(&AppConfig::default()).unwrap();
let legacy = full.split("[daemon]").next().unwrap();
let loaded: AppConfig = toml::from_str(legacy).unwrap();
assert!(!loaded.daemon.enabled);
assert_eq!(loaded.daemon, DaemonConfig::default());
}
#[test]
fn daemon_sanitize_repairs_invalid_values() {
let mut cfg = AppConfig::default();
cfg.daemon.port = 0;
cfg.daemon.bind_address = "not-an-ip".to_string();
cfg.sanitize();
assert_eq!(cfg.daemon.port, 8399);
assert_eq!(cfg.daemon.bind_address, "0.0.0.0");
}
}