use camino::Utf8PathBuf;
#[derive(Debug, Clone)]
pub struct TrackInfo {
pub path: Utf8PathBuf,
pub title: String,
pub artist: Option<String>,
pub album_artist: Option<String>,
pub album: Option<String>,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub year: Option<u32>,
pub genre: Option<String>,
pub duration_secs: Option<f64>,
pub sample_rate_hz: Option<u32>,
pub bit_depth: Option<u8>,
pub bitrate_kbps: Option<u32>,
pub channels: Option<u8>,
}
impl TrackInfo {
pub fn from_path(path: Utf8PathBuf) -> Self {
let title = path
.file_stem()
.unwrap_or(path.file_name().unwrap_or("unknown"))
.to_owned();
Self {
path,
title,
artist: None,
album_artist: None,
album: None,
track_number: None,
disc_number: None,
year: None,
genre: None,
duration_secs: None,
sample_rate_hz: None,
bit_depth: None,
bitrate_kbps: None,
channels: None,
}
}
pub fn with_tags(mut self) -> Self {
use lofty::prelude::*;
let Ok(tagged) = lofty::read_from_path(self.path.as_std_path()) else {
return self;
};
if let Some(tag) = tagged.primary_tag() {
if let Some(a) = tag.artist() {
self.artist = Some(a.into_owned());
}
if let Some(aa) = tag.get_string(&lofty::tag::ItemKey::AlbumArtist) {
self.album_artist = Some(aa.to_owned());
}
if let Some(al) = tag.album() {
self.album = Some(al.into_owned());
}
if let Some(title) = tag.title() {
let t = title.into_owned();
if !t.is_empty() {
self.title = t;
}
}
self.track_number = tag.track();
self.disc_number = tag.disk();
self.year = tag.year();
if let Some(g) = tag.genre() {
self.genre = Some(g.into_owned());
}
}
let props = tagged.properties();
let secs = props.duration().as_secs_f64();
if secs > 0.0 {
self.duration_secs = Some(secs);
}
self.sample_rate_hz = props.sample_rate();
self.bit_depth = props.bit_depth();
self.bitrate_kbps = props.audio_bitrate();
self.channels = props.channels();
self
}
}
pub struct Queue {
tracks: Vec<TrackInfo>,
cursor: usize,
pub repeat: RepeatMode,
pub shuffle: bool,
shuffle_order: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RepeatMode {
#[default]
Off,
All,
One,
}
impl RepeatMode {
pub fn next(self) -> Self {
match self {
Self::Off => Self::All,
Self::All => Self::One,
Self::One => Self::Off,
}
}
pub fn label(self) -> &'static str {
match self {
Self::Off => "off",
Self::All => "all",
Self::One => "one",
}
}
}
impl Default for Queue {
fn default() -> Self {
Self::new()
}
}
impl Queue {
pub fn new() -> Self {
Self {
tracks: Vec::new(),
cursor: 0,
repeat: RepeatMode::Off,
shuffle: false,
shuffle_order: Vec::new(),
}
}
pub fn set(&mut self, tracks: Vec<TrackInfo>) {
self.tracks = tracks;
self.cursor = 0;
self.rebuild_shuffle();
}
pub fn push(&mut self, track: TrackInfo) {
self.tracks.push(track);
self.rebuild_shuffle();
}
pub fn clear(&mut self) {
self.tracks.clear();
self.cursor = 0;
self.shuffle_order.clear();
}
pub fn is_empty(&self) -> bool {
self.tracks.is_empty()
}
pub fn len(&self) -> usize {
self.tracks.len()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn current(&self) -> Option<&TrackInfo> {
let idx = self.effective_idx(self.cursor)?;
self.tracks.get(idx)
}
pub fn peek_next(&self) -> Option<&TrackInfo> {
if self.tracks.is_empty() {
return None;
}
let next_cursor = match self.repeat {
RepeatMode::One => self.cursor,
RepeatMode::All => (self.cursor + 1) % self.tracks.len(),
RepeatMode::Off => {
if self.cursor + 1 < self.tracks.len() {
self.cursor + 1
} else {
return None;
}
}
};
self.tracks.get(self.effective_idx(next_cursor)?)
}
pub fn tracks(&self) -> &[TrackInfo] {
&self.tracks
}
pub fn advance(&mut self) -> bool {
if self.tracks.is_empty() {
return false;
}
match self.repeat {
RepeatMode::One => true, RepeatMode::All => {
self.cursor = (self.cursor + 1) % self.tracks.len();
true
}
RepeatMode::Off => {
if self.cursor + 1 < self.tracks.len() {
self.cursor += 1;
true
} else {
false
}
}
}
}
pub fn prev(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn jump_to(&mut self, idx: usize) {
if idx < self.tracks.len() {
self.cursor = idx;
}
}
pub fn toggle_shuffle(&mut self) {
self.shuffle = !self.shuffle;
self.rebuild_shuffle();
}
pub fn update_at(&mut self, phys_idx: usize, track: TrackInfo) {
if let Some(t) = self.tracks.get_mut(phys_idx) {
*t = track;
}
}
pub fn current_phys_idx(&self) -> Option<usize> {
self.effective_idx(self.cursor)
}
fn effective_idx(&self, cursor: usize) -> Option<usize> {
if self.tracks.is_empty() {
return None;
}
if self.shuffle && !self.shuffle_order.is_empty() {
self.shuffle_order.get(cursor).copied()
} else {
Some(cursor.min(self.tracks.len() - 1))
}
}
fn rebuild_shuffle(&mut self) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
if !self.shuffle || self.tracks.is_empty() {
self.shuffle_order.clear();
return;
}
let mut hasher = DefaultHasher::new();
self.tracks.len().hash(&mut hasher);
if let Some(t) = self.tracks.first() {
t.path.hash(&mut hasher);
}
let seed = hasher.finish();
let n = self.tracks.len();
let mut order: Vec<usize> = (0..n).collect();
let mut s = seed;
for i in (1..n).rev() {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
let j = (s as usize) % (i + 1);
order.swap(i, j);
}
self.shuffle_order = order;
}
}