feat(mcp): implement media channel management and scheduling features
This commit is contained in:
178
k-tv-backend/mcp/src/main.rs
Normal file
178
k-tv-backend/mcp/src/main.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
use domain::{
|
||||
ChannelService, DomainError, DomainResult, IMediaProvider, 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 maybe_provider: Option<Arc<dyn IMediaProvider>> = None;
|
||||
|
||||
#[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);
|
||||
maybe_provider = Some(Arc::new(infra::JellyfinMediaProvider::new(
|
||||
infra::JellyfinConfig {
|
||||
base_url,
|
||||
api_key,
|
||||
user_id,
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local-files")]
|
||||
if maybe_provider.is_none() {
|
||||
if let Some(dir) = std::env::var("LOCAL_FILES_DIR").ok().map(std::path::PathBuf::from) {
|
||||
if 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,
|
||||
};
|
||||
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; });
|
||||
maybe_provider = Some(Arc::new(infra::LocalFilesProvider::new(idx, lf_cfg)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let media_provider: Arc<dyn IMediaProvider> = maybe_provider.unwrap_or_else(|| {
|
||||
tracing::warn!("No media provider configured. Set JELLYFIN_BASE_URL or LOCAL_FILES_DIR.");
|
||||
Arc::new(NoopMediaProvider)
|
||||
});
|
||||
|
||||
let schedule_engine = ScheduleEngineService::new(
|
||||
Arc::clone(&media_provider),
|
||||
channel_repo,
|
||||
schedule_repo,
|
||||
);
|
||||
|
||||
let server = KTvMcpServer {
|
||||
channel_service: Arc::new(channel_service),
|
||||
schedule_engine: Arc::new(schedule_engine),
|
||||
media_provider,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user