mod db; mod web; use std::sync::Arc; use clap::Parser; #[derive(Parser, Debug)] #[command(version, about = "Furumi Web Player: database-backed music player")] struct Args { /// IP address and port for the web player #[arg(long, env = "FURUMI_PLAYER_BIND", default_value = "0.0.0.0:8080")] bind: String, /// PostgreSQL connection URL #[arg(long, env = "FURUMI_PLAYER_DATABASE_URL")] database_url: String, /// Root directory where music files are stored (agent's storage_dir) #[arg(long, env = "FURUMI_PLAYER_STORAGE_DIR")] storage_dir: std::path::PathBuf, /// OIDC Issuer URL (e.g. https://auth.example.com/application/o/furumi/) #[arg(long, env = "FURUMI_PLAYER_OIDC_ISSUER_URL")] oidc_issuer_url: Option, /// OIDC Client ID #[arg(long, env = "FURUMI_PLAYER_OIDC_CLIENT_ID")] oidc_client_id: Option, /// OIDC Client Secret #[arg(long, env = "FURUMI_PLAYER_OIDC_CLIENT_SECRET")] oidc_client_secret: Option, /// OIDC Redirect URL (e.g. https://music.example.com/auth/callback) #[arg(long, env = "FURUMI_PLAYER_OIDC_REDIRECT_URL")] oidc_redirect_url: Option, /// OIDC Session Secret (32+ chars, for HMAC). Random if not provided. #[arg(long, env = "FURUMI_PLAYER_OIDC_SESSION_SECRET")] oidc_session_secret: Option, } #[tokio::main] async fn main() -> Result<(), Box> { // Install ring as the default crypto provider for rustls rustls::crypto::ring::default_provider() .install_default() .expect("Failed to install rustls crypto provider"); tracing_subscriber::fmt::init(); let args = Args::parse(); let version = option_env!("FURUMI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")); tracing::info!("Furumi Web Player v{} starting", version); tracing::info!("Storage directory: {:?}", args.storage_dir); if !args.storage_dir.exists() || !args.storage_dir.is_dir() { eprintln!("Error: Storage directory {:?} does not exist or is not a directory", args.storage_dir); std::process::exit(1); } tracing::info!("Connecting to database..."); let pool = db::connect(&args.database_url).await?; tracing::info!("Database connected"); // Initialize OIDC if configured let oidc_state = if let (Some(issuer), Some(client_id), Some(secret), Some(redirect)) = ( args.oidc_issuer_url, args.oidc_client_id, args.oidc_client_secret, args.oidc_redirect_url, ) { tracing::info!("OIDC (SSO): enabled (issuer: {})", issuer); match web::auth::oidc_init(issuer, client_id, secret, redirect, args.oidc_session_secret).await { Ok(state) => Some(Arc::new(state)), Err(e) => { eprintln!("Error initializing OIDC: {}", e); std::process::exit(1); } } } else { tracing::info!("OIDC (SSO): disabled (no OIDC configuration provided)"); None }; let bind_addr: std::net::SocketAddr = args.bind.parse().unwrap_or_else(|e| { eprintln!("Error: Invalid bind address '{}': {}", args.bind, e); std::process::exit(1); }); let state = Arc::new(web::AppState { pool, storage_dir: Arc::new(args.storage_dir), oidc: oidc_state, }); tracing::info!("Web player: http://{}", bind_addr); let app = web::build_router(state); let listener = tokio::net::TcpListener::bind(bind_addr).await?; axum::serve(listener, app).await?; Ok(()) }