bckplayer 0.1.0

A cross-platform CLI media player interface for VLC
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::config_manager::ConfigModule;

/// YouTube configuration module
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YoutubeConfig {
    pub enabled: bool,
    pub cache_directory: PathBuf,
    pub max_cache_size_mb: u64,
    pub max_cache_age_days: u64,
    pub preferred_quality: String,
    pub audio_only: bool,
    pub ytdlp_path: Option<PathBuf>,
    pub concurrent_downloads: u32,
    pub retry_attempts: u32,
}

impl ConfigModule for YoutubeConfig {
    fn module_name() -> &'static str {
        "youtube"
    }

    fn default() -> Self {
        let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        let cache_dir = home_dir.join(".bckplayer").join("youtube_cache");

        Self {
            enabled: true,
            cache_directory: cache_dir,
            max_cache_size_mb: 1024, // 1GB
            max_cache_age_days: 30,
            preferred_quality: "best[height<=720]".to_string(),
            audio_only: false,
            ytdlp_path: None, // Auto-detect
            concurrent_downloads: 2,
            retry_attempts: 3,
        }
    }

    fn validate(&self) -> anyhow::Result<()> {
        if self.max_cache_size_mb == 0 {
            return Err(anyhow::anyhow!("Cache size must be greater than 0"));
        }
        // Impose a reasonable upper bound (100 GB) to prevent runaway cache sizes
        if self.max_cache_size_mb > 100_000 {
            return Err(anyhow::anyhow!(
                "Cache size cannot exceed 100000 MB (100 GB)"
            ));
        }
        if self.max_cache_age_days == 0 {
            return Err(anyhow::anyhow!("Cache age must be greater than 0"));
        }
        if self.concurrent_downloads == 0 {
            return Err(anyhow::anyhow!("Concurrent downloads must be greater than 0"));
        }
        if self.retry_attempts > 10 {
            return Err(anyhow::anyhow!("Retry attempts cannot exceed 10"));
        }
        Ok(())
    }

    fn on_load(&mut self) -> anyhow::Result<()> {
        // Ensure cache directory exists
        if !self.cache_directory.exists() {
            std::fs::create_dir_all(&self.cache_directory)?;
        }
        Ok(())
    }
}

/// Network configuration module
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
    pub timeout_seconds: u64,
    pub retry_attempts: u32,
    pub user_agent: String,
    pub proxy_url: Option<String>,
    pub max_redirects: u32,
}

impl ConfigModule for NetworkConfig {
    fn module_name() -> &'static str {
        "network"
    }

    fn default() -> Self {
        Self {
            timeout_seconds: 30,
            retry_attempts: 3,
            user_agent: "bckplayer/0.1.0".to_string(),
            proxy_url: None,
            max_redirects: 5,
        }
    }

    fn validate(&self) -> anyhow::Result<()> {
        if self.timeout_seconds == 0 {
            return Err(anyhow::anyhow!("Timeout must be greater than 0"));
        }
        if self.retry_attempts > 10 {
            return Err(anyhow::anyhow!("Retry attempts cannot exceed 10"));
        }
        if self.max_redirects > 20 {
            return Err(anyhow::anyhow!("Max redirects cannot exceed 20"));
        }
        Ok(())
    }
}

/// Appearance configuration module
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppearanceConfig {
    pub theme: String,
    pub font_size: u16,
    pub show_fps: bool,
    pub show_logs: bool,
    pub log_level: String,
    pub progress_bar_style: String,
}

impl ConfigModule for AppearanceConfig {
    fn module_name() -> &'static str {
        "appearance"
    }

    fn default() -> Self {
        Self {
            theme: "dark".to_string(),
            font_size: 14,
            show_fps: false,
            show_logs: true,
            log_level: "info".to_string(),
            progress_bar_style: "simple".to_string(),
        }
    }

    fn validate(&self) -> anyhow::Result<()> {
        let valid_themes = ["dark", "light", "auto"];
        if !valid_themes.contains(&self.theme.as_str()) {
            return Err(anyhow::anyhow!("Invalid theme. Must be one of: {:?}", valid_themes));
        }
        
        if !(8..=72).contains(&self.font_size) {
            return Err(anyhow::anyhow!("Font size must be between 8 and 72"));
        }

        let valid_log_levels = ["trace", "debug", "info", "warn", "error"];
        if !valid_log_levels.contains(&self.log_level.as_str()) {
            return Err(anyhow::anyhow!("Invalid log level. Must be one of: {:?}", valid_log_levels));
        }

        Ok(())
    }
}

/// Media configuration module
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaConfig {
    pub auto_load_subtitles: bool,
    pub subtitle_languages: Vec<String>,
    pub audio_languages: Vec<String>,
    pub hardware_decoding: bool,
    pub buffer_size_seconds: f64,
    pub skip_intro_seconds: Option<f64>,
    pub skip_outro_seconds: Option<f64>,
}

impl ConfigModule for MediaConfig {
    fn module_name() -> &'static str {
        "media"
    }

    fn default() -> Self {
        Self {
            auto_load_subtitles: true,
            subtitle_languages: vec!["en".to_string(), "eng".to_string()],
            audio_languages: vec!["en".to_string(), "eng".to_string()],
            hardware_decoding: true,
            buffer_size_seconds: 10.0,
            skip_intro_seconds: None,
            skip_outro_seconds: None,
        }
    }

    fn validate(&self) -> anyhow::Result<()> {
        if self.buffer_size_seconds < 1.0 || self.buffer_size_seconds > 300.0 {
            return Err(anyhow::anyhow!("Buffer size must be between 1 and 300 seconds"));
        }
        
        if let Some(skip) = self.skip_intro_seconds {
            if skip < 0.0 || skip > 600.0 {
                return Err(anyhow::anyhow!("Skip intro must be between 0 and 600 seconds"));
            }
        }
        
        if let Some(skip) = self.skip_outro_seconds {
            if skip < 0.0 || skip > 600.0 {
                return Err(anyhow::anyhow!("Skip outro must be between 0 and 600 seconds"));
            }
        }

        Ok(())
    }
}

/// Advanced configuration module
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedConfig {
    pub debug_mode: bool,
    pub memory_limit_mb: Option<u64>,
    pub cpu_cores: Option<u32>,
    pub experimental_features: bool,
    pub custom_ffmpeg_args: Vec<String>,
    pub log_file_path: Option<PathBuf>,
    pub performance_monitoring: bool,
}

impl ConfigModule for AdvancedConfig {
    fn module_name() -> &'static str {
        "advanced"
    }

    fn default() -> Self {
        Self {
            debug_mode: false,
            memory_limit_mb: None,
            cpu_cores: None,
            experimental_features: false,
            custom_ffmpeg_args: Vec::new(),
            log_file_path: None,
            performance_monitoring: false,
        }
    }

    fn validate(&self) -> anyhow::Result<()> {
        if let Some(memory) = self.memory_limit_mb {
            if memory < 128 {
                return Err(anyhow::anyhow!("Memory limit must be at least 128MB"));
            }
        }
        
        if let Some(cores) = self.cpu_cores {
            if cores == 0 || cores > 64 {
                return Err(anyhow::anyhow!("CPU cores must be between 1 and 64"));
            }
        }

        Ok(())
    }
}