leenfetch_core/modules/linux/
song.rs1use std::env;
2use std::process::Command;
3
4use crate::modules::enums::SongInfo;
5
6pub fn get_song() -> Option<SongInfo> {
7 let player = detect_player()?;
8
9 let output = match player.as_str() {
11 "mocp" => run(&["mocp", "-Q", "%artist \n%album \n%song"]),
12 "deadbeef" => run(&[
13 "deadbeef",
14 "--nowplaying-tf",
15 "%artist% \\n%album% \\n%title%",
16 ]),
17 "qmmp" => run(&["qmmp", "--nowplaying", "%p \n%a \n%t"]),
18 "spotify" | "vlc" | "rhythmbox" | "clementine" | "lollypop" => run(&[
19 "playerctl",
20 "metadata",
21 "--player",
22 player.as_str(),
23 "--format",
24 "{{ artist }}\n{{ album }}\n{{ title }}",
25 ]),
26 "mpd" | "mopidy" => run(&["mpc", "-f", "%artist% \n%album% \n%title%", "current"]),
27 "playerctl" => run(&[
28 "playerctl",
29 "metadata",
30 "--format",
31 "{{ artist }}\n{{ album }}\n{{ title }}",
32 ]),
33 _ => return None,
34 }?;
35
36 let mut lines = output.lines();
38 let artist = lines.next().unwrap_or("Unknown Artist").trim().to_string();
39 let album = lines.next().unwrap_or("Unknown Album").trim().to_string();
40 let title = lines.next().unwrap_or("Unknown Song").trim().to_string();
41
42 Some(SongInfo {
43 artist,
44 album,
45 title,
46 })
47}
48
49fn run(cmd: &[&str]) -> Option<String> {
50 let output = Command::new(cmd[0]).args(&cmd[1..]).output().ok()?;
51 if !output.status.success() {
52 return None;
53 }
54 Some(String::from_utf8_lossy(&output.stdout).to_string())
55}
56
57fn detect_player() -> Option<String> {
58 let env_player = env::var("MUSIC_PLAYER").ok();
59 if let Some(p) = env_player {
60 return Some(p);
61 }
62
63 let players = [
64 "spotify",
65 "playerctl",
66 "mocp",
67 "qmmp",
68 "mpd",
69 "mopidy",
70 "vlc",
71 "rhythmbox",
72 "deadbeef",
73 "lollypop",
74 "clementine",
75 ];
76
77 if let Ok(proc_path) = std::fs::read_dir("/proc") {
79 for entry in proc_path.flatten() {
80 let name = entry.file_name();
81 let name_str = name.to_string_lossy();
82
83 if !name_str.chars().all(|c| c.is_ascii_digit()) {
85 continue;
86 }
87
88 let comm_path = entry.path().join("comm");
89 if let Ok(comm) = std::fs::read_to_string(&comm_path) {
90 let process_name = comm.trim().to_lowercase();
91 for player in &players {
92 if process_name.contains(*player) {
93 return Some(player.to_string());
94 }
95 }
96 }
97 }
98 }
99
100 None
101}
102
103#[cfg(test)]
104mod tests {
105
106 #[test]
107 fn test_parse_output_defaults() {
108 let mock_output = "\n\n\n";
109 let mut lines = mock_output.lines();
110
111 let artist = {
112 let val = lines.next().unwrap_or("").trim();
113 if val.is_empty() {
114 "Unknown Artist"
115 } else {
116 val
117 }
118 };
119 let album = {
120 let val = lines.next().unwrap_or("").trim();
121 if val.is_empty() {
122 "Unknown Album"
123 } else {
124 val
125 }
126 };
127 let title = {
128 let val = lines.next().unwrap_or("").trim();
129 if val.is_empty() {
130 "Unknown Song"
131 } else {
132 val
133 }
134 };
135
136 assert_eq!(artist, "Unknown Artist");
137 assert_eq!(album, "Unknown Album");
138 assert_eq!(title, "Unknown Song");
139 }
140}