Files
SJNmusic/daemon/src/config.rs
T
2026-07-12 18:10:17 +02:00

185 lines
5.8 KiB
Rust

use std::fs;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use crate::paths::config_path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// Bind host. Use "127.0.0.1" for local-only, "0.0.0.0" to expose on the network.
pub host: String,
/// Port to bind the HTTP server on.
pub port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 14567,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
/// Log filter: trace, debug, info, warn, error
pub level: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
}
}
}
/// Library / filesystem configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryConfig {
/// Directory where downloaded songs live. Defaults to
/// `$HOME/.sjn/music/songs`. May be absolute or relative to the config file.
#[serde(default)]
pub music_dir: Option<String>,
/// Maximum number of songs allowed in the playback queue. 0 = unlimited.
#[serde(default)]
pub max_queue_size: usize,
/// Default repeat mode used on daemon start: one of "off", "one", "all".
#[serde(default)]
pub default_repeat: String,
}
impl Default for LibraryConfig {
fn default() -> Self {
Self {
music_dir: None,
max_queue_size: 0,
default_repeat: "off".to_string(),
}
}
}
/// Controls how fuzzy search behaves and which routes /search and /search/all use.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
/// Jaro-Winkler score in [0.0, 1.0] below which fuzzy matches are rejected.
#[serde(default = "default_fuzzy")]
pub fuzzy_threshold: f64,
}
fn default_fuzzy() -> f64 {
0.65
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
fuzzy_threshold: default_fuzzy(),
}
}
}
/// Controls how /init's interactive search picker behaves before downloading
/// a song. The full picker lives in the CLI; the daemon just exposes how many
/// candidates to fetch by default and how to invoke `yt-dlp`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadConfig {
/// Default number of yt-dlp search candidates shown to the user before a
/// download is started. Users can override per-call via `/search/yt?limit=N`.
#[serde(default = "default_search_count")]
pub search_count: usize,
}
fn default_search_count() -> usize {
3
}
impl Default for DownloadConfig {
fn default() -> Self {
Self {
search_count: default_search_count(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub library: LibraryConfig,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)]
pub download: DownloadConfig,
/// When `true`, the daemon requires `Authorization: Bearer <token>`
/// for any request whose peer is NOT a loopback address. The matching
/// `auth_token` is auto-generated at startup if it is still empty
/// (see `daemon::run`), then persisted to disk via
/// [`Config::save`] so subsequent restarts reuse the same token.
#[serde(default)]
pub auth_enabled: bool,
/// Bearer credential compared against incoming `Authorization` headers
/// when `auth_enabled = true`. Empty string disables auth entirely
/// regardless of the `auth_enabled` flag — the daemon treats any
/// `auth_token == ""` as "no token configured, allow all remote".
#[serde(default)]
pub auth_token: String,
}
impl Config {
/// Persist the current `Config` back to its TOML file. Used by the
/// daemon only after a new bearer token has been auto-generated at
/// startup, so the same token is reusable across restarts. Falls
/// back silently on serialization/write failures — the daemon will
/// still boot with the in-memory token, it just won't survive a
/// restart (and will log a warning about that).
pub fn save(&self) -> std::io::Result<()> {
let path = config_path();
let serialized = toml::to_string_pretty(self).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
})?;
fs::write(&path, serialized)
}
/// Load configuration from the TOML file. If the file does not exist, create
/// it with sensible defaults so the daemon can be configured on first run.
pub fn load() -> Self {
let path = config_path();
if !std::path::Path::new(&path).exists() {
warn!("Config not found at {path}, creating default config");
let cfg = Config::default();
if let Ok(s) = toml::to_string_pretty(&cfg) {
if let Err(e) = fs::write(&path, s) {
warn!("Failed to write default config: {e}");
} else {
info!("Wrote default config to {path}");
}
}
return cfg;
}
match fs::read_to_string(&path) {
Ok(text) => match toml::from_str::<Config>(&text) {
Ok(cfg) => {
debug!("Loaded config from {path}: {:?}", cfg);
cfg
}
Err(e) => {
warn!("Failed to parse config at {path}: {e}. Falling back to defaults.");
Config::default()
}
},
Err(e) => {
warn!("Failed to read config at {path}: {e}. Falling back to defaults.");
Config::default()
}
}
}
}