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, total: 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() }); let width = total.to_string().len(); if album_artist.is_empty() { format!("{:0width$}. {}", index + 1, title) } else { format!("{:0width$}. {} - {}", 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 == 0 && len > 1 { self.shuffled_indices.shuffle(&mut rng); } else 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(); } }