437 lines
12 KiB
Rust
437 lines
12 KiB
Rust
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,
|
|
|
|
#[serde(default)]
|
|
pub metadata: MetadataConfig,
|
|
|
|
#[serde(default)]
|
|
pub scheduling: SchedulingConfig,
|
|
|
|
#[serde(default)]
|
|
pub subsonic: SubsonicConfig,
|
|
}
|
|
|
|
#[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 ~300.
|
|
#[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,
|
|
|
|
/// Enable automatic cookie refresh via headless Firefox.
|
|
#[serde(default)]
|
|
pub cookie_refresh_enabled: bool,
|
|
|
|
/// How often to refresh cookies (hours).
|
|
#[serde(default = "default_cookie_refresh_hours")]
|
|
pub cookie_refresh_hours: u32,
|
|
|
|
/// Port for noVNC during interactive YouTube login.
|
|
#[serde(default = "default_vnc_port")]
|
|
pub vnc_port: u16,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IndexingConfig {
|
|
/// Number of concurrent file processors during library scan.
|
|
#[serde(default = "default_concurrency")]
|
|
pub concurrency: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MetadataConfig {
|
|
/// Source for structured metadata: "musicbrainz" (default).
|
|
#[serde(default = "default_metadata_source")]
|
|
pub metadata_source: String,
|
|
|
|
/// Source for artist images: "wikipedia" (default).
|
|
#[serde(default = "default_artist_image_source")]
|
|
pub artist_image_source: String,
|
|
|
|
/// Source for artist bios: "wikipedia" (default) or "lastfm".
|
|
#[serde(default = "default_artist_bio_source")]
|
|
pub artist_bio_source: String,
|
|
|
|
/// Source for lyrics: "lrclib" (default).
|
|
#[serde(default = "default_lyrics_source")]
|
|
pub lyrics_source: String,
|
|
|
|
/// Source for cover art: "coverartarchive" (default).
|
|
#[serde(default = "default_cover_art_source")]
|
|
pub cover_art_source: String,
|
|
|
|
/// Last.fm API key for fetching artist bios. Set via SHANTY_LASTFM_API_KEY env var.
|
|
/// Required if artist_bio_source is "lastfm".
|
|
#[serde(skip)]
|
|
pub lastfm_api_key: Option<String>,
|
|
|
|
/// fanart.tv API key for artist images/banners. Set via SHANTY_FANART_API_KEY env var.
|
|
/// Required if artist_image_source is "fanarttv".
|
|
#[serde(skip)]
|
|
pub fanart_api_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SchedulingConfig {
|
|
/// Enable automatic pipeline runs.
|
|
#[serde(default)]
|
|
pub pipeline_enabled: bool,
|
|
|
|
/// Hours between pipeline runs (after previous completion).
|
|
#[serde(default = "default_pipeline_interval_hours")]
|
|
pub pipeline_interval_hours: u32,
|
|
|
|
/// Enable automatic new release checking for monitored artists.
|
|
#[serde(default)]
|
|
pub monitor_enabled: bool,
|
|
|
|
/// Hours between monitor checks.
|
|
#[serde(default = "default_monitor_interval_hours")]
|
|
pub monitor_interval_hours: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SubsonicConfig {
|
|
/// Whether the Subsonic API is enabled.
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
|
|
/// Whether transcoding via ffmpeg is enabled.
|
|
#[serde(default = "default_true")]
|
|
pub transcoding_enabled: bool,
|
|
}
|
|
|
|
impl Default for SubsonicConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
transcoding_enabled: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for SchedulingConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
pipeline_enabled: true,
|
|
pipeline_interval_hours: default_pipeline_interval_hours(),
|
|
monitor_enabled: true,
|
|
monitor_interval_hours: default_monitor_interval_hours(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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(),
|
|
metadata: MetadataConfig::default(),
|
|
scheduling: SchedulingConfig::default(),
|
|
subsonic: SubsonicConfig::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(),
|
|
cookie_refresh_enabled: false,
|
|
cookie_refresh_hours: default_cookie_refresh_hours(),
|
|
vnc_port: default_vnc_port(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for IndexingConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
concurrency: default_concurrency(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for MetadataConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
metadata_source: default_metadata_source(),
|
|
artist_image_source: default_artist_image_source(),
|
|
artist_bio_source: default_artist_bio_source(),
|
|
lyrics_source: default_lyrics_source(),
|
|
cover_art_source: default_cover_art_source(),
|
|
lastfm_api_key: None,
|
|
fanart_api_key: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
250
|
|
}
|
|
fn default_rate_limit_auth() -> u32 {
|
|
1800
|
|
}
|
|
fn default_concurrency() -> usize {
|
|
4
|
|
}
|
|
fn default_metadata_source() -> String {
|
|
"musicbrainz".to_string()
|
|
}
|
|
fn default_artist_image_source() -> String {
|
|
"wikipedia".to_string()
|
|
}
|
|
fn default_artist_bio_source() -> String {
|
|
"wikipedia".to_string()
|
|
}
|
|
fn default_lyrics_source() -> String {
|
|
"lrclib".to_string()
|
|
}
|
|
fn default_cover_art_source() -> String {
|
|
"coverartarchive".to_string()
|
|
}
|
|
fn default_pipeline_interval_hours() -> u32 {
|
|
3
|
|
}
|
|
fn default_monitor_interval_hours() -> u32 {
|
|
12
|
|
}
|
|
fn default_cookie_refresh_hours() -> u32 {
|
|
6
|
|
}
|
|
fn default_vnc_port() -> u16 {
|
|
6080
|
|
}
|
|
|
|
/// Return the application data directory (e.g. ~/.local/share/shanty).
|
|
pub fn data_dir() -> PathBuf {
|
|
dirs::data_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join("shanty")
|
|
}
|
|
|
|
// --- 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")
|
|
&& let Ok(port) = v.parse()
|
|
{
|
|
config.web.port = port;
|
|
}
|
|
if let Ok(v) = std::env::var("SHANTY_WEB_BIND") {
|
|
config.web.bind = v;
|
|
}
|
|
if let Ok(v) = std::env::var("SHANTY_LASTFM_API_KEY") {
|
|
config.metadata.lastfm_api_key = Some(v);
|
|
}
|
|
if let Ok(v) = std::env::var("SHANTY_FANART_API_KEY") {
|
|
config.metadata.fanart_api_key = Some(v);
|
|
}
|
|
config
|
|
}
|
|
}
|