use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, de};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RepeatMode {
#[default]
Off,
All,
One,
}
impl<'de> Deserialize<'de> for RepeatMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct RepeatModeVisitor;
impl de::Visitor<'_> for RepeatModeVisitor {
type Value = RepeatMode;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a repeat mode string or legacy boolean")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(if value {
RepeatMode::All
} else {
RepeatMode::Off
})
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value.trim().to_ascii_lowercase().as_str() {
"off" | "none" | "false" => Ok(RepeatMode::Off),
"all" | "queue" | "on" | "true" => Ok(RepeatMode::All),
"one" | "track" | "single" => Ok(RepeatMode::One),
_ => Err(E::unknown_variant(
value,
&["off", "all", "one", "false", "true"],
)),
}
}
}
deserializer.deserialize_any(RepeatModeVisitor)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AdvanceTrigger {
EndOfStream,
UserNext,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueueAction {
Play(usize),
Restart(usize),
Stop,
}
pub fn sequential_advance(
current: Option<usize>,
queue_len: usize,
repeat: RepeatMode,
trigger: AdvanceTrigger,
) -> QueueAction {
if queue_len == 0 {
return QueueAction::Stop;
}
let Some(current) = current.filter(|index| *index < queue_len) else {
return QueueAction::Play(0);
};
if trigger == AdvanceTrigger::EndOfStream && repeat == RepeatMode::One {
return QueueAction::Restart(current);
}
if current + 1 < queue_len {
return QueueAction::Play(current + 1);
}
if repeat == RepeatMode::All {
if queue_len == 1 {
QueueAction::Restart(current)
} else {
QueueAction::Play(0)
}
} else {
QueueAction::Stop
}
}
pub fn sequential_previous(
current: Option<usize>,
queue_len: usize,
repeat: RepeatMode,
restart_current: bool,
) -> QueueAction {
if queue_len == 0 {
return QueueAction::Stop;
}
let Some(current) = current.filter(|index| *index < queue_len) else {
return QueueAction::Play(0);
};
if restart_current {
return QueueAction::Restart(current);
}
if current > 0 {
return QueueAction::Play(current - 1);
}
if repeat == RepeatMode::All && queue_len > 1 {
QueueAction::Play(queue_len - 1)
} else {
QueueAction::Restart(current)
}
}
#[derive(Clone, Debug)]
pub struct ShuffleOrder {
random_state: u64,
queue_len: usize,
bag: Vec<usize>,
history: Vec<usize>,
future: Vec<usize>,
}
impl Default for ShuffleOrder {
fn default() -> Self {
Self::new(0x4b4e_4f54_544d_5553)
}
}
impl ShuffleOrder {
pub fn new(seed: u64) -> Self {
Self {
random_state: seed,
queue_len: 0,
bag: Vec::new(),
history: Vec::new(),
future: Vec::new(),
}
}
pub fn reset(&mut self, queue_len: usize, current: Option<usize>) {
let current = current.filter(|index| *index < queue_len);
self.queue_len = queue_len;
self.history.clear();
self.future.clear();
if let Some(current) = current {
self.history.push(current);
}
self.refill_initial_bag(current);
}
pub fn record_selection(&mut self, index: usize, queue_len: usize) {
if index >= queue_len {
return;
}
self.ensure_queue(queue_len, Some(index));
self.align_current(Some(index));
}
pub fn advance(
&mut self,
current: Option<usize>,
queue_len: usize,
repeat: RepeatMode,
trigger: AdvanceTrigger,
) -> QueueAction {
if queue_len == 0 {
self.reset(0, None);
return QueueAction::Stop;
}
let current = current.filter(|index| *index < queue_len);
self.ensure_queue(queue_len, current);
self.align_current(current);
if trigger == AdvanceTrigger::EndOfStream && repeat == RepeatMode::One {
return current.map_or(QueueAction::Stop, QueueAction::Restart);
}
if let Some(index) = self.future.pop() {
self.push_history(index);
return action_for_index(current, index);
}
if let Some(index) = self.bag.pop() {
self.push_history(index);
return action_for_index(current, index);
}
if repeat != RepeatMode::All {
return QueueAction::Stop;
}
self.refill_repeat_bag(current);
let Some(index) = self.bag.pop() else {
return current.map_or(QueueAction::Stop, QueueAction::Restart);
};
self.push_history(index);
action_for_index(current, index)
}
pub fn previous(
&mut self,
current: Option<usize>,
queue_len: usize,
restart_current: bool,
) -> QueueAction {
if queue_len == 0 {
self.reset(0, None);
return QueueAction::Stop;
}
let current = current.filter(|index| *index < queue_len);
self.ensure_queue(queue_len, current);
self.align_current(current);
let Some(current) = current else {
let Some(index) = self.bag.pop() else {
return QueueAction::Play(0);
};
self.push_history(index);
return QueueAction::Play(index);
};
if restart_current {
return QueueAction::Restart(current);
}
if self.history.len() < 2 {
return QueueAction::Restart(current);
}
if let Some(leaving) = self.history.pop() {
self.future.push(leaving);
}
QueueAction::Play(*self.history.last().expect("history has a previous item"))
}
#[cfg(test)]
pub fn remaining(&self) -> usize {
self.bag.len()
}
#[cfg(test)]
pub fn history(&self) -> &[usize] {
&self.history
}
#[cfg(test)]
pub fn future_len(&self) -> usize {
self.future.len()
}
fn ensure_queue(&mut self, queue_len: usize, current: Option<usize>) {
if self.queue_len != queue_len {
self.reset(queue_len, current);
}
}
fn align_current(&mut self, current: Option<usize>) {
match current {
Some(current) if self.history.last().copied() == Some(current) => {
remove_value(&mut self.bag, current);
}
Some(current) => {
self.history.push(current);
self.future.clear();
remove_value(&mut self.bag, current);
}
None if !self.history.is_empty() || !self.future.is_empty() => {
self.history.clear();
self.future.clear();
self.refill_initial_bag(None);
}
None => {}
}
}
fn push_history(&mut self, index: usize) {
if self.history.last().copied() != Some(index) {
self.history.push(index);
}
}
fn refill_initial_bag(&mut self, current: Option<usize>) {
self.bag = (0..self.queue_len)
.filter(|index| Some(*index) != current)
.collect();
self.shuffle_bag();
}
fn refill_repeat_bag(&mut self, current: Option<usize>) {
self.bag = (0..self.queue_len).collect();
self.shuffle_bag();
if self.bag.len() > 1
&& let Some(current) = current
&& self.bag.last().copied() == Some(current)
&& let Some(other) = self.bag.iter().position(|index| *index != current)
{
let last = self.bag.len() - 1;
self.bag.swap(last, other);
}
}
fn shuffle_bag(&mut self) {
for upper in (1..self.bag.len()).rev() {
let selected = (self.next_random() % (upper as u64 + 1)) as usize;
self.bag.swap(upper, selected);
}
}
fn next_random(&mut self) -> u64 {
self.random_state = self.random_state.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut value = self.random_state;
value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
}
fn action_for_index(current: Option<usize>, index: usize) -> QueueAction {
if current == Some(index) {
QueueAction::Restart(index)
} else {
QueueAction::Play(index)
}
}
fn remove_value(values: &mut Vec<usize>, target: usize) {
if let Some(position) = values.iter().position(|value| *value == target) {
values.swap_remove(position);
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
fn played_index(action: QueueAction) -> Option<usize> {
match action {
QueueAction::Play(index) | QueueAction::Restart(index) => Some(index),
QueueAction::Stop => None,
}
}
#[test]
fn repeat_mode_serializes_to_stable_strings() {
assert_eq!(serde_json::to_string(&RepeatMode::Off).unwrap(), "\"off\"");
assert_eq!(serde_json::to_string(&RepeatMode::All).unwrap(), "\"all\"");
assert_eq!(serde_json::to_string(&RepeatMode::One).unwrap(), "\"one\"");
}
#[test]
fn repeat_mode_accepts_legacy_booleans_and_aliases() {
assert_eq!(
serde_json::from_str::<RepeatMode>("false").unwrap(),
RepeatMode::Off
);
assert_eq!(
serde_json::from_str::<RepeatMode>("true").unwrap(),
RepeatMode::All
);
assert_eq!(
serde_json::from_str::<RepeatMode>("\"queue\"").unwrap(),
RepeatMode::All
);
assert_eq!(
serde_json::from_str::<RepeatMode>("\"TRACK\"").unwrap(),
RepeatMode::One
);
}
#[test]
fn repeat_mode_defaults_to_off() {
#[derive(Deserialize)]
struct StoredSettings {
#[serde(default)]
repeat: RepeatMode,
}
assert_eq!(
serde_json::from_str::<StoredSettings>("{}").unwrap().repeat,
RepeatMode::Off
);
}
#[test]
fn sequential_advance_handles_empty_and_unselected_queues() {
assert_eq!(
sequential_advance(None, 0, RepeatMode::Off, AdvanceTrigger::UserNext),
QueueAction::Stop
);
assert_eq!(
sequential_advance(None, 3, RepeatMode::Off, AdvanceTrigger::UserNext),
QueueAction::Play(0)
);
assert_eq!(
sequential_advance(Some(99), 3, RepeatMode::Off, AdvanceTrigger::UserNext),
QueueAction::Play(0)
);
}
#[test]
fn sequential_advance_obeys_all_repeat_modes() {
assert_eq!(
sequential_advance(Some(0), 3, RepeatMode::Off, AdvanceTrigger::UserNext),
QueueAction::Play(1)
);
assert_eq!(
sequential_advance(Some(2), 3, RepeatMode::Off, AdvanceTrigger::EndOfStream),
QueueAction::Stop
);
assert_eq!(
sequential_advance(Some(2), 3, RepeatMode::All, AdvanceTrigger::EndOfStream),
QueueAction::Play(0)
);
assert_eq!(
sequential_advance(Some(1), 3, RepeatMode::One, AdvanceTrigger::EndOfStream),
QueueAction::Restart(1)
);
assert_eq!(
sequential_advance(Some(1), 3, RepeatMode::One, AdvanceTrigger::UserNext),
QueueAction::Play(2)
);
}
#[test]
fn a_single_sequential_track_restarts_only_when_policy_calls_for_it() {
assert_eq!(
sequential_advance(Some(0), 1, RepeatMode::All, AdvanceTrigger::EndOfStream),
QueueAction::Restart(0)
);
assert_eq!(
sequential_advance(Some(0), 1, RepeatMode::One, AdvanceTrigger::EndOfStream),
QueueAction::Restart(0)
);
assert_eq!(
sequential_advance(Some(0), 1, RepeatMode::One, AdvanceTrigger::UserNext),
QueueAction::Stop
);
}
#[test]
fn sequential_previous_restarts_or_wraps_at_boundaries() {
assert_eq!(
sequential_previous(None, 3, RepeatMode::Off, false),
QueueAction::Play(0)
);
assert_eq!(
sequential_previous(Some(2), 3, RepeatMode::Off, true),
QueueAction::Restart(2)
);
assert_eq!(
sequential_previous(Some(2), 3, RepeatMode::Off, false),
QueueAction::Play(1)
);
assert_eq!(
sequential_previous(Some(0), 3, RepeatMode::Off, false),
QueueAction::Restart(0)
);
assert_eq!(
sequential_previous(Some(0), 3, RepeatMode::All, false),
QueueAction::Play(2)
);
}
#[test]
fn seeded_shuffle_is_deterministic() {
let collect = |seed| {
let mut shuffle = ShuffleOrder::new(seed);
shuffle.reset(8, Some(0));
let mut current = Some(0);
let mut order = Vec::new();
while let Some(index) =
played_index(shuffle.advance(current, 8, RepeatMode::Off, AdvanceTrigger::UserNext))
{
order.push(index);
current = Some(index);
}
order
};
assert_eq!(collect(42), collect(42));
assert_ne!(collect(42), collect(43));
}
#[test]
fn shuffle_plays_every_other_item_once_before_stopping() {
let mut shuffle = ShuffleOrder::new(7);
shuffle.reset(6, Some(2));
let mut current = Some(2);
let mut played = Vec::new();
loop {
let action = shuffle.advance(current, 6, RepeatMode::Off, AdvanceTrigger::EndOfStream);
let Some(index) = played_index(action) else {
break;
};
played.push(index);
current = Some(index);
}
assert_eq!(played.len(), 5);
assert!(!played.contains(&2));
assert_eq!(played.iter().copied().collect::<HashSet<_>>().len(), 5);
assert_eq!(shuffle.remaining(), 0);
}
#[test]
fn repeat_all_starts_complete_new_shuffle_cycles_without_adjacent_repeat() {
let mut shuffle = ShuffleOrder::new(19);
shuffle.reset(4, Some(0));
let mut current = Some(0);
for _ in 0..3 {
let next = played_index(shuffle.advance(
current,
4,
RepeatMode::All,
AdvanceTrigger::EndOfStream,
))
.unwrap();
current = Some(next);
}
let cycle_end = current.unwrap();
let mut next_cycle = Vec::new();
for _ in 0..4 {
let next = played_index(shuffle.advance(
current,
4,
RepeatMode::All,
AdvanceTrigger::EndOfStream,
))
.unwrap();
next_cycle.push(next);
current = Some(next);
}
assert_ne!(next_cycle[0], cycle_end);
assert_eq!(next_cycle.iter().copied().collect::<HashSet<_>>().len(), 4);
}
#[test]
fn repeat_one_restarts_without_consuming_the_shuffle_bag() {
let mut shuffle = ShuffleOrder::new(99);
shuffle.reset(5, Some(3));
let remaining = shuffle.remaining();
assert_eq!(
shuffle.advance(Some(3), 5, RepeatMode::One, AdvanceTrigger::EndOfStream,),
QueueAction::Restart(3)
);
assert_eq!(shuffle.remaining(), remaining);
assert!(matches!(
shuffle.advance(Some(3), 5, RepeatMode::One, AdvanceTrigger::UserNext,),
QueueAction::Play(_)
));
}
#[test]
fn shuffle_previous_and_next_walk_real_history() {
let mut shuffle = ShuffleOrder::new(123);
shuffle.reset(6, Some(0));
let first =
played_index(shuffle.advance(Some(0), 6, RepeatMode::Off, AdvanceTrigger::UserNext))
.unwrap();
let second = played_index(shuffle.advance(
Some(first),
6,
RepeatMode::Off,
AdvanceTrigger::UserNext,
))
.unwrap();
assert_eq!(
shuffle.previous(Some(second), 6, false),
QueueAction::Play(first)
);
assert_eq!(shuffle.future_len(), 1);
assert_eq!(
shuffle.previous(Some(first), 6, false),
QueueAction::Play(0)
);
assert_eq!(shuffle.future_len(), 2);
assert_eq!(
shuffle.advance(Some(0), 6, RepeatMode::Off, AdvanceTrigger::UserNext,),
QueueAction::Play(first)
);
assert_eq!(
shuffle.advance(Some(first), 6, RepeatMode::Off, AdvanceTrigger::UserNext,),
QueueAction::Play(second)
);
}
#[test]
fn direct_selection_branches_history_and_cannot_immediately_repeat() {
let mut shuffle = ShuffleOrder::new(55);
shuffle.reset(5, Some(0));
let first =
played_index(shuffle.advance(Some(0), 5, RepeatMode::Off, AdvanceTrigger::UserNext))
.unwrap();
assert_eq!(
shuffle.previous(Some(first), 5, false),
QueueAction::Play(0)
);
assert_eq!(shuffle.future_len(), 1);
shuffle.record_selection(4, 5);
assert_eq!(shuffle.history().last(), Some(&4));
assert_eq!(shuffle.future_len(), 0);
assert_ne!(
played_index(shuffle.advance(Some(4), 5, RepeatMode::Off, AdvanceTrigger::UserNext,)),
Some(4)
);
}
#[test]
fn queue_length_changes_discard_stale_shuffle_indices() {
let mut shuffle = ShuffleOrder::new(88);
shuffle.reset(10, Some(9));
let action = shuffle.advance(Some(1), 3, RepeatMode::Off, AdvanceTrigger::UserNext);
assert!(played_index(action).is_some_and(|index| index < 3));
assert!(shuffle.history().iter().all(|index| *index < 3));
}
#[test]
fn single_item_shuffle_has_clear_repeat_semantics() {
let mut shuffle = ShuffleOrder::new(1);
shuffle.reset(1, Some(0));
assert_eq!(
shuffle.advance(Some(0), 1, RepeatMode::Off, AdvanceTrigger::EndOfStream,),
QueueAction::Stop
);
assert_eq!(
shuffle.advance(Some(0), 1, RepeatMode::All, AdvanceTrigger::EndOfStream,),
QueueAction::Restart(0)
);
assert_eq!(shuffle.previous(Some(0), 1, false), QueueAction::Restart(0));
}
}