//! Provider registry — routes media operations to the correct named provider. //! //! Item IDs are prefixed with the provider key separated by `::`, e.g. //! `"jellyfin::abc123"` or `"local::base64path"`. The registry strips the //! prefix before calling the underlying provider and re-stamps returned IDs //! so every item is self-routing throughout its lifetime. //! //! An empty prefix (un-prefixed IDs from old data, or new blocks with no //! `provider_id` set) falls back to the primary (first-registered) provider. use std::sync::Arc; use async_trait::async_trait; use domain::errors::{DomainError, DomainResult}; use domain::ports::{ Collection, IMediaProvider, IProviderRegistry, ProviderCapabilities, SeriesSummary, StreamQuality, }; use domain::{ContentType, MediaFilter, MediaItem, MediaItemId}; /// Registry of named media providers. /// /// Providers are registered with a short key (e.g. `"jellyfin"`, `"local"`). /// The first registered provider is the *primary* — it handles un-prefixed IDs /// and empty `provider_id` strings for backward compatibility. pub struct ProviderRegistry { /// Ordered list of `(key, provider)` pairs. Order determines the primary. providers: Vec<(String, Arc)>, } impl ProviderRegistry { pub fn new() -> Self { Self { providers: Vec::new() } } /// Register a provider under `id`. The first registered becomes the primary. pub fn register(&mut self, id: impl Into, provider: Arc) { self.providers.push((id.into(), provider)); } pub fn is_empty(&self) -> bool { self.providers.is_empty() } /// Return the provider registered under `id`, if any. pub fn get_provider(&self, id: &str) -> Option> { self.providers .iter() .find(|(pid, _)| pid == id) .map(|(_, p)| Arc::clone(p)) } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- fn prefix_id(provider_id: &str, raw_id: &str) -> MediaItemId { MediaItemId::new(format!("{}::{}", provider_id, raw_id)) } /// Split `"provider_key::raw_id"` into `(key, raw_id)`. /// Un-prefixed IDs return `("", full_id)` → primary provider fallback. fn parse_prefix(id: &MediaItemId) -> (&str, &str) { let s: &str = id.as_ref(); match s.find("::") { Some(pos) => (&s[..pos], &s[pos + 2..]), None => ("", s), } } /// Resolve a provider key to the provider, defaulting to primary on empty key. /// Returns `(resolved_key, provider)` so the caller can re-stamp IDs. fn resolve_provider<'a>( &'a self, provider_id: &str, ) -> DomainResult<(&'a str, &'a Arc)> { if provider_id.is_empty() { self.providers .first() .map(|(id, p)| (id.as_str(), p)) .ok_or_else(|| DomainError::InfrastructureError("No providers registered".into())) } else { self.providers .iter() .find(|(id, _)| id == provider_id) .map(|(id, p)| (id.as_str(), p)) .ok_or_else(|| { DomainError::InfrastructureError( format!("Provider '{}' not found", provider_id), ) }) } } fn wrap_items(provider_id: &str, items: Vec) -> Vec { items .into_iter() .map(|mut item| { item.id = Self::prefix_id(provider_id, item.id.as_ref()); item }) .collect() } } impl Default for ProviderRegistry { fn default() -> Self { Self::new() } } #[async_trait] impl IProviderRegistry for ProviderRegistry { async fn fetch_items(&self, provider_id: &str, filter: &MediaFilter) -> DomainResult> { let (pid, provider) = self.resolve_provider(provider_id)?; let items = provider.fetch_items(filter).await?; Ok(Self::wrap_items(pid, items)) } async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult> { let (prefix, raw) = Self::parse_prefix(item_id); let (pid, provider) = self.resolve_provider(prefix)?; let raw_id = MediaItemId::new(raw); let result = provider.fetch_by_id(&raw_id).await?; Ok(result.map(|mut item| { item.id = Self::prefix_id(pid, item.id.as_ref()); item })) } async fn get_stream_url(&self, item_id: &MediaItemId, quality: &StreamQuality) -> DomainResult { let (prefix, raw) = Self::parse_prefix(item_id); let (_, provider) = self.resolve_provider(prefix)?; let raw_id = MediaItemId::new(raw); provider.get_stream_url(&raw_id, quality).await } fn provider_ids(&self) -> Vec { self.providers.iter().map(|(id, _)| id.clone()).collect() } fn primary_id(&self) -> &str { self.providers .first() .map(|(id, _)| id.as_str()) .unwrap_or("") } fn capabilities(&self, provider_id: &str) -> Option { let target = if provider_id.is_empty() { self.providers.first().map(|(id, _)| id.as_str())? } else { provider_id }; self.providers .iter() .find(|(id, _)| id == target) .map(|(_, p)| p.capabilities()) } async fn list_collections(&self, provider_id: &str) -> DomainResult> { let (_, provider) = self.resolve_provider(provider_id)?; provider.list_collections().await } async fn list_series(&self, provider_id: &str, collection_id: Option<&str>) -> DomainResult> { let (_, provider) = self.resolve_provider(provider_id)?; provider.list_series(collection_id).await } async fn list_genres(&self, provider_id: &str, content_type: Option<&ContentType>) -> DomainResult> { let (_, provider) = self.resolve_provider(provider_id)?; provider.list_genres(content_type).await } }