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 crate::entities::{
ScheduledSlot,
};
use crate::errors::{DomainError, DomainResult};
use crate::ports::{IMediaProvider, StreamQuality};
use crate::ports::{IProviderRegistry, StreamQuality};
use crate::repositories::{ChannelRepository, ScheduleRepository};
use crate::value_objects::{
BlockId, ChannelId, FillStrategy, MediaFilter, MediaItemId, RecyclePolicy,
@@ -26,19 +26,19 @@ mod recycle;
/// `ScheduledSlot`s via the `IMediaProvider`, and applying the `RecyclePolicy`
/// to avoid replaying recently aired items.
pub struct ScheduleEngineService {
media_provider: Arc<dyn IMediaProvider>,
provider_registry: Arc<dyn IProviderRegistry>,
channel_repo: Arc<dyn ChannelRepository>,
schedule_repo: Arc<dyn ScheduleRepository>,
}
impl ScheduleEngineService {
pub fn new(
media_provider: Arc<dyn IMediaProvider>,
provider_registry: Arc<dyn IProviderRegistry>,
channel_repo: Arc<dyn ChannelRepository>,
schedule_repo: Arc<dyn ScheduleRepository>,
) -> Self {
Self {
media_provider,
provider_registry,
channel_repo,
schedule_repo,
}
@@ -223,9 +223,9 @@ impl ScheduleEngineService {
self.schedule_repo.find_active(channel_id, at).await
}
/// Delegate stream URL resolution to the configured media provider.
/// Delegate stream URL resolution to the provider registry (routes via ID prefix).
pub async fn get_stream_url(&self, item_id: &MediaItemId, quality: &StreamQuality) -> DomainResult<String> {
self.media_provider.get_stream_url(item_id, quality).await
self.provider_registry.get_stream_url(item_id, quality).await
}
/// Return all slots that overlap the given time window — the EPG data.
@@ -256,12 +256,12 @@ impl ScheduleEngineService {
last_item_id: Option<&MediaItemId>,
) -> DomainResult<Vec<ScheduledSlot>> {
match &block.content {
BlockContent::Manual { items } => {
BlockContent::Manual { items, .. } => {
self.resolve_manual(items, start, end, block.id).await
}
BlockContent::Algorithmic { filter, strategy } => {
BlockContent::Algorithmic { filter, strategy, provider_id } => {
self.resolve_algorithmic(
filter, strategy, start, end, history, policy, generation,
provider_id, filter, strategy, start, end, history, policy, generation,
block.id, last_item_id,
block.loop_on_finish,
block.ignore_recycle_policy,
@@ -287,7 +287,7 @@ impl ScheduleEngineService {
if cursor >= end {
break;
}
if let Some(item) = self.media_provider.fetch_by_id(item_id).await? {
if let Some(item) = self.provider_registry.fetch_by_id(item_id).await? {
let item_end =
(cursor + Duration::seconds(item.duration_secs as i64)).min(end);
slots.push(ScheduledSlot {
@@ -312,6 +312,7 @@ impl ScheduleEngineService {
/// previous generation. Used only by `Sequential` for series continuity.
async fn resolve_algorithmic(
&self,
provider_id: &str,
filter: &MediaFilter,
strategy: &FillStrategy,
start: DateTime<Utc>,
@@ -327,7 +328,7 @@ impl ScheduleEngineService {
// `candidates` — all items matching the filter, in provider order.
// Kept separate from `pool` so Sequential can rotate through the full
// ordered list while still honouring cooldowns.
let candidates = self.media_provider.fetch_items(filter).await?;
let candidates = self.provider_registry.fetch_items(provider_id, filter).await?;
if candidates.is_empty() {
return Ok(vec![]);