jellytau/src-tauri/src/player/sleep_timer.rs
Duncan Tourolle ada3ed64ab Workstream E (backend): wire tauri-specta — annotate commands, derive Type, generate-ready Builder
- Add #[specta::specta] to all 201 #[tauri::command] functions.
- Derive specta::Type on all IPC DTOs (repository/types, settings, player/storage/
  download command DTOs, player enums, jellyfin SessionInfo/NowPlayingItem/PlayState,
  ThumbnailCacheStats, DownloadInfo, CacheConfig, etc.).
- Replace tauri::generate_handler! with a tauri_specta::Builder + collect_commands!
  in lib.rs (exports bindings.ts in debug builds).

Two contract changes required by specta constraints (frontend migration follows):
- specta caps command arity at 10 args: download_item_and_start / download_item /
  download_video now take a single request struct (params bundled, body unchanged
  via destructuring).
- specta can't parse split serde rename_all: SessionInfo/NowPlayingItem/PlayState
  switched to rename_all = "PascalCase" (Jellyfin deserialization preserved; these
  now serialize PascalCase to the frontend).

cargo check --lib is clean (0 errors). Frontend migration to bindings.ts is the next step.
2026-06-20 18:20:25 +02:00

152 lines
4.3 KiB
Rust

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);
}
}