record-daemon: initial commit
This commit is contained in:
29
record-daemon/src/config/mod.rs
Normal file
29
record-daemon/src/config/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! Configuration module for the record daemon.
|
||||
//!
|
||||
//! This module handles user-configurable settings, encoding presets,
|
||||
//! and configuration file persistence.
|
||||
|
||||
mod persistence;
|
||||
mod presets;
|
||||
mod settings;
|
||||
|
||||
pub use persistence::{load_config, save_config, ConfigPersistence};
|
||||
pub use presets::{AmfQuality, AudioSettings, EncoderPreset, QualityLevel};
|
||||
pub use settings::{OutputSettings, Settings, VideoSettings};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Default configuration file name.
|
||||
pub const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
|
||||
/// Get the default configuration directory.
|
||||
pub fn get_config_dir() -> Option<PathBuf> {
|
||||
directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon")
|
||||
.map(|dirs| dirs.config_dir().to_path_buf())
|
||||
}
|
||||
|
||||
/// Get the default output directory for recordings.
|
||||
pub fn get_default_output_dir() -> Option<PathBuf> {
|
||||
directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon")
|
||||
.map(|dirs| dirs.data_dir().join("recordings"))
|
||||
}
|
||||
176
record-daemon/src/config/persistence.rs
Normal file
176
record-daemon/src/config/persistence.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
//! Configuration persistence - loading and saving settings.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::{get_config_dir, Settings, CONFIG_FILE_NAME};
|
||||
use crate::error::{ConfigError, Result};
|
||||
|
||||
/// Configuration persistence handler.
|
||||
pub struct ConfigPersistence {
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ConfigPersistence {
|
||||
/// Create a new persistence handler with the given config path.
|
||||
pub fn new(config_path: PathBuf) -> Self {
|
||||
Self { config_path }
|
||||
}
|
||||
|
||||
/// Create a persistence handler using the default config location.
|
||||
pub fn default_location() -> io::Result<Self> {
|
||||
let config_dir = get_config_dir()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Config directory not found"))?;
|
||||
|
||||
Ok(Self::new(config_dir.join(CONFIG_FILE_NAME)))
|
||||
}
|
||||
|
||||
/// Get the configuration file path.
|
||||
pub fn config_path(&self) -> &PathBuf {
|
||||
&self.config_path
|
||||
}
|
||||
|
||||
/// Check if the configuration file exists.
|
||||
pub fn exists(&self) -> bool {
|
||||
self.config_path.exists()
|
||||
}
|
||||
|
||||
/// Load settings from the configuration file.
|
||||
pub fn load(&self) -> Result<Settings> {
|
||||
if !self.exists() {
|
||||
debug!("Config file not found, using defaults");
|
||||
return Ok(Settings::default());
|
||||
}
|
||||
|
||||
let mut file = fs::File::open(&self.config_path).map_err(ConfigError::ReadError)?;
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)
|
||||
.map_err(ConfigError::ReadError)?;
|
||||
|
||||
let settings: Settings = toml::from_str(&contents).map_err(ConfigError::from)?;
|
||||
|
||||
info!("Loaded configuration from {:?}", self.config_path);
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// Save settings to the configuration file.
|
||||
pub fn save(&self, settings: &Settings) -> Result<()> {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).map_err(ConfigError::WriteError)?;
|
||||
debug!("Created config directory: {:?}", parent);
|
||||
}
|
||||
}
|
||||
|
||||
let contents = toml::to_string_pretty(settings)
|
||||
.map_err(|e| ConfigError::InvalidConfig(e.to_string()))?;
|
||||
|
||||
let mut file = fs::File::create(&self.config_path).map_err(ConfigError::WriteError)?;
|
||||
|
||||
file.write_all(contents.as_bytes())
|
||||
.map_err(ConfigError::WriteError)?;
|
||||
|
||||
info!("Saved configuration to {:?}", self.config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate settings and apply any necessary migrations.
|
||||
pub fn validate_and_migrate(&self, settings: &mut Settings) -> Result<()> {
|
||||
// Ensure output directory exists
|
||||
if !settings.output.path.exists() {
|
||||
fs::create_dir_all(&settings.output.path).map_err(|e| {
|
||||
ConfigError::InvalidConfig(format!("Cannot create output directory: {}", e))
|
||||
})?;
|
||||
debug!("Created output directory: {:?}", settings.output.path);
|
||||
}
|
||||
|
||||
// Validate frame rate
|
||||
if settings.video.frame_rate == 0 || settings.video.frame_rate > 240 {
|
||||
warn!(
|
||||
"Invalid frame rate {}, defaulting to 60",
|
||||
settings.video.frame_rate
|
||||
);
|
||||
settings.video.frame_rate = 60;
|
||||
}
|
||||
|
||||
// Validate container format
|
||||
let valid_containers = ["mp4", "mkv", "mov", "flv"];
|
||||
if !valid_containers.contains(&settings.output.container.as_str()) {
|
||||
warn!(
|
||||
"Invalid container format '{}', defaulting to mp4",
|
||||
settings.output.container
|
||||
);
|
||||
settings.output.container = "mp4".to_string();
|
||||
}
|
||||
|
||||
// Validate log level
|
||||
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
||||
if !valid_levels.contains(&settings.daemon.log_level.as_str()) {
|
||||
warn!(
|
||||
"Invalid log level '{}', defaulting to info",
|
||||
settings.daemon.log_level
|
||||
);
|
||||
settings.daemon.log_level = "info".to_string();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from the default location.
|
||||
pub fn load_config() -> Result<Settings> {
|
||||
let persistence = ConfigPersistence::default_location().map_err(|e| {
|
||||
ConfigError::InvalidConfig(format!("Cannot access config directory: {}", e))
|
||||
})?;
|
||||
|
||||
let mut settings = persistence.load()?;
|
||||
persistence.validate_and_migrate(&mut settings)?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// Save configuration to the default location.
|
||||
pub fn save_config(settings: &Settings) -> Result<()> {
|
||||
let persistence = ConfigPersistence::default_location().map_err(|e| {
|
||||
ConfigError::InvalidConfig(format!("Cannot access config directory: {}", e))
|
||||
})?;
|
||||
|
||||
persistence.save(settings)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_save_and_load_config() {
|
||||
let dir = tempdir().unwrap();
|
||||
let config_path = dir.path().join("config.toml");
|
||||
let persistence = ConfigPersistence::new(config_path);
|
||||
|
||||
let settings = Settings::default();
|
||||
persistence.save(&settings).unwrap();
|
||||
|
||||
assert!(persistence.exists());
|
||||
|
||||
let loaded = persistence.load().unwrap();
|
||||
assert_eq!(settings.video.frame_rate, loaded.video.frame_rate);
|
||||
assert_eq!(settings.daemon.auto_record, loaded.daemon.auto_record);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_nonexistent_config() {
|
||||
let dir = tempdir().unwrap();
|
||||
let config_path = dir.path().join("nonexistent.toml");
|
||||
let persistence = ConfigPersistence::new(config_path);
|
||||
|
||||
let settings = persistence.load().unwrap();
|
||||
assert_eq!(settings.video.frame_rate, 60);
|
||||
}
|
||||
}
|
||||
303
record-daemon/src/config/presets.rs
Normal file
303
record-daemon/src/config/presets.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
//! Encoding presets and quality levels for video recording.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Video encoder preset configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum EncoderPreset {
|
||||
/// NVIDIA NVENC hardware encoder.
|
||||
Nvenc {
|
||||
/// Target bitrate in kbps.
|
||||
#[serde(default = "default_nvenc_bitrate")]
|
||||
bitrate: u32,
|
||||
/// Constant Quality level (lower = better quality).
|
||||
#[serde(default = "default_nvenc_cq")]
|
||||
cq_level: u32,
|
||||
/// Use two-pass encoding.
|
||||
#[serde(default = "default_two_pass")]
|
||||
two_pass: bool,
|
||||
},
|
||||
/// AMD AMF hardware encoder.
|
||||
Amf {
|
||||
/// Target bitrate in kbps.
|
||||
#[serde(default = "default_amf_bitrate")]
|
||||
bitrate: u32,
|
||||
/// Quality preset (speed, balanced, quality).
|
||||
#[serde(default = "default_amf_quality")]
|
||||
quality: AmfQuality,
|
||||
},
|
||||
/// Software x264 encoder (fallback).
|
||||
X264 {
|
||||
/// x264 preset (ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow).
|
||||
#[serde(default = "default_x264_preset")]
|
||||
preset: String,
|
||||
/// Target bitrate in kbps.
|
||||
#[serde(default = "default_x264_bitrate")]
|
||||
bitrate: u32,
|
||||
/// Use constant rate factor instead of bitrate.
|
||||
#[serde(default)]
|
||||
crf: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for EncoderPreset {
|
||||
fn default() -> Self {
|
||||
Self::Nvenc {
|
||||
bitrate: default_nvenc_bitrate(),
|
||||
cq_level: default_nvenc_cq(),
|
||||
two_pass: default_two_pass(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_nvenc_bitrate() -> u32 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_nvenc_cq() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_two_pass() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_amf_bitrate() -> u32 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_amf_quality() -> AmfQuality {
|
||||
AmfQuality::Balanced
|
||||
}
|
||||
|
||||
fn default_x264_preset() -> String {
|
||||
"veryfast".to_string()
|
||||
}
|
||||
|
||||
fn default_x264_bitrate() -> u32 {
|
||||
6000
|
||||
}
|
||||
|
||||
/// AMD AMF quality preset.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AmfQuality {
|
||||
Speed,
|
||||
#[default]
|
||||
Balanced,
|
||||
Quality,
|
||||
}
|
||||
|
||||
/// Quality level presets that configure resolution and other parameters.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum QualityLevel {
|
||||
/// 720p @ 30fps, lower bitrate.
|
||||
Low,
|
||||
/// 1080p @ 30fps, moderate bitrate.
|
||||
Medium,
|
||||
/// 1080p @ 60fps, high bitrate.
|
||||
#[default]
|
||||
High,
|
||||
/// 1440p @ 60fps, maximum quality.
|
||||
Ultra,
|
||||
}
|
||||
|
||||
impl QualityLevel {
|
||||
/// Get the target resolution for this quality level.
|
||||
pub fn resolution(&self) -> (u32, u32) {
|
||||
match self {
|
||||
QualityLevel::Low => (1280, 720),
|
||||
QualityLevel::Medium => (1920, 1080),
|
||||
QualityLevel::High => (1920, 1080),
|
||||
QualityLevel::Ultra => (2560, 1440),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the recommended frame rate for this quality level.
|
||||
pub fn frame_rate(&self) -> u32 {
|
||||
match self {
|
||||
QualityLevel::Low => 30,
|
||||
QualityLevel::Medium => 30,
|
||||
QualityLevel::High => 60,
|
||||
QualityLevel::Ultra => 60,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the recommended bitrate in kbps for this quality level.
|
||||
pub fn recommended_bitrate(&self) -> u32 {
|
||||
match self {
|
||||
QualityLevel::Low => 4500,
|
||||
QualityLevel::Medium => 6000,
|
||||
QualityLevel::High => 8000,
|
||||
QualityLevel::Ultra => 12000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn preset_default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Audio capture settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AudioSettings {
|
||||
/// Enable audio recording.
|
||||
#[serde(default = "preset_default_true")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Audio bitrate in kbps.
|
||||
#[serde(default = "default_audio_bitrate")]
|
||||
pub bitrate: u32,
|
||||
|
||||
/// Sample rate in Hz.
|
||||
#[serde(default = "default_sample_rate")]
|
||||
pub sample_rate: u32,
|
||||
|
||||
/// Number of audio channels.
|
||||
#[serde(default = "default_channels")]
|
||||
pub channels: u32,
|
||||
|
||||
/// Capture game audio.
|
||||
#[serde(default = "preset_default_true")]
|
||||
pub capture_game: bool,
|
||||
|
||||
/// Capture microphone.
|
||||
#[serde(default)]
|
||||
pub capture_mic: bool,
|
||||
|
||||
/// Microphone device name (if capture_mic is true).
|
||||
#[serde(default)]
|
||||
pub mic_device: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AudioSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
bitrate: default_audio_bitrate(),
|
||||
sample_rate: default_sample_rate(),
|
||||
channels: default_channels(),
|
||||
capture_game: true,
|
||||
capture_mic: false,
|
||||
mic_device: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_audio_bitrate() -> u32 {
|
||||
192
|
||||
}
|
||||
|
||||
fn default_sample_rate() -> u32 {
|
||||
48000
|
||||
}
|
||||
|
||||
fn default_channels() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
/// Encoder capabilities detection.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncoderCapability {
|
||||
/// NVIDIA NVENC available.
|
||||
Nvenc,
|
||||
/// AMD AMF available.
|
||||
Amf,
|
||||
/// Intel QuickSync available.
|
||||
QuickSync,
|
||||
/// Software encoding only.
|
||||
Software,
|
||||
}
|
||||
|
||||
impl EncoderPreset {
|
||||
/// Check if this preset uses hardware encoding.
|
||||
pub fn is_hardware(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EncoderPreset::Nvenc { .. } | EncoderPreset::Amf { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the encoder name for OBS.
|
||||
pub fn encoder_name(&self) -> &'static str {
|
||||
match self {
|
||||
EncoderPreset::Nvenc { .. } => "jim_nvenc",
|
||||
EncoderPreset::Amf { .. } => "amd_amf_h264",
|
||||
EncoderPreset::X264 { .. } => "x264",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the effective bitrate for this preset.
|
||||
pub fn effective_bitrate(&self) -> u32 {
|
||||
match self {
|
||||
EncoderPreset::Nvenc { bitrate, .. } => *bitrate,
|
||||
EncoderPreset::Amf { bitrate, .. } => *bitrate,
|
||||
EncoderPreset::X264 { bitrate, .. } => *bitrate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a preset optimized for the given quality level.
|
||||
pub fn for_quality(quality: QualityLevel, capability: EncoderCapability) -> Self {
|
||||
let bitrate = quality.recommended_bitrate();
|
||||
|
||||
match capability {
|
||||
EncoderCapability::Nvenc => EncoderPreset::Nvenc {
|
||||
bitrate,
|
||||
cq_level: 20,
|
||||
two_pass: true,
|
||||
},
|
||||
EncoderCapability::Amf => EncoderPreset::Amf {
|
||||
bitrate,
|
||||
quality: AmfQuality::Balanced,
|
||||
},
|
||||
EncoderCapability::QuickSync => EncoderPreset::X264 {
|
||||
preset: "veryfast".to_string(),
|
||||
bitrate,
|
||||
crf: None,
|
||||
},
|
||||
EncoderCapability::Software => EncoderPreset::X264 {
|
||||
preset: "superfast".to_string(),
|
||||
bitrate: (bitrate as f32 * 0.8) as u32,
|
||||
crf: Some(23),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_quality_level_resolution() {
|
||||
assert_eq!(QualityLevel::Low.resolution(), (1280, 720));
|
||||
assert_eq!(QualityLevel::High.resolution(), (1920, 1080));
|
||||
assert_eq!(QualityLevel::Ultra.resolution(), (2560, 1440));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_preset_hardware_detection() {
|
||||
let nvenc = EncoderPreset::Nvenc {
|
||||
bitrate: 8000,
|
||||
cq_level: 20,
|
||||
two_pass: true,
|
||||
};
|
||||
assert!(nvenc.is_hardware());
|
||||
|
||||
let x264 = EncoderPreset::X264 {
|
||||
preset: "fast".to_string(),
|
||||
bitrate: 6000,
|
||||
crf: None,
|
||||
};
|
||||
assert!(!x264.is_hardware());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preset_for_quality() {
|
||||
let preset = EncoderPreset::for_quality(QualityLevel::High, EncoderCapability::Nvenc);
|
||||
assert_eq!(preset.effective_bitrate(), 8000);
|
||||
assert!(preset.is_hardware());
|
||||
}
|
||||
}
|
||||
179
record-daemon/src/config/settings.rs
Normal file
179
record-daemon/src/config/settings.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! User-configurable settings for the record daemon.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::presets::{AudioSettings, EncoderPreset, QualityLevel};
|
||||
use crate::config::get_default_output_dir;
|
||||
|
||||
/// Main settings structure.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Settings {
|
||||
/// Video recording settings.
|
||||
#[serde(default)]
|
||||
pub video: VideoSettings,
|
||||
|
||||
/// Output settings.
|
||||
#[serde(default)]
|
||||
pub output: OutputSettings,
|
||||
|
||||
/// Audio capture settings.
|
||||
#[serde(default)]
|
||||
pub audio: AudioSettings,
|
||||
|
||||
/// Daemon behavior settings.
|
||||
#[serde(default)]
|
||||
pub daemon: DaemonSettings,
|
||||
}
|
||||
|
||||
/// Video recording settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoSettings {
|
||||
/// Encoder preset (codec and quality settings).
|
||||
#[serde(default)]
|
||||
pub encoder_preset: EncoderPreset,
|
||||
|
||||
/// Quality level preset.
|
||||
#[serde(default)]
|
||||
pub quality: QualityLevel,
|
||||
|
||||
/// Target frame rate.
|
||||
#[serde(default = "default_frame_rate")]
|
||||
pub frame_rate: u32,
|
||||
|
||||
/// Enable hardware acceleration.
|
||||
#[serde(default = "default_true")]
|
||||
pub hardware_acceleration: bool,
|
||||
}
|
||||
|
||||
impl Default for VideoSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
encoder_preset: EncoderPreset::default(),
|
||||
quality: QualityLevel::default(),
|
||||
frame_rate: default_frame_rate(),
|
||||
hardware_acceleration: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_frame_rate() -> u32 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Output settings for recordings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutputSettings {
|
||||
/// Directory where recordings will be saved.
|
||||
#[serde(default = "default_output_path")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// File naming pattern.
|
||||
/// Supports: {date}, {time}, {game_id}, {champion}, {queue_type}
|
||||
#[serde(default = "default_naming_pattern")]
|
||||
pub naming_pattern: String,
|
||||
|
||||
/// Container format (mp4, mkv, etc.).
|
||||
#[serde(default = "default_container")]
|
||||
pub container: String,
|
||||
|
||||
/// Split recordings by size (MB). 0 = no splitting.
|
||||
#[serde(default)]
|
||||
pub split_size_mb: u32,
|
||||
|
||||
/// Split recordings by time (minutes). 0 = no splitting.
|
||||
#[serde(default)]
|
||||
pub split_time_minutes: u32,
|
||||
}
|
||||
|
||||
impl Default for OutputSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: default_output_path(),
|
||||
naming_pattern: default_naming_pattern(),
|
||||
container: default_container(),
|
||||
split_size_mb: 0,
|
||||
split_time_minutes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_path() -> PathBuf {
|
||||
get_default_output_dir().unwrap_or_else(|| PathBuf::from("./recordings"))
|
||||
}
|
||||
|
||||
fn default_naming_pattern() -> String {
|
||||
"{date}_{time}_{champion}".to_string()
|
||||
}
|
||||
|
||||
fn default_container() -> String {
|
||||
"mp4".to_string()
|
||||
}
|
||||
|
||||
/// Daemon behavior settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DaemonSettings {
|
||||
/// Automatically start recording when a match begins.
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_record: bool,
|
||||
|
||||
/// Monitor for League Client startup.
|
||||
#[serde(default = "default_true")]
|
||||
pub monitor_client: bool,
|
||||
|
||||
/// Polling interval for client detection (milliseconds).
|
||||
#[serde(default = "default_poll_interval")]
|
||||
pub poll_interval_ms: u64,
|
||||
|
||||
/// IPC socket path. If None, uses default path.
|
||||
#[serde(default)]
|
||||
pub socket_path: Option<PathBuf>,
|
||||
|
||||
/// Log level (trace, debug, info, warn, error).
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
|
||||
/// Keep event timeline after recording ends.
|
||||
#[serde(default = "default_true")]
|
||||
pub save_timeline: bool,
|
||||
}
|
||||
|
||||
impl Default for DaemonSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_record: true,
|
||||
monitor_client: true,
|
||||
poll_interval_ms: default_poll_interval(),
|
||||
socket_path: None,
|
||||
log_level: default_log_level(),
|
||||
save_timeline: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_poll_interval() -> u64 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_settings_serialization() {
|
||||
let settings = Settings::default();
|
||||
let toml_str = toml::to_string_pretty(&settings).unwrap();
|
||||
let parsed: Settings = toml::from_str(&toml_str).unwrap();
|
||||
|
||||
assert_eq!(settings.video.frame_rate, parsed.video.frame_rate);
|
||||
assert_eq!(settings.daemon.auto_record, parsed.daemon.auto_record);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user