use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Error};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
pub const EQ_BANDS: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct EqBand {
pub cutoff: i32,
pub q: i32,
pub gain: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default)]
pub eq_enabled: bool,
#[serde(default = "default_eq_band_settings")]
pub eq_band_settings: Vec<EqBand>,
#[serde(default)]
pub bass: i32,
#[serde(default)]
pub treble: i32,
#[serde(default)]
pub bass_cutoff: i32,
#[serde(default)]
pub treble_cutoff: i32,
}
impl Default for Settings {
fn default() -> Self {
Self {
eq_enabled: false,
eq_band_settings: default_eq_band_settings(),
bass: 0,
treble: 0,
bass_cutoff: 0,
treble_cutoff: 0,
}
}
}
pub fn default_eq_band_settings() -> Vec<EqBand> {
const CUTOFFS_HZ: [i32; EQ_BANDS] = [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
CUTOFFS_HZ
.iter()
.map(|&hz| EqBand {
cutoff: hz,
q: 7,
gain: 0,
})
.collect()
}
impl Settings {
pub fn load() -> Self {
let path = match settings_path() {
Ok(path) => path,
Err(err) => {
eprintln!("warning: {}", err);
return Self::default();
}
};
let mut settings = match fs::read_to_string(&path) {
Ok(content) => match toml::from_str::<Settings>(&content) {
Ok(settings) => settings,
Err(err) => {
eprintln!("warning: settings file corrupted ({}), using defaults", err);
Self::default()
}
},
Err(_) => Self::default(),
};
settings.ensure_eq_bands();
settings
}
pub fn save(&self) -> Result<(), Error> {
let path = settings_path()?;
ensure_parent(&path)?;
let serialized = toml::to_string_pretty(self).context("failed to serialize settings")?;
fs::write(&path, serialized).context("failed to write settings file")
}
fn ensure_eq_bands(&mut self) {
let defaults = default_eq_band_settings();
if self.eq_band_settings.len() < EQ_BANDS {
for i in self.eq_band_settings.len()..EQ_BANDS {
self.eq_band_settings.push(defaults[i]);
}
} else {
self.eq_band_settings.truncate(EQ_BANDS);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_roundtrip_toml() {
let mut settings = Settings::default();
settings.eq_enabled = true;
settings.eq_band_settings[3].gain = -55;
settings.bass = 6;
settings.treble = -3;
let text = toml::to_string_pretty(&settings).unwrap();
let back: Settings = toml::from_str(&text).unwrap();
assert!(back.eq_enabled);
assert_eq!(back.eq_band_settings, settings.eq_band_settings);
assert_eq!(back.bass, 6);
assert_eq!(back.treble, -3);
}
#[test]
fn short_band_lists_are_padded_to_ten() {
let mut settings: Settings = toml::from_str(
"eq_enabled = true\n[[eq_band_settings]]\ncutoff = 60\nq = 7\ngain = 30\n",
)
.unwrap();
settings.ensure_eq_bands();
assert_eq!(settings.eq_band_settings.len(), EQ_BANDS);
assert_eq!(settings.eq_band_settings[0].cutoff, 60);
assert_eq!(settings.eq_band_settings[9].cutoff, 16000);
}
}
fn settings_path() -> Result<PathBuf, Error> {
let dirs = ProjectDirs::from("io", "tunein-cli", "tunein-cli")
.ok_or_else(|| Error::msg("unable to determine configuration directory"))?;
Ok(dirs.config_dir().join("settings.toml"))
}
fn ensure_parent(path: &Path) -> Result<(), Error> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("failed to create settings directory")?;
}
Ok(())
}