use std::collections::HashSet;
use std::fmt;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const CONFIG_DIR: &str = "rescrobbled";
const CONFIG_FILE: &str = "config.toml";
fn deserialize_duration_seconds<'de, D: Deserializer<'de>>(
de: D,
) -> Result<Option<Duration>, D::Error> {
Ok(Some(Duration::from_secs(u64::deserialize(de)?)))
}
fn serialize_duration_seconds<S: Serializer>(
value: &Option<Duration>,
se: S,
) -> Result<S::Ok, S::Error> {
if let Some(d) = value {
se.serialize_some(&d.as_secs())
} else {
se.serialize_none()
}
}
#[derive(Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(alias = "api-key")]
pub lastfm_key: Option<String>,
#[serde(alias = "api-secret")]
pub lastfm_secret: Option<String>,
#[serde(alias = "lb-token")]
pub listenbrainz_token: Option<String>,
pub enable_notifications: Option<bool>,
#[serde(
default,
deserialize_with = "deserialize_duration_seconds",
serialize_with = "serialize_duration_seconds"
)]
pub min_play_time: Option<Duration>,
pub player_whitelist: Option<HashSet<String>>,
pub filter_script: Option<String>,
}
impl Config {
fn template() -> String {
let template = Self {
lastfm_key: Some(String::new()),
lastfm_secret: Some(String::new()),
listenbrainz_token: Some(String::new()),
enable_notifications: Some(false),
min_play_time: Some(Duration::from_secs(0)),
player_whitelist: Some(HashSet::new()),
filter_script: Some(String::new()),
};
toml::to_string(&template)
.unwrap()
.lines()
.map(|l| format!("# {}\n", l))
.collect()
}
}
#[derive(Debug)]
pub enum ConfigError {
Io(io::Error),
Format(String),
Created(PathBuf),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "{}", err),
ConfigError::Format(msg) => write!(f, "{}", msg),
ConfigError::Created(path) => {
write!(f, "Created config file at {}", path.to_string_lossy())
}
}
}
}
pub fn config_dir() -> Result<PathBuf, ConfigError> {
let mut path = dirs::config_dir().ok_or_else(|| {
ConfigError::Io(io::Error::new(
io::ErrorKind::NotFound,
"User config directory not found",
))
})?;
path.push(CONFIG_DIR);
fs::create_dir_all(&path).map_err(ConfigError::Io)?;
Ok(path)
}
pub fn load_config() -> Result<Config, ConfigError> {
let mut path = config_dir()?;
path.push(CONFIG_FILE);
if !path.exists() {
fs::write(&path, Config::template()).map_err(ConfigError::Io)?;
return Err(ConfigError::Created(path));
}
let buffer = fs::read_to_string(&path).map_err(ConfigError::Io)?;
toml::from_str(&buffer)
.map_err(|err| ConfigError::Format(format!("Could not parse config: {}", err)))
}