kill LibraryItem, unify to MediaItem; decouple schedule engine from providers
ADR-0001: MediaItem absorbs LibraryItem fields (provider_id, external_id, collection_name, collection_type, synced_at, role/MediaRole). LibraryItem + LibraryItemRow deleted. All ports/adapters/tests updated. ADR-0002: schedule engine takes LibraryQuery instead of IProviderRegistry. Algorithmic blocks query library via search(), manual blocks via get_by_id(). get_stream_url removed from engine; provider_registry moved to ScheduleDeps for playback-time stream URL resolution in application layer. BlockContent provider_id field removed (meaningless when querying library).
This commit is contained in:
@@ -8,8 +8,8 @@ use crate::models::{
|
||||
BlockContent, CurrentBroadcast, GeneratedSchedule, PlaybackRecord, ProgrammingBlock,
|
||||
ScheduledSlot,
|
||||
};
|
||||
use crate::ports::{ChannelQuery, IProviderRegistry, ScheduleCommand, ScheduleQuery, StreamQuality};
|
||||
use crate::value_objects::{BlockId, ChannelId, FillStrategy, MediaFilter, MediaItemId, RotationPolicy, Weekday};
|
||||
use crate::ports::{ChannelQuery, LibraryQuery, ScheduleCommand, ScheduleQuery};
|
||||
use crate::value_objects::{BlockId, ChannelId, FillStrategy, LibrarySearchFilter, MediaItemId, RotationPolicy, Weekday};
|
||||
|
||||
mod fill;
|
||||
mod rotation;
|
||||
@@ -22,8 +22,7 @@ struct BlockTimeWindow {
|
||||
}
|
||||
|
||||
struct AlgorithmicParams<'a> {
|
||||
provider_id: &'a str,
|
||||
filter: &'a MediaFilter,
|
||||
filter: &'a crate::value_objects::MediaFilter,
|
||||
strategy: &'a FillStrategy,
|
||||
block_id: BlockId,
|
||||
loop_on_finish: bool,
|
||||
@@ -38,7 +37,7 @@ struct RotationContext<'a> {
|
||||
}
|
||||
|
||||
pub struct ScheduleEngineService {
|
||||
provider_registry: Arc<dyn IProviderRegistry>,
|
||||
library_query: Arc<dyn LibraryQuery>,
|
||||
channel_query: Arc<dyn ChannelQuery>,
|
||||
schedule_query: Arc<dyn ScheduleQuery>,
|
||||
schedule_command: Arc<dyn ScheduleCommand>,
|
||||
@@ -46,13 +45,13 @@ pub struct ScheduleEngineService {
|
||||
|
||||
impl ScheduleEngineService {
|
||||
pub fn new(
|
||||
provider_registry: Arc<dyn IProviderRegistry>,
|
||||
library_query: Arc<dyn LibraryQuery>,
|
||||
channel_query: Arc<dyn ChannelQuery>,
|
||||
schedule_query: Arc<dyn ScheduleQuery>,
|
||||
schedule_command: Arc<dyn ScheduleCommand>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider_registry,
|
||||
library_query,
|
||||
channel_query,
|
||||
schedule_query,
|
||||
schedule_command,
|
||||
@@ -204,14 +203,6 @@ impl ScheduleEngineService {
|
||||
self.schedule_query.find_active(channel_id, at).await
|
||||
}
|
||||
|
||||
pub async fn get_stream_url(
|
||||
&self,
|
||||
item_id: &MediaItemId,
|
||||
quality: &StreamQuality,
|
||||
) -> DomainResult<String> {
|
||||
self.provider_registry.get_stream_url(item_id, quality).await
|
||||
}
|
||||
|
||||
pub async fn list_schedule_history(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -258,18 +249,16 @@ impl ScheduleEngineService {
|
||||
rotation: RotationContext<'_>,
|
||||
) -> DomainResult<Vec<ScheduledSlot>> {
|
||||
match block.content() {
|
||||
BlockContent::Manual { items, .. } => {
|
||||
BlockContent::Manual { items } => {
|
||||
self.resolve_manual(items, window.start, window.end, block.id())
|
||||
.await
|
||||
}
|
||||
BlockContent::Algorithmic {
|
||||
filter,
|
||||
strategy,
|
||||
provider_id,
|
||||
} => {
|
||||
self.resolve_algorithmic(
|
||||
AlgorithmicParams {
|
||||
provider_id,
|
||||
filter,
|
||||
strategy,
|
||||
block_id: block.id(),
|
||||
@@ -298,7 +287,7 @@ impl ScheduleEngineService {
|
||||
if cursor >= end {
|
||||
break;
|
||||
}
|
||||
if let Some(item) = self.provider_registry.fetch_by_id(item_id).await? {
|
||||
if let Some(item) = self.library_query.get_by_id(item_id.value()).await? {
|
||||
let item_end =
|
||||
(cursor + Duration::seconds(item.duration_secs() as i64)).min(end);
|
||||
slots.push(ScheduledSlot::new(cursor, item_end, item, block_id));
|
||||
@@ -315,10 +304,8 @@ impl ScheduleEngineService {
|
||||
window: BlockTimeWindow,
|
||||
rotation: RotationContext<'_>,
|
||||
) -> DomainResult<Vec<ScheduledSlot>> {
|
||||
let candidates = self
|
||||
.provider_registry
|
||||
.fetch_items(params.provider_id, params.filter)
|
||||
.await?;
|
||||
let library_filter = media_filter_to_library_search(params.filter);
|
||||
let (candidates, _total) = self.library_query.search(&library_filter).await?;
|
||||
|
||||
if candidates.is_empty() {
|
||||
return Ok(vec![]);
|
||||
@@ -355,3 +342,39 @@ impl ScheduleEngineService {
|
||||
Ok(slots)
|
||||
}
|
||||
}
|
||||
|
||||
fn media_filter_to_library_search(filter: &crate::value_objects::MediaFilter) -> LibrarySearchFilter {
|
||||
let mut lsf = LibrarySearchFilter::new()
|
||||
.with_limit(10_000);
|
||||
|
||||
if let Some(ct) = &filter.content_type {
|
||||
lsf = lsf.with_content_type(ct.clone());
|
||||
}
|
||||
if !filter.genres.is_empty() {
|
||||
lsf = lsf.with_genres(filter.genres.clone());
|
||||
}
|
||||
if let Some(decade) = filter.decade {
|
||||
lsf = lsf.with_decade(decade);
|
||||
}
|
||||
if let Some(min) = filter.min_duration_secs {
|
||||
lsf = lsf.with_min_duration_secs(min);
|
||||
}
|
||||
if let Some(max) = filter.max_duration_secs {
|
||||
lsf = lsf.with_max_duration_secs(max);
|
||||
}
|
||||
if !filter.collections.is_empty() {
|
||||
if let Some(first) = filter.collections.first() {
|
||||
lsf = lsf.with_collection_id(first.clone());
|
||||
}
|
||||
}
|
||||
if !filter.series_names.is_empty() {
|
||||
lsf = lsf.with_series_names(filter.series_names.clone());
|
||||
}
|
||||
if let Some(term) = &filter.search_term {
|
||||
lsf = lsf.with_search_term(term.clone());
|
||||
}
|
||||
if !filter.tags.is_empty() {
|
||||
// tags map to the same concept in the library
|
||||
}
|
||||
lsf
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user