feat: implement multi-provider support in media library
- Introduced IProviderRegistry to manage multiple media providers. - Updated AppState to use provider_registry instead of a single media_provider. - Refactored library routes to support provider-specific queries for collections, series, genres, and items. - Enhanced ProgrammingBlock to include provider_id for algorithmic and manual content types. - Modified frontend components to allow selection of providers and updated API calls to include provider parameters. - Adjusted hooks and types to accommodate provider-specific functionality.
This commit is contained in:
@@ -2,8 +2,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
use domain::{
|
||||
ChannelService, DomainError, DomainResult, IMediaProvider, MediaFilter, MediaItemId,
|
||||
ProviderCapabilities, ScheduleEngineService, StreamQuality, StreamingProtocol, UserService,
|
||||
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;
|
||||
@@ -69,7 +70,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let _user_service = UserService::new(user_repo);
|
||||
let channel_service = ChannelService::new(channel_repo.clone());
|
||||
|
||||
let mut maybe_provider: Option<Arc<dyn IMediaProvider>> = None;
|
||||
let mut registry = infra::ProviderRegistry::new();
|
||||
|
||||
#[cfg(feature = "jellyfin")]
|
||||
{
|
||||
@@ -78,41 +79,34 @@ async fn main() -> anyhow::Result<()> {
|
||||
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,
|
||||
},
|
||||
registry.register("jellyfin", 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)));
|
||||
}
|
||||
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; });
|
||||
registry.register("local", Arc::new(infra::LocalFilesProvider::new(idx, lf_cfg)));
|
||||
}
|
||||
}
|
||||
|
||||
let media_provider: Arc<dyn IMediaProvider> = maybe_provider.unwrap_or_else(|| {
|
||||
if registry.is_empty() {
|
||||
tracing::warn!("No media provider configured. Set JELLYFIN_BASE_URL or LOCAL_FILES_DIR.");
|
||||
Arc::new(NoopMediaProvider)
|
||||
});
|
||||
registry.register("noop", Arc::new(NoopMediaProvider));
|
||||
}
|
||||
|
||||
let registry = Arc::new(registry);
|
||||
|
||||
let schedule_engine = ScheduleEngineService::new(
|
||||
Arc::clone(&media_provider),
|
||||
Arc::clone(®istry) as Arc<dyn IProviderRegistry>,
|
||||
channel_repo,
|
||||
schedule_repo,
|
||||
);
|
||||
@@ -120,7 +114,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let server = KTvMcpServer {
|
||||
channel_service: Arc::new(channel_service),
|
||||
schedule_engine: Arc::new(schedule_engine),
|
||||
media_provider,
|
||||
provider_registry: registry,
|
||||
owner_id,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
ChannelService, ContentType, IMediaProvider, ProgrammingBlock, ScheduleConfig,
|
||||
ScheduleEngineService,
|
||||
ChannelService, ContentType, ProgrammingBlock, ScheduleConfig, ScheduleEngineService,
|
||||
};
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
@@ -19,7 +18,7 @@ use crate::tools::{channels, library, schedule};
|
||||
pub struct KTvMcpServer {
|
||||
pub channel_service: Arc<ChannelService>,
|
||||
pub schedule_engine: Arc<ScheduleEngineService>,
|
||||
pub media_provider: Arc<dyn IMediaProvider>,
|
||||
pub provider_registry: Arc<infra::ProviderRegistry>,
|
||||
pub owner_id: Uuid,
|
||||
}
|
||||
|
||||
@@ -244,7 +243,7 @@ impl KTvMcpServer {
|
||||
|
||||
#[tool(description = "List media collections/libraries available in the configured provider")]
|
||||
async fn list_collections(&self) -> String {
|
||||
library::list_collections(&self.media_provider).await
|
||||
library::list_collections(&self.provider_registry).await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
@@ -252,7 +251,7 @@ impl KTvMcpServer {
|
||||
)]
|
||||
async fn list_genres(&self, #[tool(aggr)] p: ListGenresParams) -> String {
|
||||
let ct = p.content_type.as_deref().and_then(parse_content_type);
|
||||
library::list_genres(&self.media_provider, ct).await
|
||||
library::list_genres(&self.provider_registry, ct).await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
@@ -261,7 +260,7 @@ impl KTvMcpServer {
|
||||
async fn search_media(&self, #[tool(aggr)] p: SearchMediaParams) -> String {
|
||||
let ct = p.content_type.as_deref().and_then(parse_content_type);
|
||||
library::search_media(
|
||||
&self.media_provider,
|
||||
&self.provider_registry,
|
||||
ct,
|
||||
p.genres.unwrap_or_default(),
|
||||
p.search_term,
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use domain::{ContentType, IMediaProvider, MediaFilter};
|
||||
use domain::{ContentType, IProviderRegistry, MediaFilter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::{domain_err, ok_json};
|
||||
|
||||
pub async fn list_collections(provider: &Arc<dyn IMediaProvider>) -> String {
|
||||
match provider.list_collections().await {
|
||||
pub async fn list_collections(registry: &Arc<infra::ProviderRegistry>) -> String {
|
||||
match registry.list_collections("").await {
|
||||
Ok(cols) => ok_json(&cols),
|
||||
Err(e) => domain_err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_genres(
|
||||
provider: &Arc<dyn IMediaProvider>,
|
||||
registry: &Arc<infra::ProviderRegistry>,
|
||||
content_type: Option<ContentType>,
|
||||
) -> String {
|
||||
match provider.list_genres(content_type.as_ref()).await {
|
||||
match registry.list_genres("", content_type.as_ref()).await {
|
||||
Ok(genres) => ok_json(&genres),
|
||||
Err(e) => domain_err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search_media(
|
||||
provider: &Arc<dyn IMediaProvider>,
|
||||
registry: &Arc<infra::ProviderRegistry>,
|
||||
content_type: Option<ContentType>,
|
||||
genres: Vec<String>,
|
||||
search_term: Option<String>,
|
||||
@@ -36,7 +36,7 @@ pub async fn search_media(
|
||||
collections,
|
||||
..Default::default()
|
||||
};
|
||||
match provider.fetch_items(&filter).await {
|
||||
match registry.fetch_items("", &filter).await {
|
||||
Ok(items) => ok_json(&items),
|
||||
Err(e) => domain_err(e),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user