aboutsummaryrefslogtreecommitdiffstats
path: root/src/daemon.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-05-05 22:28:13 +0200
committersalmonik <raisersoft@proton.me>2026-05-05 22:28:13 +0200
commitb2ec417db4c0503a1079fe64edab07a035719fe7 (patch)
tree1af4c68286787841a78ec3ea02df57730ef86a8f /src/daemon.rs
parentfix(audio): resolve deadlock preventing auto-advance to next track (diff)
downloadrsplayer-b2ec417db4c0503a1079fe64edab07a035719fe7.tar.gz
rsplayer-b2ec417db4c0503a1079fe64edab07a035719fe7.zip
Add metadata extraction and display for folder playback
Extract ID3/metadata tags from audio files using symphonia and display as 'INDEX. ARTIST - TITLE' format, falling back to filename. Only shows the track number before any slash (e.g., '15' from '15/19').
Diffstat (limited to 'src/daemon.rs')
-rw-r--r--src/daemon.rs137
1 files changed, 126 insertions, 11 deletions
diff --git a/src/daemon.rs b/src/daemon.rs
index 9fa5cea..4122c37 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -15,8 +15,120 @@ enum ShuffleMode {
15 Album, 15 Album,
16} 16}
17 17
18#[derive(Debug, Clone)]
19struct Track {
20 path: PathBuf,
21 title: Option<String>,
22 artist: Option<String>,
23 album: Option<String>,
24}
25
26impl Track {
27 fn from_path(path: PathBuf) -> Self {
28 let metadata = extract_metadata(&path);
29 let metadata = metadata.as_ref();
30 Track {
31 path,
32 title: metadata.and_then(|m| m.title.clone()),
33 artist: metadata.and_then(|m| m.artist.clone()),
34 album: metadata.and_then(|m| m.album.clone()),
35 }
36 }
37
38 fn display_string(&self, index: usize) -> String {
39 let album_artist = self
40 .artist
41 .clone()
42 .or_else(|| self.album.clone())
43 .unwrap_or_default();
44 let title = self.title.clone().unwrap_or_else(|| {
45 self.path
46 .file_name()
47 .and_then(|n| n.to_str())
48 .unwrap_or("Unknown")
49 .to_string()
50 });
51
52 if album_artist.is_empty() {
53 format!("{}. {}", index + 1, title)
54 } else {
55 format!("{}. {} - {}", index + 1, album_artist, title)
56 }
57 }
58}
59
60#[derive(Debug, Default, Clone)]
61struct AudioMetadata {
62 title: Option<String>,
63 artist: Option<String>,
64 album: Option<String>,
65}
66
67fn extract_metadata(path: &Path) -> Option<AudioMetadata> {
68 use symphonia::core::io::MediaSourceStream;
69 use symphonia::core::meta::StandardTagKey;
70 use symphonia::core::probe::Hint;
71
72 let file = std::fs::File::open(path).ok()?;
73 let mss = MediaSourceStream::new(Box::new(file), Default::default());
74
75 let hint = Hint::new();
76 let mut probed = symphonia::default::get_probe()
77 .format(&hint, mss, &Default::default(), &Default::default())
78 .ok()?;
79
80 let mut metadata = AudioMetadata::default();
81
82 // First check probed metadata (e.g., ID3v2 headers before container)
83 if let Some(meta) = probed.metadata.get() {
84 if let Some(rev) = meta.current() {
85 for tag in rev.tags() {
86 match tag.std_key {
87 Some(StandardTagKey::TrackTitle) => {
88 metadata.title = Some(tag.value.to_string());
89 }
90 Some(StandardTagKey::Artist) => {
91 metadata.artist = Some(tag.value.to_string());
92 }
93 Some(StandardTagKey::Album) => {
94 metadata.album = Some(tag.value.to_string());
95 }
96 _ => {}
97 }
98 }
99 }
100 }
101
102 // If nothing found, try container metadata
103 if metadata.title.is_none() && metadata.artist.is_none() && metadata.album.is_none() {
104 let mut format = probed.format;
105 if let Some(rev) = format.metadata().current() {
106 for tag in rev.tags() {
107 match tag.std_key {
108 Some(StandardTagKey::TrackTitle) => {
109 metadata.title = Some(tag.value.to_string());
110 }
111 Some(StandardTagKey::Artist) => {
112 metadata.artist = Some(tag.value.to_string());
113 }
114 Some(StandardTagKey::Album) => {
115 metadata.album = Some(tag.value.to_string());
116 }
117 _ => {}
118 }
119 }
120 }
121 }
122
123 if metadata.title.is_some() || metadata.artist.is_some() || metadata.album.is_some() {
124 Some(metadata)
125 } else {
126 None
127 }
128}
129
18struct Queue { 130struct Queue {
19 tracks: Vec<PathBuf>, 131 tracks: Vec<Track>,
20 position: usize, 132 position: usize,
21 shuffle: ShuffleMode, 133 shuffle: ShuffleMode,
22 shuffled_indices: Vec<usize>, 134 shuffled_indices: Vec<usize>,
@@ -33,7 +145,7 @@ impl Queue {
33 } 145 }
34 146
35 fn add_track(&mut self, path: PathBuf) { 147 fn add_track(&mut self, path: PathBuf) {
36 self.tracks.push(path); 148 self.tracks.push(Track::from_path(path));
37 self.update_shuffle_indices(); 149 self.update_shuffle_indices();
38 } 150 }
39 151
@@ -51,7 +163,7 @@ impl Queue {
51 } 163 }
52 } 164 }
53 165
54 fn current(&self) -> Option<&PathBuf> { 166 fn current(&self) -> Option<&Track> {
55 if self.tracks.is_empty() { 167 if self.tracks.is_empty() {
56 None 168 None
57 } else if let ShuffleMode::All = self.shuffle { 169 } else if let ShuffleMode::All = self.shuffle {
@@ -73,7 +185,7 @@ impl Queue {
73 let max_pos = self.tracks.len().saturating_sub(1); 185 let max_pos = self.tracks.len().saturating_sub(1);
74 if self.position < max_pos { 186 if self.position < max_pos {
75 self.position += 1; 187 self.position += 1;
76 self.current().cloned() 188 self.current().map(|t| t.path.clone())
77 } else { 189 } else {
78 None 190 None
79 } 191 }
@@ -82,7 +194,7 @@ impl Queue {
82 fn previous(&mut self) -> Option<PathBuf> { 194 fn previous(&mut self) -> Option<PathBuf> {
83 if self.position > 0 { 195 if self.position > 0 {
84 self.position -= 1; 196 self.position -= 1;
85 self.current().cloned() 197 self.current().map(|t| t.path.clone())
86 } else { 198 } else {
87 None 199 None
88 } 200 }
@@ -334,8 +446,10 @@ fn process_command(
334 match cmd { 446 match cmd {
335 "status" => { 447 "status" => {
336 if audio.is_playing() { 448 if audio.is_playing() {
337 if let Some(track) = audio.current_track() { 449 if let Some(track_path) = audio.current_track() {
338 format!("Playing\nTrack: {}\n", track.display()) 450 let track = Track::from_path(track_path);
451 let track_str = track.display_string(0);
452 format!("Playing\nTrack: {}\n", track_str)
339 } else { 453 } else {
340 "Playing\n".to_string() 454 "Playing\n".to_string()
341 } 455 }
@@ -450,8 +564,8 @@ fn process_command(
450 } 564 }
451 // If shuffle is on, shuffled_indices is already set 565 // If shuffle is on, shuffled_indices is already set
452 // Play the first track from the shuffled list 566 // Play the first track from the shuffled list
453 if let Some(track) = q.current().cloned() { 567 if let Some(track) = q.current() {
454 audio.play(track); 568 audio.play(track.path.clone());
455 } 569 }
456 } 570 }
457 format!("Queued {} tracks from: {}\n", files.len(), path_str) 571 format!("Queued {} tracks from: {}\n", files.len(), path_str)
@@ -506,10 +620,11 @@ fn process_command(
506 }; 620 };
507 for (display_idx, &track_idx) in indices.iter().enumerate() { 621 for (display_idx, &track_idx) in indices.iter().enumerate() {
508 if let Some(track) = q.tracks.get(track_idx) { 622 if let Some(track) = q.tracks.get(track_idx) {
623 let track_str = track.display_string(display_idx);
509 if display_idx == current_position { 624 if display_idx == current_position {
510 output.push_str(&format!("> {} (playing)\n", track.display())); 625 output.push_str(&format!("> {} (playing)\n", track_str));
511 } else { 626 } else {
512 output.push_str(&format!(" {}\n", track.display())); 627 output.push_str(&format!(" {}\n", track_str));
513 } 628 }
514 } 629 }
515 } 630 }