// Autoplay decision logic // TRACES: UR-023, UR-026 | DR-047, DR-048, DR-029 use crate::repository::types::MediaItem; use serde::{Deserialize, Serialize}; /// Autoplay decision result - determines what happens after playback ends #[derive(specta::Type, Debug, Clone, Serialize)] #[serde(tag = "action", rename_all = "camelCase")] pub enum AutoplayDecision { /// Stop playback (no next item or timer expired) Stop, /// Advance to next track in queue (for audio/movies) AdvanceToNext, /// The stream ended well short of the item's runtime — the connection /// dropped, not the media. Re-open the same stream at `position` instead of /// running any end-of-item logic (UR-040). ResumeStream { position: f64 }, /// Show next episode popup with countdown ShowNextEpisodePopup { current_episode: MediaItem, next_episode: MediaItem, countdown_seconds: u32, auto_advance: bool, }, } /// Autoplay settings (controls next episode behavior) #[derive(specta::Type, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutoplaySettings { /// Whether autoplay is enabled for next episodes pub enabled: bool, /// Countdown duration in seconds before auto-playing next episode pub countdown_seconds: u32, /// Maximum number of episodes to auto-play consecutively (0 = unlimited) #[serde(default)] pub max_episodes: u32, } impl Default for AutoplaySettings { fn default() -> Self { Self { enabled: true, countdown_seconds: 10, max_episodes: 0, } } } impl AutoplaySettings { /// Validate and clamp countdown seconds to reasonable range (5-30 seconds) pub fn with_validated_countdown(mut self) -> Self { self.countdown_seconds = self.countdown_seconds.clamp(5, 30); self } } #[cfg(test)] mod tests { use super::*; #[test] fn test_autoplay_settings_defaults() { let settings = AutoplaySettings::default(); assert!(settings.enabled); assert_eq!(settings.countdown_seconds, 10); assert_eq!(settings.max_episodes, 0); } #[test] fn test_autoplay_settings_backward_compat() { // Deserialize old JSON without max_episodes field let json = r#"{"enabled":true,"countdownSeconds":15}"#; let settings: AutoplaySettings = serde_json::from_str(json).unwrap(); assert!(settings.enabled); assert_eq!(settings.countdown_seconds, 15); assert_eq!(settings.max_episodes, 0); // defaults to 0 (unlimited) } #[test] fn test_autoplay_settings_with_max_episodes() { let json = r#"{"enabled":true,"countdownSeconds":10,"maxEpisodes":5}"#; let settings: AutoplaySettings = serde_json::from_str(json).unwrap(); assert_eq!(settings.max_episodes, 5); } #[test] fn test_countdown_validation() { let settings = AutoplaySettings { enabled: true, countdown_seconds: 2, // Too short max_episodes: 0, } .with_validated_countdown(); assert_eq!(settings.countdown_seconds, 5); // Clamped to min let settings = AutoplaySettings { enabled: true, countdown_seconds: 60, // Too long max_episodes: 0, } .with_validated_countdown(); assert_eq!(settings.countdown_seconds, 30); // Clamped to max let settings = AutoplaySettings { enabled: true, countdown_seconds: 15, // Valid max_episodes: 0, } .with_validated_countdown(); assert_eq!(settings.countdown_seconds, 15); // Unchanged } }