176 lines
6.2 KiB
Rust
176 lines
6.2 KiB
Rust
//! 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<dyn IMediaProvider>)>,
|
|
}
|
|
|
|
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<String>, provider: Arc<dyn IMediaProvider>) {
|
|
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<Arc<dyn IMediaProvider>> {
|
|
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<dyn IMediaProvider>)> {
|
|
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<MediaItem>) -> Vec<MediaItem> {
|
|
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<Vec<MediaItem>> {
|
|
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<Option<MediaItem>> {
|
|
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<String> {
|
|
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<String> {
|
|
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<ProviderCapabilities> {
|
|
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<Vec<Collection>> {
|
|
let (_, provider) = self.resolve_provider(provider_id)?;
|
|
provider.list_collections().await
|
|
}
|
|
|
|
async fn list_series(&self, provider_id: &str, collection_id: Option<&str>) -> DomainResult<Vec<SeriesSummary>> {
|
|
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<Vec<String>> {
|
|
let (_, provider) = self.resolve_provider(provider_id)?;
|
|
provider.list_genres(content_type).await
|
|
}
|
|
}
|