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:
2026-03-14 23:59:21 +01:00
parent c53892159a
commit ead65e6be2
21 changed files with 468 additions and 150 deletions

View File

@@ -10,7 +10,7 @@ use axum::http::{HeaderName, HeaderValue};
use std::sync::Arc;
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
use domain::{ChannelService, IMediaProvider, ProviderCapabilities, ScheduleEngineService, StreamingProtocol, UserService};
use domain::{ChannelService, IMediaProvider, IProviderRegistry, ProviderCapabilities, ScheduleEngineService, StreamingProtocol, UserService};
use infra::factory::{build_channel_repository, build_schedule_repository, build_user_repository};
use infra::run_migrations;
use k_core::http::server::{ServerConfig, apply_standard_middleware};
@@ -72,11 +72,11 @@ async fn main() -> anyhow::Result<()> {
let user_service = UserService::new(user_repo);
let channel_service = ChannelService::new(channel_repo.clone());
// Build media provider — Jellyfin → local-files → noop, first match wins.
// Build provider registry — all configured providers are registered simultaneously.
#[cfg(feature = "local-files")]
let mut local_index: Option<Arc<infra::LocalIndex>> = None;
let mut maybe_provider: Option<Arc<dyn IMediaProvider>> = None;
let mut registry = infra::ProviderRegistry::new();
#[cfg(feature = "jellyfin")]
if let (Some(base_url), Some(api_key), Some(user_id)) = (
@@ -85,7 +85,7 @@ async fn main() -> anyhow::Result<()> {
&config.jellyfin_user_id,
) {
tracing::info!("Media provider: Jellyfin at {}", base_url);
maybe_provider = Some(Arc::new(infra::JellyfinMediaProvider::new(infra::JellyfinConfig {
registry.register("jellyfin", Arc::new(infra::JellyfinMediaProvider::new(infra::JellyfinConfig {
base_url: base_url.clone(),
api_key: api_key.clone(),
user_id: user_id.clone(),
@@ -93,35 +93,33 @@ async fn main() -> anyhow::Result<()> {
}
#[cfg(feature = "local-files")]
if maybe_provider.is_none() {
if let Some(dir) = &config.local_files_dir {
if let k_core::db::DatabasePool::Sqlite(ref sqlite_pool) = db_pool {
tracing::info!("Media provider: local files at {:?}", dir);
let lf_cfg = infra::LocalFilesConfig {
root_dir: dir.clone(),
base_url: config.base_url.clone(),
};
let idx = Arc::new(infra::LocalIndex::new(&lf_cfg, sqlite_pool.clone()).await);
local_index = Some(Arc::clone(&idx));
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)));
} else {
tracing::warn!("local-files requires SQLite; ignoring LOCAL_FILES_DIR");
}
if let Some(dir) = &config.local_files_dir {
if let k_core::db::DatabasePool::Sqlite(ref sqlite_pool) = db_pool {
tracing::info!("Media provider: local files at {:?}", dir);
let lf_cfg = infra::LocalFilesConfig {
root_dir: dir.clone(),
base_url: config.base_url.clone(),
};
let idx = Arc::new(infra::LocalIndex::new(&lf_cfg, sqlite_pool.clone()).await);
local_index = Some(Arc::clone(&idx));
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)));
} else {
tracing::warn!("local-files requires SQLite; ignoring LOCAL_FILES_DIR");
}
}
let media_provider: Arc<dyn IMediaProvider> = maybe_provider.unwrap_or_else(|| {
tracing::warn!(
"No media provider configured. Set JELLYFIN_BASE_URL / LOCAL_FILES_DIR."
);
Arc::new(NoopMediaProvider)
});
if registry.is_empty() {
tracing::warn!("No media provider configured. Set JELLYFIN_BASE_URL / LOCAL_FILES_DIR.");
registry.register("noop", Arc::new(NoopMediaProvider));
}
let registry = Arc::new(registry);
let bg_channel_repo = channel_repo.clone();
let schedule_engine = ScheduleEngineService::new(
Arc::clone(&media_provider),
Arc::clone(&registry) as Arc<dyn IProviderRegistry>,
channel_repo,
schedule_repo,
);
@@ -131,7 +129,7 @@ async fn main() -> anyhow::Result<()> {
user_service,
channel_service,
schedule_engine,
media_provider,
registry,
config.clone(),
)
.await?;