Added config stuff

This commit is contained in:
Connor Johnstone
2026-03-18 15:14:45 -04:00
parent edcc5cb110
commit b8f178ae08
6 changed files with 461 additions and 1 deletions

254
shanty-config/src/lib.rs Normal file
View File

@@ -0,0 +1,254 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(default = "default_library_path")]
pub library_path: PathBuf,
#[serde(default = "default_database_url")]
pub database_url: String,
#[serde(default = "default_download_path")]
pub download_path: PathBuf,
#[serde(default = "default_organization_format")]
pub organization_format: String,
/// Which secondary release group types to include. Empty = studio releases only.
/// Options: "Compilation", "Live", "Soundtrack", "Remix", "DJ-mix", "Demo", etc.
#[serde(default)]
pub allowed_secondary_types: Vec<String>,
#[serde(default)]
pub web: WebConfig,
#[serde(default)]
pub tagging: TaggingConfig,
#[serde(default)]
pub download: DownloadConfig,
#[serde(default)]
pub indexing: IndexingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
#[serde(default = "default_port")]
pub port: u16,
#[serde(default = "default_bind")]
pub bind: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaggingConfig {
#[serde(default)]
pub auto_tag: bool,
#[serde(default = "default_true")]
pub write_tags: bool,
#[serde(default = "default_confidence")]
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadConfig {
#[serde(default = "default_format")]
pub format: String,
#[serde(default = "default_search_source")]
pub search_source: String,
#[serde(default)]
pub cookies_path: Option<PathBuf>,
/// Requests per hour (unauthenticated). Actual YouTube limit is ~500.
#[serde(default = "default_rate_limit")]
pub rate_limit: u32,
/// Requests per hour (with cookies). Actual YouTube limit is ~2000.
#[serde(default = "default_rate_limit_auth")]
pub rate_limit_auth: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexingConfig {
/// Number of concurrent file processors during library scan.
#[serde(default = "default_concurrency")]
pub concurrency: usize,
}
// --- Defaults ---
impl Default for AppConfig {
fn default() -> Self {
Self {
library_path: default_library_path(),
database_url: default_database_url(),
download_path: default_download_path(),
organization_format: default_organization_format(),
allowed_secondary_types: vec![],
web: WebConfig::default(),
tagging: TaggingConfig::default(),
download: DownloadConfig::default(),
indexing: IndexingConfig::default(),
}
}
}
impl Default for WebConfig {
fn default() -> Self {
Self {
port: default_port(),
bind: default_bind(),
}
}
}
impl Default for TaggingConfig {
fn default() -> Self {
Self {
auto_tag: false,
write_tags: true,
confidence: default_confidence(),
}
}
}
impl Default for DownloadConfig {
fn default() -> Self {
Self {
format: default_format(),
search_source: default_search_source(),
cookies_path: None,
rate_limit: default_rate_limit(),
rate_limit_auth: default_rate_limit_auth(),
}
}
}
impl Default for IndexingConfig {
fn default() -> Self {
Self {
concurrency: default_concurrency(),
}
}
}
fn default_library_path() -> PathBuf {
dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))
}
fn default_database_url() -> String {
let data_dir = dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("shanty");
std::fs::create_dir_all(&data_dir).ok();
format!("sqlite://{}?mode=rwc", data_dir.join("shanty.db").display())
}
fn default_download_path() -> PathBuf {
let dir = dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("shanty")
.join("downloads");
std::fs::create_dir_all(&dir).ok();
dir
}
fn default_organization_format() -> String {
"{artist}/{album}/{track_number} - {title}.{ext}".to_string()
}
fn default_port() -> u16 { 8085 }
fn default_bind() -> String { "0.0.0.0".to_string() }
fn default_confidence() -> f64 { 0.8 }
fn default_format() -> String { "opus".to_string() }
fn default_search_source() -> String { "ytmusic".to_string() }
fn default_true() -> bool { true }
fn default_rate_limit() -> u32 { 450 }
fn default_rate_limit_auth() -> u32 { 1800 }
fn default_concurrency() -> usize { 4 }
// --- Loading and Saving ---
impl AppConfig {
/// Resolve the config file path.
pub fn config_path(override_path: Option<&str>) -> PathBuf {
override_path
.map(PathBuf::from)
.or_else(|| std::env::var("SHANTY_CONFIG").ok().map(PathBuf::from))
.unwrap_or_else(|| {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("shanty")
.join("config.yaml")
})
}
/// Load config from file, falling back to defaults.
pub fn load(path: Option<&str>) -> Self {
let config_path = Self::config_path(path);
if config_path.exists() {
match std::fs::read_to_string(&config_path) {
Ok(contents) => match serde_yaml::from_str(&contents) {
Ok(config) => {
tracing::info!(path = %config_path.display(), "loaded config");
return Self::apply_env_overrides(config);
}
Err(e) => {
tracing::warn!(path = %config_path.display(), error = %e, "failed to parse config, using defaults");
}
},
Err(e) => {
tracing::warn!(path = %config_path.display(), error = %e, "failed to read config, using defaults");
}
}
} else {
tracing::info!(path = %config_path.display(), "no config file found, using defaults");
}
Self::apply_env_overrides(AppConfig::default())
}
/// Save config to YAML file.
pub fn save(&self, path: Option<&str>) -> Result<(), String> {
let config_path = Self::config_path(path);
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("failed to create config directory: {e}"))?;
}
let yaml = serde_yaml::to_string(self)
.map_err(|e| format!("failed to serialize config: {e}"))?;
std::fs::write(&config_path, yaml)
.map_err(|e| format!("failed to write config to {}: {e}", config_path.display()))?;
tracing::info!(path = %config_path.display(), "config saved");
Ok(())
}
fn apply_env_overrides(mut config: Self) -> Self {
if let Ok(v) = std::env::var("SHANTY_DATABASE_URL") {
config.database_url = v;
}
if let Ok(v) = std::env::var("SHANTY_LIBRARY_PATH") {
config.library_path = PathBuf::from(v);
}
if let Ok(v) = std::env::var("SHANTY_DOWNLOAD_PATH") {
config.download_path = PathBuf::from(v);
}
if let Ok(v) = std::env::var("SHANTY_WEB_PORT") {
if let Ok(port) = v.parse() {
config.web.port = port;
}
}
if let Ok(v) = std::env::var("SHANTY_WEB_BIND") {
config.web.bind = v;
}
config
}
}