178 lines
5.7 KiB
Rust
178 lines
5.7 KiB
Rust
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<dyn IProviderRegistry>,
|
|
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<Vec<domain::MediaItem>> {
|
|
Err(DomainError::InfrastructureError(
|
|
"No media provider configured.".into(),
|
|
))
|
|
}
|
|
|
|
async fn fetch_by_id(&self, _: &MediaItemId) -> DomainResult<Option<domain::MediaItem>> {
|
|
Err(DomainError::InfrastructureError(
|
|
"No media provider configured.".into(),
|
|
))
|
|
}
|
|
|
|
async fn get_stream_url(
|
|
&self,
|
|
_: &MediaItemId,
|
|
_: &StreamQuality,
|
|
) -> DomainResult<String> {
|
|
Err(DomainError::InfrastructureError(
|
|
"No media provider configured.".into(),
|
|
))
|
|
}
|
|
}
|