- 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.
152 lines
4.3 KiB
Rust
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);
|
|
}
|
|
}
|