Skip to main content

jellytau_lib/player/
autoplay.rs

1// Autoplay decision logic
2// TRACES: UR-023, UR-026 | DR-047, DR-048, DR-029
3use crate::repository::types::MediaItem;
4use serde::{Deserialize, Serialize};
5
6/// Autoplay decision result - determines what happens after playback ends
7#[derive(specta::Type, Debug, Clone, Serialize)]
8#[serde(tag = "action", rename_all = "camelCase")]
9// `ShowNextEpisodePopup` carries two `MediaItem`s, so it dwarfs the unit
10// variants. Boxing them is not worth it here: this enum is constructed once per
11// end-of-item (never in a hot loop or a large collection), and it is an IPC type
12// — the indirection would have to stay invisible to serde/specta while every
13// match arm gained a deref, for no measurable gain.
14#[allow(clippy::large_enum_variant)]
15pub enum AutoplayDecision {
16    /// Stop playback (no next item or timer expired)
17    Stop,
18    /// Advance to next track in queue (for audio/movies)
19    AdvanceToNext,
20    /// The stream ended well short of the item's runtime — the connection
21    /// dropped, not the media. Re-open the same stream at `position` instead of
22    /// running any end-of-item logic (UR-040).
23    ResumeStream { position: f64 },
24    /// Show next episode popup with countdown
25    ShowNextEpisodePopup {
26        current_episode: MediaItem,
27        next_episode: MediaItem,
28        countdown_seconds: u32,
29        auto_advance: bool,
30    },
31}
32
33/// Autoplay settings (controls next episode behavior)
34#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct AutoplaySettings {
37    /// Whether autoplay is enabled for next episodes
38    pub enabled: bool,
39    /// Countdown duration in seconds before auto-playing next episode
40    pub countdown_seconds: u32,
41    /// Maximum number of episodes to auto-play consecutively (0 = unlimited)
42    #[serde(default)]
43    pub max_episodes: u32,
44}
45
46impl Default for AutoplaySettings {
47    fn default() -> Self {
48        Self {
49            enabled: true,
50            countdown_seconds: 10,
51            max_episodes: 0,
52        }
53    }
54}
55
56impl AutoplaySettings {
57    /// Validate and clamp countdown seconds to reasonable range (5-30 seconds)
58    pub fn with_validated_countdown(mut self) -> Self {
59        self.countdown_seconds = self.countdown_seconds.clamp(5, 30);
60        self
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_autoplay_settings_defaults() {
70        let settings = AutoplaySettings::default();
71        assert!(settings.enabled);
72        assert_eq!(settings.countdown_seconds, 10);
73        assert_eq!(settings.max_episodes, 0);
74    }
75
76    #[test]
77    fn test_autoplay_settings_backward_compat() {
78        // Deserialize old JSON without max_episodes field
79        let json = r#"{"enabled":true,"countdownSeconds":15}"#;
80        let settings: AutoplaySettings = serde_json::from_str(json).unwrap();
81        assert!(settings.enabled);
82        assert_eq!(settings.countdown_seconds, 15);
83        assert_eq!(settings.max_episodes, 0); // defaults to 0 (unlimited)
84    }
85
86    #[test]
87    fn test_autoplay_settings_with_max_episodes() {
88        let json = r#"{"enabled":true,"countdownSeconds":10,"maxEpisodes":5}"#;
89        let settings: AutoplaySettings = serde_json::from_str(json).unwrap();
90        assert_eq!(settings.max_episodes, 5);
91    }
92
93    #[test]
94    fn test_countdown_validation() {
95        let settings = AutoplaySettings {
96            enabled: true,
97            countdown_seconds: 2, // Too short
98            max_episodes: 0,
99        }
100        .with_validated_countdown();
101        assert_eq!(settings.countdown_seconds, 5); // Clamped to min
102
103        let settings = AutoplaySettings {
104            enabled: true,
105            countdown_seconds: 60, // Too long
106            max_episodes: 0,
107        }
108        .with_validated_countdown();
109        assert_eq!(settings.countdown_seconds, 30); // Clamped to max
110
111        let settings = AutoplaySettings {
112            enabled: true,
113            countdown_seconds: 15, // Valid
114            max_episodes: 0,
115        }
116        .with_validated_countdown();
117        assert_eq!(settings.countdown_seconds, 15); // Unchanged
118    }
119}