1use anyhow::{Context, Result};
2use serde::Deserialize;
3use std::{
4 fs,
5 path::PathBuf,
6 sync::{OnceLock, RwLock},
7};
8use tracing::debug;
9
10use crate::{error::LyricsError, utils::ensure_parent_dir};
11
12static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
14
15pub fn get_config() -> &'static RwLock<Config> {
16 CONFIG.get_or_init(|| RwLock::new(Config::default()))
17}
18
19#[derive(Debug, Deserialize, Default)]
20#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
21pub struct Config {
22 pub player_filter: PlayerFilter,
23 pub ui: Ui,
24 pub sources: Sources,
25}
26
27#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
28#[serde(rename_all = "lowercase")]
29pub enum PlayerProtocol {
30 #[default]
31 Auto,
32 Mpris,
33 Mpd,
34}
35
36#[derive(Debug, Deserialize)]
37pub struct PlayerFilter {
38 #[serde(default)]
39 pub protocol: PlayerProtocol,
40 #[serde(default = "Vec::new")]
41 pub only: Vec<String>,
42 #[serde(default = "default_player_except")]
43 pub except: Vec<String>,
44 #[serde(default = "default_mpd_host")]
45 pub mpd_host: String,
46 #[serde(default = "default_mpd_port")]
47 pub mpd_port: u16,
48}
49
50fn default_player_except() -> Vec<String> {
51 vec![
52 "browser".to_string(),
53 "video".to_string(),
54 "screen-cast".to_string(),
55 "chromium".to_string(),
56 "firefox".to_string(),
57 ]
58}
59
60fn default_mpd_host() -> String {
61 "127.0.0.1".to_string()
62}
63
64fn default_mpd_port() -> u16 {
65 6600
66}
67
68impl Default for PlayerFilter {
69 fn default() -> Self {
70 PlayerFilter {
71 protocol: PlayerProtocol::Mpris,
72 only: vec![],
73 except: default_player_except(),
74 mpd_host: default_mpd_host(),
75 mpd_port: default_mpd_port(),
76 }
77 }
78}
79
80#[derive(Debug, Deserialize)]
81pub struct Ui {
82 #[serde(default = "default_true")]
83 pub title: bool,
84 #[serde(default)]
85 pub time: bool,
86 #[serde(default = "default_true")]
87 pub progress_bar: bool,
88 #[serde(default)]
89 pub text_center: bool,
90}
91
92impl Default for Ui {
93 fn default() -> Self {
94 Self {
95 title: true,
96 time: false,
97 progress_bar: true,
98 text_center: false,
99 }
100 }
101}
102
103#[derive(Debug, Deserialize)]
104pub struct Sources {
105 #[serde(default = "default_true")]
106 pub netease: bool,
107 #[serde(default = "default_true")]
108 pub qq: bool,
109 #[serde(default = "default_true")]
110 pub kugou: bool,
111}
112
113impl Default for Sources {
114 fn default() -> Self {
115 Sources {
116 netease: true,
117 qq: true,
118 kugou: true,
119 }
120 }
121}
122
123fn default_true() -> bool {
124 true
125}
126
127impl Config {
128 pub fn load_or_default(path: Option<PathBuf>) -> Result<(), LyricsError> {
129 let config_path = match path {
130 Some(p) => p,
131 None => config_path(),
132 };
133
134 let config: Config = if config_path.exists() {
135 let config_content = fs::read_to_string(&config_path).with_context(|| {
136 format!("Failed to read config file: {}", config_path.display())
137 })?;
138
139 toml::from_str(&config_content).with_context(|| {
140 format!("Failed to parse config file: {}", config_path.display())
141 })?
142 } else {
143 Self::default()
144 };
145
146 debug!("config: {:?}", config);
147
148 let mut c = get_config().write().expect("Get config failed.");
149 *c = config;
150
151 Ok(())
152 }
153}
154
155const CONFIG_PATH: &str = "lyrics";
156
157pub fn config_path() -> PathBuf {
158 let config_dir = dirs::home_dir()
159 .unwrap_or_else(|| PathBuf::from("."))
160 .join(".config")
161 .join(CONFIG_PATH)
162 .join("config.toml");
163 ensure_parent_dir(&config_dir);
164 config_dir
165}
166
167pub fn log_path() -> PathBuf {
168 let log_file = dirs::home_dir()
169 .unwrap_or_else(|| PathBuf::from("."))
170 .join(".cache")
171 .join(CONFIG_PATH);
172 ensure_parent_dir(&log_file);
174 log_file
175}
176
177pub fn lyrics_path() -> PathBuf {
178 let cache_dir = dirs::home_dir()
179 .unwrap_or_else(|| PathBuf::from("."))
180 .join(".local")
181 .join("share")
182 .join(CONFIG_PATH);
183 ensure_parent_dir(&cache_dir);
184 cache_dir
185}