use std::sync::Arc; use std::time::Duration as StdDuration; use domain::{ ChannelService, DomainError, DomainResult, IMediaProvider, IProviderRegistry, MediaFilter, MediaItemId, ProviderCapabilities, ScheduleEngineService, StreamQuality, StreamingProtocol, UserService, }; use infra::factory::{build_channel_repository, build_schedule_repository, build_user_repository}; use infra::run_migrations; use tracing::info; use uuid::Uuid; mod error; mod server; mod tools; use server::KTvMcpServer; #[tokio::main] async fn main() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive("mcp=info".parse().unwrap()), ) .init(); let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "sqlite:data.db?mode=rwc".to_string()); let owner_id: Uuid = std::env::var("MCP_USER_ID") .map_err(|_| anyhow::anyhow!("MCP_USER_ID env var is required (UUID of the user)"))? .parse() .map_err(|_| anyhow::anyhow!("MCP_USER_ID must be a valid UUID"))?; info!("Connecting to database: {}", database_url); #[cfg(all(feature = "sqlite", not(feature = "postgres")))] let db_type = k_core::db::DbType::Sqlite; #[cfg(all(feature = "postgres", not(feature = "sqlite")))] let db_type = k_core::db::DbType::Postgres; #[cfg(all(feature = "sqlite", feature = "postgres"))] let db_type = if database_url.starts_with("postgres") { k_core::db::DbType::Postgres } else { k_core::db::DbType::Sqlite }; let db_config = k_core::db::DatabaseConfig { db_type, url: database_url.clone(), max_connections: 5, min_connections: 1, acquire_timeout: StdDuration::from_secs(30), }; let db_pool = k_core::db::connect(&db_config).await?; run_migrations(&db_pool).await?; let user_repo = build_user_repository(&db_pool).await?; let channel_repo = build_channel_repository(&db_pool).await?; let schedule_repo = build_schedule_repository(&db_pool).await?; let _user_service = UserService::new(user_repo); let channel_service = ChannelService::new(channel_repo.clone()); let mut registry = infra::ProviderRegistry::new(); #[cfg(feature = "jellyfin")] { let base_url = std::env::var("JELLYFIN_BASE_URL").ok(); let api_key = std::env::var("JELLYFIN_API_KEY").ok(); let user_id = std::env::var("JELLYFIN_USER_ID").ok(); if let (Some(base_url), Some(api_key), Some(user_id)) = (base_url, api_key, user_id) { info!("Media provider: Jellyfin at {}", base_url); registry.register("jellyfin", Arc::new(infra::JellyfinMediaProvider::new( infra::JellyfinConfig { base_url, api_key, user_id }, ))); } } #[cfg(feature = "local-files")] if let Some(dir) = std::env::var("LOCAL_FILES_DIR").ok().map(std::path::PathBuf::from) { let k_core::db::DatabasePool::Sqlite(ref sqlite_pool) = db_pool; let base_url = std::env::var("BASE_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); let lf_cfg = infra::LocalFilesConfig { root_dir: dir, base_url, transcode_dir: None, cleanup_ttl_hours: 24, }; let idx = Arc::new(infra::LocalIndex::new(&lf_cfg, sqlite_pool.clone()).await); let scan_idx = Arc::clone(&idx); tokio::spawn(async move { scan_idx.rescan().await; }); registry.register("local", Arc::new(infra::LocalFilesProvider::new(idx, lf_cfg, None))); } if registry.is_empty() { tracing::warn!("No media provider configured. Set JELLYFIN_BASE_URL or LOCAL_FILES_DIR."); registry.register("noop", Arc::new(NoopMediaProvider)); } let registry = Arc::new(registry); let schedule_engine = ScheduleEngineService::new( Arc::clone(®istry) as Arc, channel_repo, schedule_repo, ); let server = KTvMcpServer { channel_service: Arc::new(channel_service), schedule_engine: Arc::new(schedule_engine), provider_registry: registry, owner_id, }; info!("K-TV MCP server starting (stdio transport), owner_id={}", owner_id); use rmcp::ServiceExt; let service = server .serve(rmcp::transport::stdio()) .await .inspect_err(|e| tracing::error!("MCP server error: {e}"))?; service.waiting().await?; Ok(()) } struct NoopMediaProvider; #[async_trait::async_trait] impl IMediaProvider for NoopMediaProvider { fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { collections: false, series: false, genres: false, tags: false, decade: false, search: false, streaming_protocol: StreamingProtocol::DirectFile, rescan: false, transcode: false, } } async fn fetch_items(&self, _: &MediaFilter) -> DomainResult> { Err(DomainError::InfrastructureError( "No media provider configured.".into(), )) } async fn fetch_by_id(&self, _: &MediaItemId) -> DomainResult> { Err(DomainError::InfrastructureError( "No media provider configured.".into(), )) } async fn get_stream_url( &self, _: &MediaItemId, _: &StreamQuality, ) -> DomainResult { Err(DomainError::InfrastructureError( "No media provider configured.".into(), )) } }