Added config stuff
This commit is contained in:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -2906,6 +2906,37 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shanty"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-cors",
|
||||||
|
"actix-files",
|
||||||
|
"actix-web",
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"serde_json",
|
||||||
|
"shanty-config",
|
||||||
|
"shanty-db",
|
||||||
|
"shanty-search",
|
||||||
|
"shanty-tag",
|
||||||
|
"shanty-web",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-actix-web",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shanty-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shanty-db"
|
name = "shanty-db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3105,6 +3136,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"shanty-config",
|
||||||
"shanty-db",
|
"shanty-db",
|
||||||
"shanty-dl",
|
"shanty-dl",
|
||||||
"shanty-index",
|
"shanty-index",
|
||||||
|
|||||||
29
Cargo.toml
29
Cargo.toml
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
exclude = ["shanty-web/frontend"]
|
exclude = ["shanty-web/frontend"]
|
||||||
members = [
|
members = [
|
||||||
|
"shanty-config",
|
||||||
"shanty-db",
|
"shanty-db",
|
||||||
"shanty-index",
|
"shanty-index",
|
||||||
"shanty-tag",
|
"shanty-tag",
|
||||||
@@ -40,3 +41,31 @@ anyhow = "1"
|
|||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# Top-level binary
|
||||||
|
[package]
|
||||||
|
name = "shanty"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "shanty"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
shanty-config = { path = "shanty-config" }
|
||||||
|
shanty-db = { path = "shanty-db" }
|
||||||
|
shanty-web = { path = "shanty-web" }
|
||||||
|
shanty-tag = { path = "shanty-tag" }
|
||||||
|
shanty-search = { path = "shanty-search" }
|
||||||
|
actix-web = "4"
|
||||||
|
actix-cors = "0.7"
|
||||||
|
actix-files = "0.6"
|
||||||
|
tracing-actix-web = "0.7"
|
||||||
|
tokio = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|||||||
11
shanty-config/Cargo.toml
Normal file
11
shanty-config/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "shanty-config"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
dirs = "6"
|
||||||
|
tracing = { workspace = true }
|
||||||
254
shanty-config/src/lib.rs
Normal file
254
shanty-config/src/lib.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule shanty-web updated: ff41233a96...32b4b533c0
134
src/main.rs
Normal file
134
src/main.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use clap::Parser;
|
||||||
|
use tracing_actix_web::TracingLogger;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use shanty_config::AppConfig;
|
||||||
|
use shanty_db::Database;
|
||||||
|
use shanty_search::MusicBrainzSearch;
|
||||||
|
use shanty_tag::MusicBrainzClient;
|
||||||
|
|
||||||
|
use shanty_web::routes;
|
||||||
|
use shanty_web::state::AppState;
|
||||||
|
use shanty_web::tasks::TaskManager;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "shanty", about = "Shanty — self-hosted music management")]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to config file.
|
||||||
|
#[arg(long, env = "SHANTY_CONFIG")]
|
||||||
|
config: Option<String>,
|
||||||
|
|
||||||
|
/// Override the port.
|
||||||
|
#[arg(long)]
|
||||||
|
port: Option<u16>,
|
||||||
|
|
||||||
|
/// Increase verbosity (-v info, -vv debug, -vvv trace).
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let filter = match cli.verbose {
|
||||||
|
0 => "info,shanty=info,shanty_web=info",
|
||||||
|
1 => "info,shanty=debug,shanty_web=debug",
|
||||||
|
_ => "debug,shanty=trace,shanty_web=trace",
|
||||||
|
};
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let mut config = AppConfig::load(cli.config.as_deref());
|
||||||
|
if let Some(port) = cli.port {
|
||||||
|
config.web.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(url = %config.database_url, "connecting to database");
|
||||||
|
let db = Database::new(&config.database_url).await?;
|
||||||
|
|
||||||
|
let mb_client = MusicBrainzClient::new()?;
|
||||||
|
let search = MusicBrainzSearch::new()?;
|
||||||
|
|
||||||
|
let bind = format!("{}:{}", config.web.bind, config.web.port);
|
||||||
|
tracing::info!(bind = %bind, "starting server");
|
||||||
|
|
||||||
|
let config_path = cli.config.clone();
|
||||||
|
let state = web::Data::new(AppState {
|
||||||
|
db,
|
||||||
|
mb_client,
|
||||||
|
search,
|
||||||
|
config: std::sync::Arc::new(tokio::sync::RwLock::new(config)),
|
||||||
|
config_path,
|
||||||
|
tasks: TaskManager::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve static files directory
|
||||||
|
let static_dir = std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|exe| exe.parent().map(|p| p.to_owned()))
|
||||||
|
.map(|p| p.join("static"))
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from("static"));
|
||||||
|
|
||||||
|
let static_dir = if static_dir.is_dir() {
|
||||||
|
static_dir
|
||||||
|
} else {
|
||||||
|
// Check next to shanty-web crate root (for development)
|
||||||
|
let dev_path = std::path::PathBuf::from(
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "/shanty-web/static"),
|
||||||
|
);
|
||||||
|
if dev_path.is_dir() {
|
||||||
|
dev_path
|
||||||
|
} else {
|
||||||
|
tracing::warn!("static directory not found — frontend will not be served");
|
||||||
|
static_dir
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::info!(path = %static_dir.display(), "serving static files");
|
||||||
|
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
let cors = Cors::permissive();
|
||||||
|
let static_dir = static_dir.clone();
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.wrap(cors)
|
||||||
|
.wrap(TracingLogger::default())
|
||||||
|
.app_data(state.clone())
|
||||||
|
.configure(routes::configure)
|
||||||
|
.service(
|
||||||
|
actix_files::Files::new("/", static_dir.clone())
|
||||||
|
.index_file("index.html")
|
||||||
|
.prefer_utf8(true),
|
||||||
|
)
|
||||||
|
.default_service(web::to({
|
||||||
|
let index_path = static_dir.join("index.html");
|
||||||
|
move |req: actix_web::HttpRequest| {
|
||||||
|
let index_path = index_path.clone();
|
||||||
|
async move {
|
||||||
|
actix_files::NamedFile::open_async(index_path)
|
||||||
|
.await
|
||||||
|
.map(|f| f.into_response(&req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.bind(&bind)?
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Graceful shutdown on Ctrl+C / SIGTERM
|
||||||
|
let handle = server.handle();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::signal::ctrl_c().await.ok();
|
||||||
|
tracing::info!("shutdown signal received, stopping server");
|
||||||
|
handle.stop(true).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.await?;
|
||||||
|
tracing::info!("server stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user