use anyhow::{Context, Result};
use serde::Deserialize;
use std::{
fs,
path::PathBuf,
sync::{OnceLock, RwLock},
};
use tracing::debug;
use crate::{error::LyricsError, utils::ensure_parent_dir};
static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
pub fn get_config() -> &'static RwLock<Config> {
CONFIG.get_or_init(|| RwLock::new(Config::default()))
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
pub player_filter: PlayerFilter,
pub ui: Ui,
pub sources: Sources,
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum PlayerProtocol {
#[default]
Auto,
Mpris,
Mpd,
}
#[derive(Debug, Deserialize)]
pub struct PlayerFilter {
#[serde(default)]
pub protocol: PlayerProtocol,
#[serde(default = "Vec::new")]
pub only: Vec<String>,
#[serde(default = "default_player_except")]
pub except: Vec<String>,
#[serde(default = "default_mpd_host")]
pub mpd_host: String,
#[serde(default = "default_mpd_port")]
pub mpd_port: u16,
}
fn default_player_except() -> Vec<String> {
vec![
"browser".to_string(),
"video".to_string(),
"screen-cast".to_string(),
"chromium".to_string(),
"firefox".to_string(),
]
}
fn default_mpd_host() -> String {
"127.0.0.1".to_string()
}
fn default_mpd_port() -> u16 {
6600
}
impl Default for PlayerFilter {
fn default() -> Self {
PlayerFilter {
protocol: PlayerProtocol::Mpris,
only: vec![],
except: default_player_except(),
mpd_host: default_mpd_host(),
mpd_port: default_mpd_port(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct Ui {
#[serde(default = "default_true")]
pub title: bool,
#[serde(default)]
pub time: bool,
#[serde(default = "default_true")]
pub progress_bar: bool,
#[serde(default)]
pub text_center: bool,
}
impl Default for Ui {
fn default() -> Self {
Self {
title: true,
time: false,
progress_bar: true,
text_center: false,
}
}
}
#[derive(Debug, Deserialize)]
pub struct Sources {
#[serde(default = "default_true")]
pub netease: bool,
#[serde(default = "default_true")]
pub qq: bool,
#[serde(default = "default_true")]
pub kugou: bool,
}
impl Default for Sources {
fn default() -> Self {
Sources {
netease: true,
qq: true,
kugou: true,
}
}
}
fn default_true() -> bool {
true
}
impl Config {
pub fn load_or_default(path: Option<PathBuf>) -> Result<(), LyricsError> {
let config_path = match path {
Some(p) => p,
None => config_path(),
};
let config: Config = if config_path.exists() {
let config_content = fs::read_to_string(&config_path).with_context(|| {
format!("Failed to read config file: {}", config_path.display())
})?;
toml::from_str(&config_content).with_context(|| {
format!("Failed to parse config file: {}", config_path.display())
})?
} else {
Self::default()
};
debug!("config: {:?}", config);
let mut c = get_config().write().expect("Get config failed.");
*c = config;
Ok(())
}
}
const CONFIG_PATH: &str = "lyrics";
pub fn config_path() -> PathBuf {
let config_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config")
.join(CONFIG_PATH)
.join("config.toml");
ensure_parent_dir(&config_dir);
config_dir
}
pub fn log_path() -> PathBuf {
let log_file = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".cache")
.join(CONFIG_PATH);
ensure_parent_dir(&log_file);
log_file
}
pub fn lyrics_path() -> PathBuf {
let cache_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".local")
.join("share")
.join(CONFIG_PATH);
ensure_parent_dir(&cache_dir);
cache_dir
}