use anyhow::{Context, Result}; use axum::{ extract::Path, middleware, response::Redirect, routing::{delete, get, post, put}, Router, response::Html, }; use std::{env, sync::Arc}; use tower::ServiceBuilder; use tower_http::{ cors::{Any, CorsLayer}, trace::TraceLayer, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod auth; mod db; mod email; mod upload; mod recurring; mod error; mod utils; mod handlers; mod models; mod services; mod app_state; use email::{EmailConfig, Mailer}; use app_state::AppState; #[tokio::main] async fn main() -> Result<()> { // Initialize tracing tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "church_api=debug,tower_http=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // Load environment variables dotenvy::dotenv().ok(); let database_url = env::var("DATABASE_URL").context("DATABASE_URL must be set")?; let jwt_secret = env::var("JWT_SECRET").context("JWT_SECRET must be set")?; // Initialize database // Database connection let pool = sqlx::PgPool::connect(&database_url) .await .context("Failed to connect to database")?; // Run migrations (disabled temporarily) // sqlx::migrate!("./migrations") // .run(&pool) // .await // .context("Failed to run migrations")?; let email_config = EmailConfig::from_env().map_err(|e| anyhow::anyhow!("Failed to load email config: {:?}", e))?; let mailer = Arc::new(Mailer::new(email_config).map_err(|e| anyhow::anyhow!("Failed to initialize mailer: {:?}", e))?); // Initialize Owncast service with default values let owncast_host = env::var("OWNCAST_HOST").unwrap_or_else(|_| "stream.rockvilletollandsda.church".to_string()); let stream_host = env::var("STREAM_HOST").unwrap_or_else(|_| "stream.rockvilletollandsda.church".to_string()); let owncast_service = Some(Arc::new(services::OwncastService::new(&owncast_host, &stream_host))); // Transcoding services removed - replaced by simple smart streaming system tracing::info!("🎬 Using direct AV1/H.264 smart streaming - no complex transcoding needed"); let state = AppState { pool: pool.clone(), jwt_secret, mailer, owncast_service, }; // Create protected admin routes let admin_routes = Router::new() .route("/users", get(handlers::auth::list_users)) .route("/bulletins", post(handlers::bulletins::create)) .route("/bulletins/:id", put(handlers::bulletins::update)) .route("/bulletins/:id", delete(handlers::bulletins::delete)) .route("/events", post(handlers::events::create)) .route("/events/pending", get(handlers::events::list_pending)) .route("/events/pending/:id/approve", post(handlers::events::approve)) .route("/events/pending/:id/reject", post(handlers::events::reject)) .route("/events/pending/:id", delete(handlers::events::delete_pending)) .route("/events/:id", put(handlers::events::update)) .route("/events/:id", delete(handlers::events::delete)) .route("/config", get(handlers::config::get_admin_config)) .route("/schedule", post(handlers::schedule::create_schedule)) .route("/schedule/:date", put(handlers::schedule::update_schedule)) .route("/schedule/:date", delete(handlers::schedule::delete_schedule)) .route("/schedule", get(handlers::schedule::list_schedules)) .route("/members", get(handlers::members::list)) .route("/members", post(handlers::members::create)) .route("/members/:id", delete(handlers::members::delete)) .route("/backup/create", post(handlers::backup::create_backup)) .route("/backup/list", get(handlers::backup::list_backups)) .route("/backup/cleanup", post(handlers::backup::cleanup_backups)) .route("/backup/now", post(handlers::backup::backup_now)) .layer(middleware::from_fn_with_state(state.clone(), auth::auth_middleware)); // Build our application with routes let app = Router::new() // Public routes (no auth required) .route("/api/auth/login", post(handlers::auth::login)) .route("/api/bulletins", get(handlers::bulletins::list)) .route("/api/bulletins/current", get(handlers::bulletins::current)) .route("/api/bulletins/next", get(handlers::bulletins::next)) .route("/api/bulletins/:id", get(handlers::bulletins::get)) .route("/api/events", get(handlers::events::list)) .route("/api/events/upcoming", get(handlers::events::upcoming)) .route("/api/events/featured", get(handlers::events::featured)) .route("/api/events/submit", post(handlers::events::submit)) .route("/api/events/:id", get(handlers::events::get)) .route("/api/config", get(handlers::config::get_public_config)) .route("/api/config/recurring-types", get(handlers::config::get_recurring_types)) .route("/api/collections/rtsda_android/records", get(handlers::legacy::android_update)) .route("/api/bible_verses/random", get(handlers::bible_verses::random)) .route("/api/bible_verses", get(handlers::bible_verses::list)) .route("/api/bible_verses/search", get(handlers::bible_verses::search)) .route("/api/contact", post(handlers::contact::submit_contact)) .route("/api/schedule", get(handlers::schedule::get_schedule)) .route("/api/conference-data", get(handlers::schedule::get_conference_data)) .route("/api/members/active", get(handlers::members::list_active)) // Hymnal API endpoints .route("/api/hymnals", get(handlers::hymnal::list_hymnals)) .route("/api/hymnals/:id", get(handlers::hymnal::get_hymnal)) .route("/api/hymnals/code/:code", get(handlers::hymnal::get_hymnal_by_code)) .route("/api/hymns", get(handlers::hymnal::list_hymns)) .route("/api/hymns/search", get(handlers::hymnal::search_hymns)) .route("/api/hymns/:hymnal_code/:number", get(handlers::hymnal::get_hymn_by_number)) .route("/api/hymnals/:id/themes", get(handlers::hymnal::list_thematic_lists)) .route("/api/hymnals/code/:code/themes", get(handlers::hymnal::list_thematic_lists_by_code)) .route("/api/responsive-readings", get(handlers::hymnal::list_responsive_readings)) .route("/api/responsive-readings/search", get(handlers::hymnal::search_responsive_readings)) .route("/api/responsive-readings/:number", get(handlers::hymnal::get_responsive_reading)) // New media library endpoints (replacing Jellyfin) .route("/api/sermons", get(handlers::media::list_sermons)) .route("/api/livestreams", get(handlers::media::list_livestreams)) .route("/api/media/items", get(handlers::media::list_media_items)) .route("/api/media/items/:id", get(handlers::media::get_media_item)) // 🎯 SMART STREAMING - AV1 direct, HLS for legacy (like Jellyfin but not shit) .route("/api/media/stream/:media_id", get(handlers::smart_streaming::smart_video_streaming)) .route("/api/media/stream/:media_id/playlist.m3u8", get(handlers::smart_streaming::generate_hls_playlist_for_segment_generation)) .route("/api/media/stream/:media_id/:segment_name", get(handlers::smart_streaming::serve_hls_segment)) .route("/api/media/:media_id/thumbnail", get(handlers::smart_streaming::serve_thumbnail)) // Legacy chunk streaming API removed - never released transcoding nightmare // Test pages .route("/chunk-test", get(serve_chunk_test_page)) .route("/smart-test", get(serve_smart_test_page)) .route("/api/media/thumbnail/:id", get(handlers::media::get_thumbnail)) // Legacy Jellyfin endpoints (keep for compatibility during transition) // Jellyfin debug routes removed - clients hit Jellyfin directly now .route("/api/stream/status", get(handlers::owncast::get_stream_status)) .route("/api/stream/live", get(handlers::owncast::get_live_status)) .route("/api/stream/hls/stream.m3u8", get(handlers::owncast::proxy_hls_playlist)) .route("/api/stream/hls/:variant/stream.m3u8", get(handlers::owncast::proxy_hls_variant)) .route("/api/stream/hls/:variant/:segment", get(handlers::owncast::proxy_hls_segment)) .route("/api/v1/stream/status", get(handlers::owncast::get_stream_status)) // V2 API routes with enhanced timezone handling .route("/api/v2/events", get(handlers::v2::events::list)) .route("/api/v2/events/upcoming", get(handlers::v2::events::get_upcoming)) .route("/api/v2/events/featured", get(handlers::v2::events::get_featured)) .route("/api/v2/events/submit", post(handlers::v2::events::submit)) .route("/api/v2/events/:id", get(handlers::v2::events::get_by_id)) .route("/api/v2/bulletins", get(handlers::v2::bulletins::list)) .route("/api/v2/bulletins/current", get(handlers::v2::bulletins::get_current)) .route("/api/v2/bulletins/next", get(handlers::v2::bulletins::get_next)) .route("/api/v2/bulletins/:id", get(handlers::v2::bulletins::get_by_id)) .route("/api/v2/bible_verses/random", get(handlers::v2::bible_verses::get_random)) .route("/api/v2/bible_verses", get(handlers::v2::bible_verses::list)) .route("/api/v2/bible_verses/search", get(handlers::v2::bible_verses::search)) .route("/api/v2/contact", post(handlers::v2::contact::submit_contact)) .route("/api/v2/schedule", get(handlers::v2::schedule::get_schedule)) .route("/api/v2/conference-data", get(handlers::v2::schedule::get_conference_data)) // Redirect /api/v1/* to /api/* .route("/api/v1/*path", get(redirect_v1_to_api)) // Mount protected admin routes (with chunk streaming cleanup) .nest("/api/admin", admin_routes) .nest("/api/upload", upload::routes()) .with_state(state) .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer( CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), ), ); // Start recurring events scheduler recurring::start_recurring_events_scheduler(pool.clone()).await; // Start database backup scheduler let backup_database_url = database_url.clone(); tokio::spawn(async move { use services::BackupScheduler; let scheduler = BackupScheduler::default_config(backup_database_url); if let Err(e) = scheduler.start().await { tracing::error!("Backup scheduler failed: {}", e); } }); // Start periodic media scanning (disabled initial scan for performance) let media_pool = pool.clone(); tokio::spawn(async move { use services::MediaScanner; let scanner = MediaScanner::new(media_pool); // Wait 30 seconds after startup before first scan to allow server to settle tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; // Periodic scanning every 10 minutes (reduced frequency for performance) let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(600)); loop { interval.tick().await; tracing::info!("Starting periodic media scan..."); if let Err(e) = scanner.scan_directory("/media/archive/jellyfin/sermons").await { tracing::error!("Periodic sermons scan failed: {:?}", e); } if let Err(e) = scanner.scan_directory("/media/archive/jellyfin/livestreams").await { tracing::error!("Periodic livestreams scan failed: {:?}", e); } tracing::info!("Periodic media scan completed"); } }); // Start Intel Arc A770 background thumbnail generation tokio::spawn(async move { use services::ThumbnailGenerator; // Wait 60 seconds after startup to allow server to settle and media scan to complete tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; tracing::info!("🚀 Starting Intel Arc A770 background thumbnail generation"); // Run thumbnail generation every 30 minutes let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1800)); loop { interval.tick().await; tracing::info!("📸 Starting Arc A770 thumbnail generation scan..."); // Generate thumbnails for sermons directory if let Err(e) = ThumbnailGenerator::scan_and_generate_missing_thumbnails( "/media/archive/jellyfin/sermons", &format!("{}/thumbnails", std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "/opt/rtsda/church-api/uploads".to_string())) ).await { tracing::error!("Sermons thumbnail generation failed: {:?}", e); } // Generate thumbnails for livestreams directory if let Err(e) = ThumbnailGenerator::scan_and_generate_missing_thumbnails( "/media/archive/jellyfin/livestreams", &format!("{}/thumbnails", std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "/opt/rtsda/church-api/uploads".to_string())) ).await { tracing::error!("Livestreams thumbnail generation failed: {:?}", e); } tracing::info!("📸 Arc A770 thumbnail generation scan completed"); } }); let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await?; tracing::info!("🚀 Church API server running on {}", listener.local_addr()?); axum::serve(listener, app).await?; Ok(()) } /// Serve the chunk streaming test page async fn serve_chunk_test_page() -> Html { let html = include_str!("../chunk_streaming_test.html"); Html(html.to_string()) } /// Serve the smart streaming test page async fn serve_smart_test_page() -> Html { let html = include_str!("../smart_streaming_test.html"); Html(html.to_string()) } /// Redirect /api/v1/* requests to /api/* async fn redirect_v1_to_api(Path(path): Path) -> Redirect { let new_path = format!("/api/{}", path); Redirect::permanent(&new_path) } #[cfg(test)] mod tests { use bcrypt::{hash, verify, DEFAULT_COST}; #[test] fn test_bcrypt() { let password = "test123"; let hashed = hash(password, DEFAULT_COST).unwrap(); println!("Hash: {}", hashed); assert!(verify(password, &hashed).unwrap()); } } #[cfg(test)] mod tests4 { use bcrypt::{hash, DEFAULT_COST}; #[test] fn generate_real_password_hash() { let password = "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"; let hashed = hash(password, DEFAULT_COST).unwrap(); println!("Hash for real password: {}", hashed); } }