diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-02 13:22:44 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-02 13:22:44 +0100 |
| commit | 2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94 (patch) | |
| tree | 24a7a55a70b2235d115d9861d24385a61725e7b8 | |
| parent | seek: jump within the current track from the CLI and over MPRIS (diff) | |
| download | rsplayer-2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94.tar.gz rsplayer-2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94.zip | |
config: parse the config once and reuse it until the file changes
Every command, status line and notification went through Config::load, which
reads *and* rewrites config.toml, so `rsp status` alone touched the file on
each call.
The daemon now keeps a parsed copy and only stats the file to notice outside
edits, so `rsp settings set` still takes effect immediately. Config::save
refreshes the cached copy too, and Config::config_path is shared by the loader
and the cache.
| -rw-r--r-- | src/config.rs | 107 | ||||
| -rw-r--r-- | src/daemon.rs | 54 | ||||
| -rw-r--r-- | src/mpris.rs | 12 |
3 files changed, 124 insertions, 49 deletions
diff --git a/src/config.rs b/src/config.rs index f20fb3a..a08f01f 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -9,8 +9,10 @@ use directories::ProjectDirs; | |||
| 9 | use serde::{Deserialize, Serialize}; | 9 | use serde::{Deserialize, Serialize}; |
| 10 | use std::fs; | 10 | use std::fs; |
| 11 | use std::path::PathBuf; | 11 | use std::path::PathBuf; |
| 12 | use std::sync::{LazyLock, RwLock}; | ||
| 13 | use std::time::SystemTime; | ||
| 12 | 14 | ||
| 13 | #[derive(Debug, Serialize, Deserialize, Default)] | 15 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] |
| 14 | pub struct Config { | 16 | pub struct Config { |
| 15 | #[serde(skip)] | 17 | #[serde(skip)] |
| 16 | pub config_path: PathBuf, | 18 | pub config_path: PathBuf, |
| @@ -25,24 +27,29 @@ pub struct Config { | |||
| 25 | pub display_format: Option<String>, | 27 | pub display_format: Option<String>, |
| 26 | } | 28 | } |
| 27 | 29 | ||
| 30 | /// Identity of the config file on disk, used to tell whether a cached copy is | ||
| 31 | /// still current. `modified` alone is not enough: several filesystems only | ||
| 32 | /// store whole seconds, so a quick edit could keep the same timestamp. | ||
| 33 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 34 | struct FileStamp { | ||
| 35 | modified: Option<SystemTime>, | ||
| 36 | len: u64, | ||
| 37 | } | ||
| 38 | |||
| 39 | struct CachedConfig { | ||
| 40 | config: Config, | ||
| 41 | stamp: FileStamp, | ||
| 42 | } | ||
| 43 | |||
| 44 | static CACHE: LazyLock<RwLock<Option<CachedConfig>>> = LazyLock::new(|| RwLock::new(None)); | ||
| 45 | |||
| 28 | impl Config { | 46 | impl Config { |
| 29 | pub fn load() -> Result<Self, Box<dyn std::error::Error>> { | 47 | pub fn load() -> Result<Self, Box<dyn std::error::Error>> { |
| 30 | let config_dir = if cfg!(debug_assertions) { | 48 | let config_path = Self::config_path()?; |
| 31 | std::env::current_exe()? | 49 | let config_dir = config_path |
| 32 | .parent() | 50 | .parent() |
| 33 | .map(|p| p.to_path_buf()) | 51 | .map(|p| p.to_path_buf()) |
| 34 | .unwrap_or_else(|| PathBuf::from(".")) | 52 | .unwrap_or_else(|| PathBuf::from(".")); |
| 35 | } else { | ||
| 36 | let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer") | ||
| 37 | .ok_or("Failed to get project directories")?; | ||
| 38 | if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { | ||
| 39 | PathBuf::from(xdg).join("rsplayer") | ||
| 40 | } else { | ||
| 41 | proj_dirs.config_dir().to_path_buf() | ||
| 42 | } | ||
| 43 | }; | ||
| 44 | |||
| 45 | let config_path = config_dir.join("config.toml"); | ||
| 46 | 53 | ||
| 47 | if !config_path.exists() { | 54 | if !config_path.exists() { |
| 48 | fs::create_dir_all(&config_dir)?; | 55 | fs::create_dir_all(&config_dir)?; |
| @@ -76,6 +83,64 @@ impl Config { | |||
| 76 | Ok(config) | 83 | Ok(config) |
| 77 | } | 84 | } |
| 78 | 85 | ||
| 86 | /// Where the config lives: next to the binary for debug builds (so a | ||
| 87 | /// checkout does not touch the installed config), XDG config otherwise. | ||
| 88 | pub fn config_path() -> Result<PathBuf, Box<dyn std::error::Error>> { | ||
| 89 | if cfg!(debug_assertions) { | ||
| 90 | return Ok(std::env::current_exe()? | ||
| 91 | .parent() | ||
| 92 | .map(|p| p.to_path_buf()) | ||
| 93 | .unwrap_or_else(|| PathBuf::from(".")) | ||
| 94 | .join("config.toml")); | ||
| 95 | } | ||
| 96 | let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer") | ||
| 97 | .ok_or("Failed to get project directories")?; | ||
| 98 | let config_dir = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { | ||
| 99 | PathBuf::from(xdg).join("rsplayer") | ||
| 100 | } else { | ||
| 101 | proj_dirs.config_dir().to_path_buf() | ||
| 102 | }; | ||
| 103 | Ok(config_dir.join("config.toml")) | ||
| 104 | } | ||
| 105 | |||
| 106 | fn file_stamp() -> FileStamp { | ||
| 107 | let stamp = Self::config_path() | ||
| 108 | .ok() | ||
| 109 | .and_then(|path| fs::metadata(path).ok()); | ||
| 110 | FileStamp { | ||
| 111 | modified: stamp.as_ref().and_then(|m| m.modified().ok()), | ||
| 112 | len: stamp.as_ref().map(|m| m.len()).unwrap_or(0), | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | /// The config, parsed once and reused until the file changes on disk. | ||
| 117 | /// | ||
| 118 | /// The daemon reads the config for every command, status line and | ||
| 119 | /// notification, and [`Config::load`] parses the TOML *and* rewrites the | ||
| 120 | /// file. Checking a stat instead keeps that off the hot paths while | ||
| 121 | /// `rsp settings set` still takes effect immediately. | ||
| 122 | pub fn cached() -> Result<Self, Box<dyn std::error::Error>> { | ||
| 123 | let stamp = Self::file_stamp(); | ||
| 124 | if let Ok(guard) = CACHE.read() | ||
| 125 | && let Some(cached) = guard.as_ref() | ||
| 126 | && cached.stamp == stamp | ||
| 127 | { | ||
| 128 | return Ok(cached.config.clone()); | ||
| 129 | } | ||
| 130 | |||
| 131 | let config = Self::load()?; | ||
| 132 | // `load` normalizes and rewrites the file, so take the stamp afterwards: | ||
| 133 | // reading it before would make our own write look like an outside edit. | ||
| 134 | let stamp = Self::file_stamp(); | ||
| 135 | if let Ok(mut guard) = CACHE.write() { | ||
| 136 | *guard = Some(CachedConfig { | ||
| 137 | config: config.clone(), | ||
| 138 | stamp, | ||
| 139 | }); | ||
| 140 | } | ||
| 141 | Ok(config) | ||
| 142 | } | ||
| 143 | |||
| 79 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { | 144 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 80 | let mut output = String::new(); | 145 | let mut output = String::new(); |
| 81 | let paths_str = self | 146 | let paths_str = self |
| @@ -131,6 +196,14 @@ impl Config { | |||
| 131 | fs::create_dir_all(parent)?; | 196 | fs::create_dir_all(parent)?; |
| 132 | } | 197 | } |
| 133 | fs::write(&self.config_path, output)?; | 198 | fs::write(&self.config_path, output)?; |
| 199 | // Keep the parsed cache in step with what was just written, so this | ||
| 200 | // write does not make the next `cached()` re-read the file. | ||
| 201 | if let Ok(mut guard) = CACHE.write() | ||
| 202 | && let Some(cached) = guard.as_mut() | ||
| 203 | { | ||
| 204 | cached.stamp = Self::file_stamp(); | ||
| 205 | cached.config = self.clone(); | ||
| 206 | } | ||
| 134 | Ok(()) | 207 | Ok(()) |
| 135 | } | 208 | } |
| 136 | 209 | ||
diff --git a/src/daemon.rs b/src/daemon.rs index 4f95820..eaae64f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -190,7 +190,7 @@ pub fn spawn_try_scrobble_current( | |||
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | 192 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { |
| 193 | let config = Config::load()?; | 193 | let config = Config::cached()?; |
| 194 | let socket_path = Config::get_socket_path(); | 194 | let socket_path = Config::get_socket_path(); |
| 195 | let pid_path = get_pid_path(&config); | 195 | let pid_path = get_pid_path(&config); |
| 196 | 196 | ||
| @@ -365,7 +365,7 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 365 | if let Ok(mut s) = scrobble_state.lock() { | 365 | if let Ok(mut s) = scrobble_state.lock() { |
| 366 | s.start(track.clone()); | 366 | s.start(track.clone()); |
| 367 | } | 367 | } |
| 368 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 368 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 369 | notification::notify_track(&track, fmt.as_deref()); | 369 | notification::notify_track(&track, fmt.as_deref()); |
| 370 | break; | 370 | break; |
| 371 | } | 371 | } |
| @@ -448,7 +448,7 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 448 | } | 448 | } |
| 449 | 449 | ||
| 450 | pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> { | 450 | pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> { |
| 451 | let config = Config::load()?; | 451 | let config = Config::cached()?; |
| 452 | let pid_path = get_pid_path(&config); | 452 | let pid_path = get_pid_path(&config); |
| 453 | 453 | ||
| 454 | match check_running_daemon(&pid_path) { | 454 | match check_running_daemon(&pid_path) { |
| @@ -708,7 +708,7 @@ enum LoadedLyrics { | |||
| 708 | 708 | ||
| 709 | /// Build the "Now playing" header shown above a track's lyrics. | 709 | /// Build the "Now playing" header shown above a track's lyrics. |
| 710 | fn lyrics_header(track: &Track) -> String { | 710 | fn lyrics_header(track: &Track) -> String { |
| 711 | let name = match Config::load() | 711 | let name = match Config::cached() |
| 712 | .ok() | 712 | .ok() |
| 713 | .and_then(|c| c.display_format.clone()) | 713 | .and_then(|c| c.display_format.clone()) |
| 714 | .as_deref() | 714 | .as_deref() |
| @@ -1022,7 +1022,7 @@ fn process_command( | |||
| 1022 | "Paused" | 1022 | "Paused" |
| 1023 | }; | 1023 | }; |
| 1024 | let track = Track::from_path(track_path); | 1024 | let track = Track::from_path(track_path); |
| 1025 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1025 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1026 | let track_str = match fmt.as_deref() { | 1026 | let track_str = match fmt.as_deref() { |
| 1027 | Some(f) => track.format(f), | 1027 | Some(f) => track.format(f), |
| 1028 | None => track.display_string(0, 1), | 1028 | None => track.display_string(0, 1), |
| @@ -1094,7 +1094,7 @@ fn process_command( | |||
| 1094 | "Shutting down\n".to_string() | 1094 | "Shutting down\n".to_string() |
| 1095 | } | 1095 | } |
| 1096 | "reload" | "validate" => { | 1096 | "reload" | "validate" => { |
| 1097 | if let Ok(config) = Config::load() { | 1097 | if let Ok(config) = Config::cached() { |
| 1098 | let temp_scrobbler = Scrobbler::new( | 1098 | let temp_scrobbler = Scrobbler::new( |
| 1099 | config.lastfm_api_key.clone(), | 1099 | config.lastfm_api_key.clone(), |
| 1100 | config.lastfm_api_secret.clone(), | 1100 | config.lastfm_api_secret.clone(), |
| @@ -1137,7 +1137,7 @@ fn process_command( | |||
| 1137 | if let Ok(mut s) = scrobble_state.lock() { | 1137 | if let Ok(mut s) = scrobble_state.lock() { |
| 1138 | s.start(track.clone()); | 1138 | s.start(track.clone()); |
| 1139 | } | 1139 | } |
| 1140 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1140 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1141 | notification::notify_track(&track, fmt.as_deref()); | 1141 | notification::notify_track(&track, fmt.as_deref()); |
| 1142 | "Playing next track\n".to_string() | 1142 | "Playing next track\n".to_string() |
| 1143 | } else { | 1143 | } else { |
| @@ -1160,7 +1160,7 @@ fn process_command( | |||
| 1160 | if let Ok(mut s) = scrobble_state.lock() { | 1160 | if let Ok(mut s) = scrobble_state.lock() { |
| 1161 | s.start(track.clone()); | 1161 | s.start(track.clone()); |
| 1162 | } | 1162 | } |
| 1163 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1163 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1164 | notification::notify_track(&track, fmt.as_deref()); | 1164 | notification::notify_track(&track, fmt.as_deref()); |
| 1165 | "Playing previous track\n".to_string() | 1165 | "Playing previous track\n".to_string() |
| 1166 | } else { | 1166 | } else { |
| @@ -1202,7 +1202,7 @@ fn process_command( | |||
| 1202 | "Usage: shuffle [on|off|album|albums]\n".to_string() | 1202 | "Usage: shuffle [on|off|album|albums]\n".to_string() |
| 1203 | }; | 1203 | }; |
| 1204 | // Save to config | 1204 | // Save to config |
| 1205 | if let Ok(mut config) = Config::load() { | 1205 | if let Ok(mut config) = Config::cached() { |
| 1206 | if mode_str == "off" || mode_str == "none" { | 1206 | if mode_str == "off" || mode_str == "none" { |
| 1207 | config.shuffle = None; | 1207 | config.shuffle = None; |
| 1208 | } else { | 1208 | } else { |
| @@ -1242,7 +1242,7 @@ fn process_command( | |||
| 1242 | let (album_tracks, display_name): (Vec<PathBuf>, String) = if path.is_file() { | 1242 | let (album_tracks, display_name): (Vec<PathBuf>, String) = if path.is_file() { |
| 1243 | let canon = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone()); | 1243 | let canon = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone()); |
| 1244 | let (artist, album, _, _, _) = crate::database::extract_metadata(&canon); | 1244 | let (artist, album, _, _, _) = crate::database::extract_metadata(&canon); |
| 1245 | let full: Vec<PathBuf> = match Config::load() { | 1245 | let full: Vec<PathBuf> = match Config::cached() { |
| 1246 | Ok(config) => match crate::database::Database::open(&config) { | 1246 | Ok(config) => match crate::database::Database::open(&config) { |
| 1247 | Ok(db) => db | 1247 | Ok(db) => db |
| 1248 | .find_tracks_by_album(&artist, &album) | 1248 | .find_tracks_by_album(&artist, &album) |
| @@ -1267,7 +1267,7 @@ fn process_command( | |||
| 1267 | .to_string(); | 1267 | .to_string(); |
| 1268 | (rotated, name) | 1268 | (rotated, name) |
| 1269 | } else { | 1269 | } else { |
| 1270 | match Config::load() { | 1270 | match Config::cached() { |
| 1271 | Ok(config) => match crate::database::Database::open(&config) { | 1271 | Ok(config) => match crate::database::Database::open(&config) { |
| 1272 | Ok(db) => { | 1272 | Ok(db) => { |
| 1273 | let tracks = match db.resolve_path(query_str) { | 1273 | let tracks = match db.resolve_path(query_str) { |
| @@ -1352,7 +1352,7 @@ fn process_command( | |||
| 1352 | if let Ok(mut s) = scrobble_state.lock() { | 1352 | if let Ok(mut s) = scrobble_state.lock() { |
| 1353 | s.start(track.clone()); | 1353 | s.start(track.clone()); |
| 1354 | } | 1354 | } |
| 1355 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1355 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1356 | notification::notify_track(track, fmt.as_deref()); | 1356 | notification::notify_track(track, fmt.as_deref()); |
| 1357 | } | 1357 | } |
| 1358 | format!( | 1358 | format!( |
| @@ -1464,7 +1464,7 @@ fn process_command( | |||
| 1464 | s.start(track.clone()); | 1464 | s.start(track.clone()); |
| 1465 | } | 1465 | } |
| 1466 | let fmt = | 1466 | let fmt = |
| 1467 | Config::load().ok().and_then(|c| c.display_format.clone()); | 1467 | Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1468 | notification::notify_track(track, fmt.as_deref()); | 1468 | notification::notify_track(track, fmt.as_deref()); |
| 1469 | } | 1469 | } |
| 1470 | format!("Playing now: {}\n", path_str) | 1470 | format!("Playing now: {}\n", path_str) |
| @@ -1486,7 +1486,7 @@ fn process_command( | |||
| 1486 | s.start(track.clone()); | 1486 | s.start(track.clone()); |
| 1487 | } | 1487 | } |
| 1488 | let fmt = | 1488 | let fmt = |
| 1489 | Config::load().ok().and_then(|c| c.display_format.clone()); | 1489 | Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1490 | notification::notify_track(track, fmt.as_deref()); | 1490 | notification::notify_track(track, fmt.as_deref()); |
| 1491 | } | 1491 | } |
| 1492 | format!("Queued: {}\n", path_str) | 1492 | format!("Queued: {}\n", path_str) |
| @@ -1514,8 +1514,9 @@ fn process_command( | |||
| 1514 | if let Ok(mut s) = scrobble_state.lock() { | 1514 | if let Ok(mut s) = scrobble_state.lock() { |
| 1515 | s.start(track.clone()); | 1515 | s.start(track.clone()); |
| 1516 | } | 1516 | } |
| 1517 | let fmt = | 1517 | let fmt = Config::cached() |
| 1518 | Config::load().ok().and_then(|c| c.display_format.clone()); | 1518 | .ok() |
| 1519 | .and_then(|c| c.display_format.clone()); | ||
| 1519 | notification::notify_track(track, fmt.as_deref()); | 1520 | notification::notify_track(track, fmt.as_deref()); |
| 1520 | } | 1521 | } |
| 1521 | format!("Playing now: {}\n", path_str) | 1522 | format!("Playing now: {}\n", path_str) |
| @@ -1535,8 +1536,9 @@ fn process_command( | |||
| 1535 | if let Ok(mut s) = scrobble_state.lock() { | 1536 | if let Ok(mut s) = scrobble_state.lock() { |
| 1536 | s.start(track.clone()); | 1537 | s.start(track.clone()); |
| 1537 | } | 1538 | } |
| 1538 | let fmt = | 1539 | let fmt = Config::cached() |
| 1539 | Config::load().ok().and_then(|c| c.display_format.clone()); | 1540 | .ok() |
| 1541 | .and_then(|c| c.display_format.clone()); | ||
| 1540 | notification::notify_track(&track, fmt.as_deref()); | 1542 | notification::notify_track(&track, fmt.as_deref()); |
| 1541 | format!("Playing: {}\n", path_str) | 1543 | format!("Playing: {}\n", path_str) |
| 1542 | } else { | 1544 | } else { |
| @@ -1568,7 +1570,7 @@ fn process_command( | |||
| 1568 | if let Ok(mut s) = scrobble_state.lock() { | 1570 | if let Ok(mut s) = scrobble_state.lock() { |
| 1569 | s.start(track.clone()); | 1571 | s.start(track.clone()); |
| 1570 | } | 1572 | } |
| 1571 | let fmt = Config::load() | 1573 | let fmt = Config::cached() |
| 1572 | .ok() | 1574 | .ok() |
| 1573 | .and_then(|c| c.display_format.clone()); | 1575 | .and_then(|c| c.display_format.clone()); |
| 1574 | notification::notify_track(track, fmt.as_deref()); | 1576 | notification::notify_track(track, fmt.as_deref()); |
| @@ -1602,7 +1604,7 @@ fn process_command( | |||
| 1602 | if let Ok(mut s) = scrobble_state.lock() { | 1604 | if let Ok(mut s) = scrobble_state.lock() { |
| 1603 | s.start(track.clone()); | 1605 | s.start(track.clone()); |
| 1604 | } | 1606 | } |
| 1605 | let fmt = Config::load() | 1607 | let fmt = Config::cached() |
| 1606 | .ok() | 1608 | .ok() |
| 1607 | .and_then(|c| c.display_format.clone()); | 1609 | .and_then(|c| c.display_format.clone()); |
| 1608 | notification::notify_track(track, fmt.as_deref()); | 1610 | notification::notify_track(track, fmt.as_deref()); |
| @@ -1622,7 +1624,7 @@ fn process_command( | |||
| 1622 | _ => format!("Error: No audio files found in '{}'\n", path_str), | 1624 | _ => format!("Error: No audio files found in '{}'\n", path_str), |
| 1623 | } | 1625 | } |
| 1624 | } else { | 1626 | } else { |
| 1625 | match Config::load() { | 1627 | match Config::cached() { |
| 1626 | Ok(config) => match crate::database::Database::open(&config) { | 1628 | Ok(config) => match crate::database::Database::open(&config) { |
| 1627 | Ok(db) => { | 1629 | Ok(db) => { |
| 1628 | // Collect tracks: try hierarchical path first, fallback to search | 1630 | // Collect tracks: try hierarchical path first, fallback to search |
| @@ -1666,7 +1668,7 @@ fn process_command( | |||
| 1666 | if let Ok(mut s) = scrobble_state.lock() { | 1668 | if let Ok(mut s) = scrobble_state.lock() { |
| 1667 | s.start(track.clone()); | 1669 | s.start(track.clone()); |
| 1668 | } | 1670 | } |
| 1669 | let fmt = Config::load() | 1671 | let fmt = Config::cached() |
| 1670 | .ok() | 1672 | .ok() |
| 1671 | .and_then(|c| c.display_format.clone()); | 1673 | .and_then(|c| c.display_format.clone()); |
| 1672 | notification::notify_track(track, fmt.as_deref()); | 1674 | notification::notify_track(track, fmt.as_deref()); |
| @@ -1710,7 +1712,7 @@ fn process_command( | |||
| 1710 | if let Ok(mut s) = scrobble_state.lock() { | 1712 | if let Ok(mut s) = scrobble_state.lock() { |
| 1711 | s.start(track.clone()); | 1713 | s.start(track.clone()); |
| 1712 | } | 1714 | } |
| 1713 | let fmt = Config::load() | 1715 | let fmt = Config::cached() |
| 1714 | .ok() | 1716 | .ok() |
| 1715 | .and_then(|c| c.display_format.clone()); | 1717 | .and_then(|c| c.display_format.clone()); |
| 1716 | notification::notify_track( | 1718 | notification::notify_track( |
| @@ -1772,7 +1774,7 @@ fn process_command( | |||
| 1772 | let current = audio.get_volume() as i8; | 1774 | let current = audio.get_volume() as i8; |
| 1773 | let new_level = (current + sign * delta).clamp(0, 100) as u8; | 1775 | let new_level = (current + sign * delta).clamp(0, 100) as u8; |
| 1774 | audio.set_volume(new_level as f32 / 100.0); | 1776 | audio.set_volume(new_level as f32 / 100.0); |
| 1775 | if let Ok(mut config) = Config::load() { | 1777 | if let Ok(mut config) = Config::cached() { |
| 1776 | config.volume = Some(new_level); | 1778 | config.volume = Some(new_level); |
| 1777 | let _ = config.save(); | 1779 | let _ = config.save(); |
| 1778 | } | 1780 | } |
| @@ -1781,7 +1783,7 @@ fn process_command( | |||
| 1781 | // Try absolute value | 1783 | // Try absolute value |
| 1782 | if let Ok(level) = arg.parse::<u8>() { | 1784 | if let Ok(level) = arg.parse::<u8>() { |
| 1783 | audio.set_volume(level as f32 / 100.0); | 1785 | audio.set_volume(level as f32 / 100.0); |
| 1784 | if let Ok(mut config) = Config::load() { | 1786 | if let Ok(mut config) = Config::cached() { |
| 1785 | config.volume = Some(level); | 1787 | config.volume = Some(level); |
| 1786 | let _ = config.save(); | 1788 | let _ = config.save(); |
| 1787 | } | 1789 | } |
| @@ -1811,7 +1813,7 @@ fn process_command( | |||
| 1811 | q.shuffled_indices().to_vec() | 1813 | q.shuffled_indices().to_vec() |
| 1812 | }; | 1814 | }; |
| 1813 | let is_playing = audio.is_playing(); | 1815 | let is_playing = audio.is_playing(); |
| 1814 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1816 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 1815 | for (display_idx, &track_idx) in indices.iter().enumerate() { | 1817 | for (display_idx, &track_idx) in indices.iter().enumerate() { |
| 1816 | if let Some(track) = q.track_at(track_idx) { | 1818 | if let Some(track) = q.track_at(track_idx) { |
| 1817 | let track_str = match fmt.as_deref() { | 1819 | let track_str = match fmt.as_deref() { |
diff --git a/src/mpris.rs b/src/mpris.rs index de19113..9daf516 100644 --- a/src/mpris.rs +++ b/src/mpris.rs | |||
| @@ -204,7 +204,7 @@ impl MprisPlayer { | |||
| 204 | if let Ok(mut s) = self.scrobble_state.lock() { | 204 | if let Ok(mut s) = self.scrobble_state.lock() { |
| 205 | s.start(track.clone()); | 205 | s.start(track.clone()); |
| 206 | } | 206 | } |
| 207 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 207 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 208 | notification::notify_track(&track, fmt.as_deref()); | 208 | notification::notify_track(&track, fmt.as_deref()); |
| 209 | } else { | 209 | } else { |
| 210 | self.audio.stop(); | 210 | self.audio.stop(); |
| @@ -226,7 +226,7 @@ impl MprisPlayer { | |||
| 226 | if let Ok(mut s) = self.scrobble_state.lock() { | 226 | if let Ok(mut s) = self.scrobble_state.lock() { |
| 227 | s.start(track.clone()); | 227 | s.start(track.clone()); |
| 228 | } | 228 | } |
| 229 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 229 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 230 | notification::notify_track(&track, fmt.as_deref()); | 230 | notification::notify_track(&track, fmt.as_deref()); |
| 231 | } | 231 | } |
| 232 | } | 232 | } |
| @@ -271,7 +271,7 @@ impl MprisPlayer { | |||
| 271 | if let Ok(mut s) = self.scrobble_state.lock() { | 271 | if let Ok(mut s) = self.scrobble_state.lock() { |
| 272 | s.start(track.clone()); | 272 | s.start(track.clone()); |
| 273 | } | 273 | } |
| 274 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 274 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 275 | notification::notify_track(track, fmt.as_deref()); | 275 | notification::notify_track(track, fmt.as_deref()); |
| 276 | } | 276 | } |
| 277 | } | 277 | } |
| @@ -319,7 +319,7 @@ impl MprisPlayer { | |||
| 319 | if let Ok(mut s) = self.scrobble_state.lock() { | 319 | if let Ok(mut s) = self.scrobble_state.lock() { |
| 320 | s.start(t.clone()); | 320 | s.start(t.clone()); |
| 321 | } | 321 | } |
| 322 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 322 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 323 | notification::notify_track(t, fmt.as_deref()); | 323 | notification::notify_track(t, fmt.as_deref()); |
| 324 | } | 324 | } |
| 325 | } else if path.is_file() { | 325 | } else if path.is_file() { |
| @@ -331,7 +331,7 @@ impl MprisPlayer { | |||
| 331 | if let Ok(mut s) = self.scrobble_state.lock() { | 331 | if let Ok(mut s) = self.scrobble_state.lock() { |
| 332 | s.start(track.clone()); | 332 | s.start(track.clone()); |
| 333 | } | 333 | } |
| 334 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 334 | let fmt = Config::cached().ok().and_then(|c| c.display_format.clone()); |
| 335 | notification::notify_track(&track, fmt.as_deref()); | 335 | notification::notify_track(&track, fmt.as_deref()); |
| 336 | } | 336 | } |
| 337 | } | 337 | } |
| @@ -417,7 +417,7 @@ impl MprisPlayer { | |||
| 417 | fn set_volume(&self, value: f64) { | 417 | fn set_volume(&self, value: f64) { |
| 418 | let level = (value * 100.0).clamp(0.0, 100.0) as u8; | 418 | let level = (value * 100.0).clamp(0.0, 100.0) as u8; |
| 419 | self.audio.set_volume(level as f32 / 100.0); | 419 | self.audio.set_volume(level as f32 / 100.0); |
| 420 | if let Ok(mut config) = Config::load() { | 420 | if let Ok(mut config) = Config::cached() { |
| 421 | config.volume = Some(level); | 421 | config.volume = Some(level); |
| 422 | let _ = config.save(); | 422 | let _ = config.save(); |
| 423 | } | 423 | } |
