From cfd0781ebae76447cc8072fd60883aabf8158d32 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 8 May 2026 00:40:40 +0200 Subject: Extract queue module, add comprehensive tests, fix queue/play bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Moved Queue, Track, ShuffleMode, AudioMetadata from daemon.rs into new src/queue.rs module with public API\n• Fixed rsp play interrupting active playback (now checks is_playing)\n• Fixed directory play -n/-i inserting files in reverse order (now uses .iter().rev())\n• Removed dead duplicate shuffle handler in process_command\n• Fixed queue display showing (playing) when playback is stopped\n• Added 34 tests covering navigation, shuffle, insert ordering, edge cases, display --- src/queue.rs | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/queue.rs (limited to 'src/queue.rs') diff --git a/src/queue.rs b/src/queue.rs new file mode 100644 index 0000000..53b104f --- /dev/null +++ b/src/queue.rs @@ -0,0 +1,250 @@ +use rand::seq::SliceRandom; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ShuffleMode { + None, + All, + Album, +} + +#[derive(Debug, Default, Clone)] +pub struct AudioMetadata { + pub title: Option, + pub artist: Option, + pub album: Option, +} + +pub 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(); + + if let Some(meta) = probed.metadata.get() + && 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 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 + } +} + +#[derive(Debug, Clone)] +pub struct Track { + pub path: PathBuf, + pub title: Option, + pub artist: Option, + pub album: Option, +} + +impl Track { + pub 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()), + } + } + + pub 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) + } + } +} + +pub struct Queue { + tracks: Vec, + position: usize, + shuffle: ShuffleMode, + shuffled_indices: Vec, +} + +impl Default for Queue { + fn default() -> Self { + Self::new() + } +} + +impl Queue { + pub fn new() -> Self { + Queue { + tracks: Vec::new(), + position: 0, + shuffle: ShuffleMode::None, + shuffled_indices: Vec::new(), + } + } + + pub fn add_track(&mut self, path: PathBuf) { + self.tracks.push(Track::from_path(path)); + self.update_shuffle_indices(); + } + + pub fn insert_after_current(&mut self, path: PathBuf) { + if self.tracks.is_empty() { + self.tracks.push(Track::from_path(path)); + } else { + let insert_idx = self.position + 1; + self.tracks.insert(insert_idx, Track::from_path(path)); + } + self.update_shuffle_indices(); + } + + pub fn track_at(&self, idx: usize) -> Option<&Track> { + self.tracks.get(idx) + } + + pub fn set_shuffle(&mut self, mode: ShuffleMode) { + self.shuffle = mode; + self.update_shuffle_indices(); + } + + fn update_shuffle_indices(&mut self) { + let len = self.tracks.len(); + self.shuffled_indices = (0..len).collect(); + if let ShuffleMode::All = self.shuffle { + let mut rng = rand::rng(); + let pos = self.position.min(len.saturating_sub(1)); + if pos + 1 < len { + self.shuffled_indices[pos + 1..].shuffle(&mut rng); + } + } + } + + pub fn current(&self) -> Option<&Track> { + if self.tracks.is_empty() { + None + } else if self.shuffle == ShuffleMode::All { + self.shuffled_indices + .get(self.position) + .and_then(|&idx| self.tracks.get(idx)) + } else if self.shuffle == ShuffleMode::Album { + self.shuffled_indices + .get(self.position) + .and_then(|&idx| self.tracks.get(idx)) + } else { + self.tracks.get(self.position) + } + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option { + let max_pos = self.tracks.len().saturating_sub(1); + if self.position < max_pos { + self.position += 1; + self.current().map(|t| t.path.clone()) + } else { + None + } + } + + pub fn previous(&mut self) -> Option { + if self.position > 0 { + self.position -= 1; + self.current().map(|t| t.path.clone()) + } else { + None + } + } + + pub fn is_empty(&self) -> bool { + self.tracks.is_empty() + } + + pub fn len(&self) -> usize { + self.tracks.len() + } + + pub fn position(&self) -> usize { + self.position + } + + pub fn set_position(&mut self, pos: usize) { + self.position = pos; + } + + #[allow(dead_code)] + pub fn tracks(&self) -> &[Track] { + &self.tracks + } + + pub fn shuffle_mode(&self) -> &ShuffleMode { + &self.shuffle + } + + pub fn shuffled_indices(&self) -> &[usize] { + &self.shuffled_indices + } + + pub fn clear(&mut self) { + self.tracks.clear(); + self.position = 0; + self.shuffled_indices.clear(); + } +} -- cgit v1.2.3