wire interstitial insertion into schedule engine resolve_block

This commit is contained in:
2026-07-12 14:05:33 +02:00
parent 8e4f724562
commit 8edd7a9c0b
3 changed files with 234 additions and 6 deletions

View File

@@ -9,7 +9,7 @@ use crate::models::{
ScheduledSlot,
};
use crate::ports::{ChannelQuery, LibraryQuery, ScheduleCommand, ScheduleQuery};
use crate::value_objects::{BlockId, ChannelId, FillStrategy, LibrarySearchFilter, MediaItemId, RotationPolicy, Weekday};
use crate::value_objects::{BlockId, ChannelId, FillStrategy, InterstitialRule, LibrarySearchFilter, MediaItemId, MediaRole, RotationPolicy, Weekday};
mod fill;
mod rotation;
@@ -248,10 +248,10 @@ impl ScheduleEngineService {
window: BlockTimeWindow,
rotation: RotationContext<'_>,
) -> DomainResult<Vec<ScheduledSlot>> {
match block.content() {
let program_slots = match block.content() {
BlockContent::Manual { items } => {
self.resolve_manual(items, window.start, window.end, block.id())
.await
.await?
}
BlockContent::Algorithmic {
filter,
@@ -265,12 +265,85 @@ impl ScheduleEngineService {
loop_on_finish: block.loop_on_finish(),
ignore_rotation_policy: block.ignore_rotation_policy(),
},
window,
BlockTimeWindow {
start: window.start,
end: window.end,
},
rotation,
)
.await?
}
};
if let Some(rule) = block.interstitial_rule() {
self.insert_interstitials(program_slots, rule, block.id(), window.end)
.await
} else {
Ok(program_slots)
}
}
async fn insert_interstitials(
&self,
program_slots: Vec<ScheduledSlot>,
rule: &InterstitialRule,
block_id: BlockId,
block_end: DateTime<Utc>,
) -> DomainResult<Vec<ScheduledSlot>> {
if program_slots.len() < 2 {
return Ok(program_slots);
}
let mut filter = media_filter_to_library_search(rule.pool_filter());
filter = filter.with_role(MediaRole::Interstitial);
let (interstitials, _) = self.library_query.search(&filter).await?;
if interstitials.is_empty() {
return Ok(program_slots);
}
let mut result: Vec<ScheduledSlot> = Vec::new();
let mut cursor = program_slots[0].start_at();
let mut interstitial_idx = 0;
for (i, slot) in program_slots.iter().enumerate() {
if cursor >= block_end {
break;
}
let program_duration = slot.item().duration_secs();
let program_end = (cursor + Duration::seconds(program_duration as i64)).min(block_end);
result.push(ScheduledSlot::new(
cursor,
program_end,
slot.item().clone(),
block_id,
));
cursor = program_end;
let should_insert = i + 1 < program_slots.len()
&& program_duration >= rule.min_gap_secs()
&& cursor < block_end;
if should_insert {
let interstitial = &interstitials[interstitial_idx % interstitials.len()];
let interstitial_end =
(cursor + Duration::seconds(interstitial.duration_secs() as i64))
.min(block_end);
if interstitial_end > cursor {
result.push(ScheduledSlot::new(
cursor,
interstitial_end,
interstitial.clone(),
block_id,
));
cursor = interstitial_end;
interstitial_idx += 1;
}
}
}
Ok(result)
}
async fn resolve_manual(
@@ -304,7 +377,8 @@ impl ScheduleEngineService {
window: BlockTimeWindow,
rotation: RotationContext<'_>,
) -> DomainResult<Vec<ScheduledSlot>> {
let library_filter = media_filter_to_library_search(params.filter);
let library_filter =
media_filter_to_library_search(params.filter).with_role(MediaRole::Program);
let (candidates, _total) = self.library_query.search(&library_filter).await?;
if candidates.is_empty() {