use serde::{Deserialize, Serialize}; /// Sleep timer mode - determines when playback should stop /// TRACES: UR-026 | DR-029 #[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum SleepTimerMode { /// Timer is off Off, /// Stop after a specific time duration Time { #[serde(rename = "endTime")] end_time: i64, // Unix timestamp in milliseconds }, /// Stop at the end of current track EndOfTrack, /// Stop after N more episodes complete (TV episodes only, not audio tracks) Episodes { remaining: u32 }, } /// Sleep timer state #[derive(specta::Type, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SleepTimerState { pub mode: SleepTimerMode, pub remaining_seconds: u32, } impl Default for SleepTimerState { fn default() -> Self { Self { mode: SleepTimerMode::Off, remaining_seconds: 0, } } } impl SleepTimerState { /// Check if timer is active pub fn is_active(&self) -> bool { !matches!(self.mode, SleepTimerMode::Off) } /// Update remaining seconds for time-based timer pub fn update_remaining_seconds(&mut self) { if let SleepTimerMode::Time { end_time } = self.mode { let now = chrono::Utc::now().timestamp_millis(); self.remaining_seconds = if now >= end_time { 0 } else { ((end_time - now) / 1000).max(0) as u32 }; } } /// Decrement episode counter, returns true if should stop /// Only counts TV episodes, not audio tracks pub fn decrement_episode(&mut self) -> bool { match &mut self.mode { SleepTimerMode::Episodes { remaining } => { if *remaining <= 1 { self.mode = SleepTimerMode::Off; self.remaining_seconds = 0; true } else { *remaining -= 1; false } } _ => false, } } /// Cancel the timer pub fn cancel(&mut self) { self.mode = SleepTimerMode::Off; self.remaining_seconds = 0; } } // TRACES: UR-026 | DR-029 | UT-012 #[cfg(test)] mod tests { use super::*; #[test] fn test_sleep_timer_episode_decrement() { let mut timer = SleepTimerState { mode: SleepTimerMode::Episodes { remaining: 2 }, remaining_seconds: 0, }; assert!(!timer.decrement_episode()); assert!(matches!( timer.mode, SleepTimerMode::Episodes { remaining: 1 } )); assert!(timer.decrement_episode()); assert!(matches!(timer.mode, SleepTimerMode::Off)); } #[test] fn test_sleep_timer_is_active() { let timer = SleepTimerState::default(); assert!(!timer.is_active()); let timer = SleepTimerState { mode: SleepTimerMode::Time { end_time: 0 }, remaining_seconds: 0, }; assert!(timer.is_active()); let timer = SleepTimerState { mode: SleepTimerMode::EndOfTrack, remaining_seconds: 0, }; assert!(timer.is_active()); let timer = SleepTimerState { mode: SleepTimerMode::Episodes { remaining: 3 }, remaining_seconds: 0, }; assert!(timer.is_active()); } #[test] fn test_sleep_timer_cancel() { let mut timer = SleepTimerState { mode: SleepTimerMode::Episodes { remaining: 5 }, remaining_seconds: 300, }; timer.cancel(); assert!(matches!(timer.mode, SleepTimerMode::Off)); assert_eq!(timer.remaining_seconds, 0); } #[test] fn test_update_remaining_seconds() { let end_time = chrono::Utc::now().timestamp_millis() + 30000; // 30 seconds from now let mut timer = SleepTimerState { mode: SleepTimerMode::Time { end_time }, remaining_seconds: 0, }; timer.update_remaining_seconds(); // Should be approximately 30 seconds (allow for small time difference) assert!(timer.remaining_seconds >= 29 && timer.remaining_seconds <= 31); } }