feat: multi-instance provider support

- provider_configs: add id TEXT PK; migrate existing rows (provider_type becomes id)
- local_files_index: add provider_id column + index; scope all queries per instance
- ProviderConfigRow: add id field; add get_by_id to trait
- LocalIndex:🆕 add provider_id param; all SQL scoped by provider_id
- factory: thread provider_id through build_local_files_bundle
- AppState.local_index: Option<Arc<LocalIndex>> → HashMap<String, Arc<LocalIndex>>
- admin_providers: restructured routes (POST /admin/providers create, PUT/DELETE /{id}, POST /test)
- admin_providers: use row.id as registry key for jellyfin and local_files
- files.rescan: optional ?provider=<id> query param
- frontend: add id to ProviderConfig, update api/hooks, new multi-instance panel UX
This commit is contained in:
2026-03-19 22:54:41 +01:00
parent 373e1c7c0a
commit 311fdd4006
14 changed files with 563 additions and 111 deletions

View File

@@ -14,7 +14,7 @@ use infra::factory::build_transcode_settings_repository;
pub struct ProviderBundle {
pub registry: Arc<infra::ProviderRegistry>,
#[cfg(feature = "local-files")]
pub local_index: Option<Arc<infra::LocalIndex>>,
pub local_index: std::collections::HashMap<String, Arc<infra::LocalIndex>>,
#[cfg(feature = "local-files")]
pub transcode_manager: Option<Arc<infra::TranscodeManager>>,
}
@@ -26,7 +26,7 @@ pub async fn build_provider_registry(
provider_config_repo: &Arc<dyn ProviderConfigRepository>,
) -> anyhow::Result<ProviderBundle> {
#[cfg(feature = "local-files")]
let mut local_index: Option<Arc<infra::LocalIndex>> = None;
let mut local_index: std::collections::HashMap<String, Arc<infra::LocalIndex>> = std::collections::HashMap::new();
#[cfg(feature = "local-files")]
let mut transcode_manager: Option<Arc<infra::TranscodeManager>> = None;
@@ -41,8 +41,8 @@ pub async fn build_provider_registry(
#[cfg(feature = "jellyfin")]
"jellyfin" => {
if let Ok(cfg) = serde_json::from_str::<infra::JellyfinConfig>(&row.config_json) {
tracing::info!("Loading Jellyfin provider from DB config");
registry.register("jellyfin", Arc::new(infra::JellyfinMediaProvider::new(cfg)));
tracing::info!("Loading Jellyfin provider [{}] from DB config", row.id);
registry.register(&row.id, Arc::new(infra::JellyfinMediaProvider::new(cfg)));
}
}
#[cfg(feature = "local-files")]
@@ -56,19 +56,20 @@ pub async fn build_provider_registry(
let cleanup_ttl_hours: u32 = cfg_map.get("cleanup_ttl_hours")
.and_then(|s| s.parse().ok())
.unwrap_or(24);
tracing::info!("Loading local-files provider from DB config at {:?}", files_dir);
tracing::info!("Loading local-files provider [{}] from DB config at {:?}", row.id, files_dir);
match infra::factory::build_local_files_bundle(
db_pool,
std::path::PathBuf::from(files_dir),
transcode_dir,
cleanup_ttl_hours,
config.base_url.clone(),
&row.id,
).await {
Ok(bundle) => {
let scan_idx = Arc::clone(&bundle.local_index);
tokio::spawn(async move { scan_idx.rescan().await; });
if let Some(ref tm) = bundle.transcode_manager {
tracing::info!("Transcoding enabled");
tracing::info!("Transcoding enabled for [{}]", row.id);
// Load persisted TTL override from transcode_settings table.
let tm_clone = Arc::clone(tm);
let repo = build_transcode_settings_repository(db_pool).await.ok();
@@ -80,11 +81,13 @@ pub async fn build_provider_registry(
}
});
}
registry.register("local", bundle.provider);
transcode_manager = bundle.transcode_manager;
local_index = Some(bundle.local_index);
registry.register(&row.id, bundle.provider);
if transcode_manager.is_none() {
transcode_manager = bundle.transcode_manager;
}
local_index.insert(row.id.clone(), bundle.local_index);
}
Err(e) => tracing::warn!("Failed to build local-files provider: {}", e),
Err(e) => tracing::warn!("Failed to build local-files provider [{}]: {}", row.id, e),
}
}
}
@@ -115,6 +118,7 @@ pub async fn build_provider_registry(
config.transcode_dir.clone(),
config.transcode_cleanup_ttl_hours,
config.base_url.clone(),
"local",
).await {
Ok(bundle) => {
let scan_idx = Arc::clone(&bundle.local_index);
@@ -133,7 +137,7 @@ pub async fn build_provider_registry(
}
registry.register("local", bundle.provider);
transcode_manager = bundle.transcode_manager;
local_index = Some(bundle.local_index);
local_index.insert("local".to_string(), bundle.local_index);
}
Err(e) => tracing::warn!("local-files requires SQLite; ignoring LOCAL_FILES_DIR: {}", e),
}