From b2ec417db4c0503a1079fe64edab07a035719fe7 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 5 May 2026 22:28:13 +0200 Subject: 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'). --- src/daemon.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 126 insertions(+), 11 deletions(-) (limited to 'src') 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 { Album, } +#[derive(Debug, Clone)] +struct Track { + path: PathBuf, + title: Option, + artist: Option, + album: Option, +} + +impl Track { + fn from_path(path: PathBuf) -> Self { + let metadata = extract_metadata(&path); + let metadata = metadata.as_ref(); + Track { + path, + title: metadata.and_then(|m| m.title.clone()), + artist: metadata.and_then(|m| m.artist.clone()), + album: metadata.and_then(|m| m.album.clone()), + } + } + + fn display_string(&self, index: usize) -> String { + let album_artist = self + .artist + .clone() + .or_else(|| self.album.clone()) + .unwrap_or_default(); + let title = self.title.clone().unwrap_or_else(|| { + self.path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Unknown") + .to_string() + }); + + if album_artist.is_empty() { + format!("{}. {}", index + 1, title) + } else { + format!("{}. {} - {}", index + 1, album_artist, title) + } + } +} + +#[derive(Debug, Default, Clone)] +struct AudioMetadata { + title: Option, + artist: Option, + album: Option, +} + +fn extract_metadata(path: &Path) -> Option { + use symphonia::core::io::MediaSourceStream; + use symphonia::core::meta::StandardTagKey; + use symphonia::core::probe::Hint; + + let file = std::fs::File::open(path).ok()?; + let mss = MediaSourceStream::new(Box::new(file), Default::default()); + + let hint = Hint::new(); + let mut probed = symphonia::default::get_probe() + .format(&hint, mss, &Default::default(), &Default::default()) + .ok()?; + + let mut metadata = AudioMetadata::default(); + + // First check probed metadata (e.g., ID3v2 headers before container) + if let Some(meta) = probed.metadata.get() { + if let Some(rev) = meta.current() { + for tag in rev.tags() { + match tag.std_key { + Some(StandardTagKey::TrackTitle) => { + metadata.title = Some(tag.value.to_string()); + } + Some(StandardTagKey::Artist) => { + metadata.artist = Some(tag.value.to_string()); + } + Some(StandardTagKey::Album) => { + metadata.album = Some(tag.value.to_string()); + } + _ => {} + } + } + } + } + + // If nothing found, try container metadata + if metadata.title.is_none() && metadata.artist.is_none() && metadata.album.is_none() { + let mut format = probed.format; + if let Some(rev) = format.metadata().current() { + for tag in rev.tags() { + match tag.std_key { + Some(StandardTagKey::TrackTitle) => { + metadata.title = Some(tag.value.to_string()); + } + Some(StandardTagKey::Artist) => { + metadata.artist = Some(tag.value.to_string()); + } + Some(StandardTagKey::Album) => { + metadata.album = Some(tag.value.to_string()); + } + _ => {} + } + } + } + } + + if metadata.title.is_some() || metadata.artist.is_some() || metadata.album.is_some() { + Some(metadata) + } else { + None + } +} + struct Queue { - tracks: Vec, + tracks: Vec, position: usize, shuffle: ShuffleMode, shuffled_indices: Vec, @@ -33,7 +145,7 @@ impl Queue { } fn add_track(&mut self, path: PathBuf) { - self.tracks.push(path); + self.tracks.push(Track::from_path(path)); self.update_shuffle_indices(); } @@ -51,7 +163,7 @@ impl Queue { } } - fn current(&self) -> Option<&PathBuf> { + fn current(&self) -> Option<&Track> { if self.tracks.is_empty() { None } else if let ShuffleMode::All = self.shuffle { @@ -73,7 +185,7 @@ impl Queue { let max_pos = self.tracks.len().saturating_sub(1); if self.position < max_pos { self.position += 1; - self.current().cloned() + self.current().map(|t| t.path.clone()) } else { None } @@ -82,7 +194,7 @@ impl Queue { fn previous(&mut self) -> Option { if self.position > 0 { self.position -= 1; - self.current().cloned() + self.current().map(|t| t.path.clone()) } else { None } @@ -334,8 +446,10 @@ fn process_command( match cmd { "status" => { if audio.is_playing() { - if let Some(track) = audio.current_track() { - format!("Playing\nTrack: {}\n", track.display()) + if let Some(track_path) = audio.current_track() { + let track = Track::from_path(track_path); + let track_str = track.display_string(0); + format!("Playing\nTrack: {}\n", track_str) } else { "Playing\n".to_string() } @@ -450,8 +564,8 @@ fn process_command( } // If shuffle is on, shuffled_indices is already set // Play the first track from the shuffled list - if let Some(track) = q.current().cloned() { - audio.play(track); + if let Some(track) = q.current() { + audio.play(track.path.clone()); } } format!("Queued {} tracks from: {}\n", files.len(), path_str) @@ -506,10 +620,11 @@ fn process_command( }; for (display_idx, &track_idx) in indices.iter().enumerate() { if let Some(track) = q.tracks.get(track_idx) { + let track_str = track.display_string(display_idx); if display_idx == current_position { - output.push_str(&format!("> {} (playing)\n", track.display())); + output.push_str(&format!("> {} (playing)\n", track_str)); } else { - output.push_str(&format!(" {}\n", track.display())); + output.push_str(&format!(" {}\n", track_str)); } } } -- cgit v1.2.3