/* * This file is a part of RSPlayer * Licensed under the GPL-3.0 License * Copyright (C) 2026 Salmonik @ salmonik@tuta.io * https://codeberg.org/salmonik/RSPlayer */ use rand::seq::SliceRandom; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, PartialEq)] pub enum ShuffleMode { None, All, Album, Albums, } #[derive(Debug, Default, Clone)] pub struct AudioMetadata { pub title: Option, pub artist: Option, pub album: Option, pub track_number: Option, pub duration_secs: Option, } pub fn extract_metadata(path: &Path) -> Option { use symphonia::core::formats::probe::Hint; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::StandardTag; 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() .probe(&hint, mss, Default::default(), Default::default()) .ok()?; // Extract duration from track's num_frames and time_base let duration_secs = probed .tracks() .iter() .find(|t| t.num_frames.is_some() && t.time_base.is_some()) .and_then(|t| { let n_frames = t.num_frames?; let tb = t.time_base?; Some(n_frames as f64 * tb.numer.get() as f64 / tb.denom.get() as f64) }); let mut metadata = AudioMetadata { duration_secs, ..Default::default() }; { let mut meta = probed.metadata(); if let Some(rev) = meta.skip_to_latest() { for tag in &rev.media.tags { match &tag.std { Some(StandardTag::TrackTitle(value)) => { metadata.title = Some(value.to_string()); } Some(StandardTag::Artist(value)) => { metadata.artist = Some(value.to_string()); } Some(StandardTag::AlbumArtist(value)) => { metadata.artist = Some(value.to_string()); } Some(StandardTag::Album(value)) => { metadata.album = Some(value.to_string()); } Some(StandardTag::TrackNumber(value)) => { metadata.track_number = value.to_string().parse().ok(); } _ => {} } } } } if metadata.title.is_some() || metadata.artist.is_some() || metadata.album.is_some() || metadata.track_number.is_some() || metadata.duration_secs.is_some() { Some(metadata) } else { None } } #[derive(Debug, Clone)] pub struct Track { pub path: PathBuf, pub title: Option, pub artist: Option, pub album: Option, pub duration_secs: Option, pub is_stream: bool, } impl Track { pub fn from_path(path: PathBuf) -> Self { let path_str = path.to_string_lossy(); if path_str.starts_with("http://") || path_str.starts_with("https://") { return Track::from_url(&path_str); } 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()), duration_secs: metadata.and_then(|m| m.duration_secs), is_stream: false, } } pub fn from_url(url: &str) -> Self { let clean_url = url.split('?').next().unwrap_or(url); let url_path = Path::new(clean_url); let title = url_path .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) .or_else(|| { url_path .file_name() .and_then(|s| s.to_str()) .map(|s| s.to_string()) }) .unwrap_or_else(|| url.to_string()); Track { path: PathBuf::from(url), title: Some(title), artist: None, album: None, duration_secs: None, is_stream: true, } } 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 fn format(&self, format_str: &str) -> String { let has_metadata = self.artist.as_ref().is_some_and(|a| !a.is_empty()) || self.album.as_ref().is_some_and(|a| !a.is_empty()); if !has_metadata { return self.title.clone().unwrap_or_else(|| { self.path .file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string() }); } let artist = self.artist.clone().unwrap_or_default(); let album = 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() }); format_str .replace("$ARTIST", &artist) .replace("$ALBUM", &album) .replace("$TRACK", &title) } } pub struct Queue { tracks: Vec, position: usize, shuffle: ShuffleMode, shuffled_indices: Vec, original_tracks: 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(), original_tracks: Vec::new(), } } pub fn add_track(&mut self, path: PathBuf) { self.tracks.push(Track::from_path(path)); self.update_shuffle_indices(); } #[allow(dead_code)] pub fn push_track(&mut self, track: Track) { self.tracks.push(track); 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) { match mode { ShuffleMode::None => { self.shuffle = ShuffleMode::None; self.restore_from_original(); } ShuffleMode::All => { self.original_tracks = self.tracks.clone(); self.shuffle = ShuffleMode::All; self.update_shuffle_indices(); } ShuffleMode::Album => { self.original_tracks = self.tracks.clone(); self.shuffle = ShuffleMode::Album; self.update_shuffle_indices_album(); } ShuffleMode::Albums => { self.original_tracks = self.tracks.clone(); self.shuffle = ShuffleMode::Albums; self.update_shuffle_indices_albums(); } } } fn restore_from_original(&mut self) { if self.original_tracks.is_empty() { self.shuffled_indices.clear(); return; } let current_pos = self.position; // Find which tracks have been played (positions 0..=current_pos in shuffled order) let mut played_track_paths: Vec = Vec::new(); for i in 0..=current_pos { if let Some(&idx) = self.shuffled_indices.get(i) && let Some(track) = self.tracks.get(idx) { played_track_paths.push(track.path.clone()); } } // Keep the played tracks in their current order let mut new_tracks: Vec = Vec::new(); for path in &played_track_paths { if let Some(track) = self.tracks.iter().find(|t| &t.path == path) { new_tracks.push(track.clone()); } } // Add remaining tracks from original in original order for orig_track in &self.original_tracks { let is_played = played_track_paths.iter().any(|p| p == &orig_track.path); if !is_played { new_tracks.push(orig_track.clone()); } } self.tracks = new_tracks; self.position = current_pos; self.shuffled_indices.clear(); } fn update_shuffle_indices(&mut self) { let len = self.tracks.len(); self.shuffled_indices = (0..len).collect(); match self.shuffle { ShuffleMode::All => { 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); } } ShuffleMode::Album => { self.update_shuffle_indices_album(); } ShuffleMode::Albums => { self.update_shuffle_indices_albums(); } ShuffleMode::None => {} } } fn update_shuffle_indices_album(&mut self) { let len = self.tracks.len(); self.shuffled_indices = (0..len).collect(); if len > 1 { let mut rng = rand::rng(); let pos = self.position.min(len.saturating_sub(1)); if pos + 1 < len { self.shuffle_album_range(pos + 1..len, &mut rng); } } } fn update_shuffle_indices_albums(&mut self) { let len = self.tracks.len(); self.shuffled_indices = (0..len).collect(); if len <= 1 { return; } let pos = self.position.min(len.saturating_sub(1)); if pos + 1 >= len { return; } let range_start = pos + 1; let mut rng = rand::rng(); // Build album groups within the range, shuffling tracks within each group let mut groups: Vec> = Vec::new(); let mut current_group: Vec = Vec::new(); let mut current_album: Option<&str> = None; for i in range_start..len { let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); if current_group.is_empty() { current_group.push(i); current_album = album; } else if album != current_album { // Shuffle tracks within this album group before adding it if current_group.len() > 1 { current_group.shuffle(&mut rng); } groups.push(std::mem::take(&mut current_group)); current_group.push(i); current_album = album; } else { current_group.push(i); } } if !current_group.is_empty() { if current_group.len() > 1 { current_group.shuffle(&mut rng); } groups.push(current_group); } // Shuffle the album group order groups.shuffle(&mut rng); let mut new_indices: Vec = self.shuffled_indices[..=pos].to_vec(); for group in &groups { new_indices.extend(group); } self.shuffled_indices = new_indices; } fn shuffle_album_range(&mut self, range: std::ops::Range, rng: &mut impl rand::Rng) { if range.start >= range.end { return; } let mut album_ranges: Vec<(usize, usize)> = Vec::new(); let mut current_album: Option<&str> = None; let mut range_start: Option = None; for i in range.clone() { let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); match (current_album, range_start) { (None, None) => { current_album = album; range_start = Some(i); } (Some(_), Some(start)) if album != current_album => { album_ranges.push((start, i)); current_album = album; range_start = Some(i); } _ => {} } } if let Some(start) = range_start { album_ranges.push((start, range.end)); } for (start, end) in album_ranges { if start + 1 < end { self.shuffled_indices[start..end].shuffle(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.shuffle == ShuffleMode::Albums { 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 shuffle_initial(&mut self) { if self.position != 0 || self.tracks.len() <= 1 { return; } let mut rng = rand::rng(); match self.shuffle { ShuffleMode::All => { self.shuffled_indices.shuffle(&mut rng); } ShuffleMode::Album => { self.shuffle_album_range(0..self.tracks.len(), &mut rng); } ShuffleMode::Albums => { let mut groups: Vec> = Vec::new(); let mut current_group: Vec = Vec::new(); let mut current_album: Option<&str> = None; for i in 0..self.tracks.len() { let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); if current_group.is_empty() { current_group.push(i); current_album = album; } else if album != current_album { if current_group.len() > 1 { current_group.shuffle(&mut rng); } groups.push(std::mem::take(&mut current_group)); current_group.push(i); current_album = album; } else { current_group.push(i); } } if !current_group.is_empty() { if current_group.len() > 1 { current_group.shuffle(&mut rng); } groups.push(current_group); } groups.shuffle(&mut rng); self.shuffled_indices = groups.concat(); } ShuffleMode::None => {} } } pub fn clear(&mut self) { self.tracks.clear(); self.position = 0; self.shuffled_indices.clear(); } }