149 lines
4.7 KiB
Rust
149 lines
4.7 KiB
Rust
use actix_cors::Cors;
|
|
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
|
use actix_web::{App, HttpServer, cookie::Key, web};
|
|
use clap::Parser;
|
|
use tracing_actix_web::TracingLogger;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use shanty_data::MusicBrainzFetcher;
|
|
use shanty_data::WikipediaFetcher;
|
|
use shanty_db::Database;
|
|
use shanty_search::MusicBrainzSearch;
|
|
|
|
use shanty_web::config::AppConfig;
|
|
use shanty_web::routes;
|
|
use shanty_web::state::AppState;
|
|
use shanty_web::tasks::TaskManager;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "shanty-web", about = "Shanty web interface backend")]
|
|
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_web=info",
|
|
1 => "info,shanty_web=debug",
|
|
_ => "debug,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 = MusicBrainzFetcher::new()?;
|
|
let search = MusicBrainzSearch::new()?;
|
|
let wiki_fetcher = WikipediaFetcher::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,
|
|
wiki_fetcher,
|
|
config: std::sync::Arc::new(tokio::sync::RwLock::new(config)),
|
|
config_path,
|
|
tasks: TaskManager::new(),
|
|
firefox_login: tokio::sync::Mutex::new(None),
|
|
scheduler: tokio::sync::Mutex::new(shanty_web::state::SchedulerInfo {
|
|
next_pipeline: None,
|
|
next_monitor: None,
|
|
}),
|
|
});
|
|
|
|
// Start background cookie refresh task
|
|
shanty_web::cookie_refresh::spawn(state.config.clone());
|
|
|
|
// Start pipeline and monitor schedulers
|
|
shanty_web::pipeline_scheduler::spawn(state.clone());
|
|
shanty_web::monitor::spawn(state.clone());
|
|
|
|
// Resolve static files directory relative to the binary location
|
|
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"));
|
|
|
|
// Also check next to the crate root (for development)
|
|
let static_dir = if static_dir.is_dir() {
|
|
static_dir
|
|
} else {
|
|
let dev_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("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 session_key = Key::generate();
|
|
|
|
HttpServer::new(move || {
|
|
let cors = Cors::permissive();
|
|
let static_dir = static_dir.clone();
|
|
|
|
App::new()
|
|
.wrap(cors)
|
|
.wrap(
|
|
SessionMiddleware::builder(CookieSessionStore::default(), session_key.clone())
|
|
.cookie_secure(false)
|
|
.build(),
|
|
)
|
|
.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),
|
|
)
|
|
// SPA fallback: serve index.html for any route not matched
|
|
// by API or static files, so client-side routing works on refresh
|
|
.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()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|