| 1 | //! Reading `music_directory` from the local MPD configuration. |
| 2 | //! |
| 3 | //! MPD only reveals its music directory over a Unix socket, so for a local |
| 4 | //! server reached over TCP rmptui reads it from MPD's own config file. |
| 5 | |
| 6 | use std::{ |
| 7 | env, fs, |
| 8 | path::{Path, PathBuf}, |
| 9 | }; |
| 10 | |
| 11 | use crate::{mpd::Address, util::expand_tilde}; |
| 12 | |
| 13 | /// The music directory of a local MPD, if `address` is local and an MPD |
| 14 | /// config file names one. |
| 15 | pub fn local_music_directory(address: &Address) -> Option<PathBuf> { |
| 16 | if !address.is_local() { |
| 17 | return None; |
| 18 | } |
| 19 | candidates() |
| 20 | .iter() |
| 21 | .find_map(|path| fs::read_to_string(path).ok()) |
| 22 | .and_then(|contents| parse_music_directory(&contents)) |
| 23 | } |
| 24 | |
| 25 | /// MPD's config file locations, in MPD's lookup order. |
| 26 | fn candidates() -> Vec<PathBuf> { |
| 27 | let mut paths = Vec::new(); |
| 28 | if let Some(dir) = env::var_os("XDG_CONFIG_HOME").filter(|d| !d.is_empty()) { |
| 29 | paths.push(Path::new(&dir).join("mpd/mpd.conf")); |
| 30 | } |
| 31 | paths.extend( |
| 32 | [ |
| 33 | "~/.config/mpd/mpd.conf", |
| 34 | "~/.mpdconf", |
| 35 | "~/.mpd/mpd.conf", |
| 36 | "/etc/mpd.conf", |
| 37 | ] |
| 38 | .map(expand_tilde), |
| 39 | ); |
| 40 | paths |
| 41 | } |
| 42 | |
| 43 | fn parse_music_directory(contents: &str) -> Option<PathBuf> { |
| 44 | contents.lines().find_map(|line| { |
| 45 | let line = line.trim(); |
| 46 | let value = line.strip_prefix("music_directory")?; |
| 47 | if !value.starts_with(char::is_whitespace) { |
| 48 | return None; |
| 49 | } |
| 50 | let value = value.trim(); |
| 51 | let value = value |
| 52 | .strip_prefix('"') |
| 53 | .and_then(|v| v.split_once('"')) |
| 54 | .map_or(value, |(inner, _)| inner); |
| 55 | (!value.is_empty()).then(|| expand_tilde(value)) |
| 56 | }) |
| 57 | } |
| 58 | |
| 59 | #[cfg(test)] |
| 60 | mod tests { |
| 61 | use super::*; |
| 62 | |
| 63 | #[test] |
| 64 | fn parses_music_directory() { |
| 65 | let conf = "# music_directory \"/commented\"\n\ |
| 66 | music_directory_x \"/no\"\n\ |
| 67 | music_directory\t\t\"~/music\" # trailing comment\n"; |
| 68 | let home = env::var("HOME").unwrap(); |
| 69 | assert_eq!( |
| 70 | parse_music_directory(conf), |
| 71 | Some(PathBuf::from(format!("{home}/music"))) |
| 72 | ); |
| 73 | assert_eq!( |
| 74 | parse_music_directory("music_directory /srv/music"), |
| 75 | Some(PathBuf::from("/srv/music")) |
| 76 | ); |
| 77 | assert_eq!(parse_music_directory("port \"6600\""), None); |
| 78 | } |
| 79 | |
| 80 | #[test] |
| 81 | fn ignores_remote_servers() { |
| 82 | assert_eq!( |
| 83 | local_music_directory(&Address::Tcp("192.168.1.20:6600".into())), |
| 84 | None |
| 85 | ); |
| 86 | } |
| 87 | } |