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}