refactor: extract build_schedule helper, deduplicate generate/preview/preview_config

This commit is contained in:
2026-07-12 15:26:30 +02:00
parent 8de7d33007
commit 5561b70e1b

View File

@@ -64,116 +64,34 @@ impl ScheduleEngineService {
channel_id: ChannelId, channel_id: ChannelId,
from: DateTime<Utc>, from: DateTime<Utc>,
) -> DomainResult<GeneratedSchedule> { ) -> DomainResult<GeneratedSchedule> {
let channel = self let channel = self.load_channel(channel_id).await?;
.channel_query
.find_by_id(channel_id)
.await?
.ok_or(DomainError::ChannelNotFound(channel_id))?;
let tz: Tz = channel
.timezone()
.parse()
.map_err(|_| DomainError::TimezoneError(channel.timezone().to_owned()))?;
let history = self
.schedule_query
.find_playback_history(channel_id)
.await?;
let latest_schedule = self.schedule_query.find_latest(channel_id).await?;
let generation = latest_schedule
.as_ref()
.map(|s| s.generation() + 1)
.unwrap_or(1);
let mut block_continuity = self
.schedule_query
.find_last_slot_per_block(channel_id)
.await?;
let valid_from = from;
let valid_until = from + Duration::days(SCHEDULE_DURATION_DAYS); let valid_until = from + Duration::days(SCHEDULE_DURATION_DAYS);
let start_date = from.with_timezone(&tz).date_naive(); let mut schedule = self
let end_date = valid_until.with_timezone(&tz).date_naive(); .build_schedule(&channel, channel.schedule_config(), from, valid_until)
.await?;
let mut slots: Vec<ScheduledSlot> = Vec::new();
let mut current_date = start_date;
while current_date <= end_date {
let weekday = Weekday::from(current_date.weekday());
for block in channel.schedule_config().blocks_for(weekday) {
let naive_start = current_date.and_time(block.start_time());
// earliest() picks first valid mapping, skipping DST gaps
let block_start_utc = match tz.from_local_datetime(&naive_start).earliest() {
Some(dt) => dt.with_timezone(&Utc),
None => continue,
};
let block_end_utc =
block_start_utc + Duration::minutes(block.duration_mins() as i64);
let slot_start = block_start_utc.max(valid_from);
let slot_end = block_end_utc.min(valid_until);
if slot_end <= slot_start {
continue;
}
let last_item_id = block_continuity.get(&block.id());
let mut block_slots = self
.resolve_block(
block,
BlockTimeWindow {
start: slot_start,
end: slot_end,
},
RotationContext {
history: &history,
policy: channel.rotation_policy(),
generation,
last_item_id,
},
)
.await?;
if let Some(last_slot) = block_slots.last() {
block_continuity.insert(block.id(), last_slot.item().id().clone());
}
slots.append(&mut block_slots);
}
current_date = current_date.succ_opt().ok_or_else(|| {
DomainError::validation("Date overflow during schedule generation")
})?;
}
slots.sort_by_key(|s| s.start_at());
if let Some(gap_filter) = channel.gap_filler() { if let Some(gap_filter) = channel.gap_filler() {
let filler_items = self.query_gap_fillers(gap_filter).await?; let filler_items = self.query_gap_fillers(gap_filter).await?;
if !filler_items.is_empty() { if !filler_items.is_empty() {
Self::fill_gaps(&mut slots, &filler_items, valid_from, valid_until); let generation = schedule.generation();
let mut slots = schedule.into_slots();
Self::fill_gaps(&mut slots, &filler_items, from, valid_until);
schedule = GeneratedSchedule::new(
channel_id,
from,
valid_until,
generation,
slots,
);
} }
} }
let schedule = GeneratedSchedule::new(
channel_id,
valid_from,
valid_until,
generation,
slots,
);
self.schedule_command.save(&schedule).await?; self.schedule_command.save(&schedule).await?;
for slot in schedule.slots() { for slot in schedule.slots() {
let record = let record =
PlaybackRecord::new(channel_id, slot.item().id().clone(), generation); PlaybackRecord::new(channel_id, slot.item().id().clone(), schedule.generation());
self.schedule_command.save_playback_record(&record).await?; self.schedule_command.save_playback_record(&record).await?;
} }
@@ -186,100 +104,10 @@ impl ScheduleEngineService {
from: DateTime<Utc>, from: DateTime<Utc>,
duration_hours: u32, duration_hours: u32,
) -> DomainResult<GeneratedSchedule> { ) -> DomainResult<GeneratedSchedule> {
let channel = self let channel = self.load_channel(channel_id).await?;
.channel_query
.find_by_id(channel_id)
.await?
.ok_or(DomainError::ChannelNotFound(channel_id))?;
let tz: Tz = channel
.timezone()
.parse()
.map_err(|_| DomainError::TimezoneError(channel.timezone().to_owned()))?;
let history = self
.schedule_query
.find_playback_history(channel_id)
.await?;
let latest_schedule = self.schedule_query.find_latest(channel_id).await?;
let generation = latest_schedule
.as_ref()
.map(|s| s.generation() + 1)
.unwrap_or(1);
let mut block_continuity = self
.schedule_query
.find_last_slot_per_block(channel_id)
.await?;
let valid_from = from;
let valid_until = from + Duration::hours(duration_hours as i64); let valid_until = from + Duration::hours(duration_hours as i64);
let start_date = from.with_timezone(&tz).date_naive(); self.build_schedule(&channel, channel.schedule_config(), from, valid_until)
let end_date = valid_until.with_timezone(&tz).date_naive(); .await
let mut slots: Vec<ScheduledSlot> = Vec::new();
let mut current_date = start_date;
while current_date <= end_date {
let weekday = Weekday::from(current_date.weekday());
for block in channel.schedule_config().blocks_for(weekday) {
let naive_start = current_date.and_time(block.start_time());
let block_start_utc = match tz.from_local_datetime(&naive_start).earliest() {
Some(dt) => dt.with_timezone(&Utc),
None => continue,
};
let block_end_utc =
block_start_utc + Duration::minutes(block.duration_mins() as i64);
let slot_start = block_start_utc.max(valid_from);
let slot_end = block_end_utc.min(valid_until);
if slot_end <= slot_start {
continue;
}
let last_item_id = block_continuity.get(&block.id());
let mut block_slots = self
.resolve_block(
block,
BlockTimeWindow {
start: slot_start,
end: slot_end,
},
RotationContext {
history: &history,
policy: channel.rotation_policy(),
generation,
last_item_id,
},
)
.await?;
if let Some(last_slot) = block_slots.last() {
block_continuity.insert(block.id(), last_slot.item().id().clone());
}
slots.append(&mut block_slots);
}
current_date = current_date.succ_opt().ok_or_else(|| {
DomainError::validation("Date overflow during schedule generation")
})?;
}
slots.sort_by_key(|s| s.start_at());
Ok(GeneratedSchedule::new(
channel_id,
valid_from,
valid_until,
generation,
slots,
))
} }
pub async fn preview_config( pub async fn preview_config(
@@ -289,12 +117,30 @@ impl ScheduleEngineService {
from: DateTime<Utc>, from: DateTime<Utc>,
duration_hours: u32, duration_hours: u32,
) -> DomainResult<GeneratedSchedule> { ) -> DomainResult<GeneratedSchedule> {
let channel = self let channel = self.load_channel(channel_id).await?;
.channel_query let valid_until = from + Duration::hours(duration_hours as i64);
self.build_schedule(&channel, config, from, valid_until)
.await
}
async fn load_channel(
&self,
channel_id: ChannelId,
) -> DomainResult<crate::models::Channel> {
self.channel_query
.find_by_id(channel_id) .find_by_id(channel_id)
.await? .await?
.ok_or(DomainError::ChannelNotFound(channel_id))?; .ok_or(DomainError::ChannelNotFound(channel_id))
}
async fn build_schedule(
&self,
channel: &crate::models::Channel,
config: &crate::models::ScheduleConfig,
valid_from: DateTime<Utc>,
valid_until: DateTime<Utc>,
) -> DomainResult<GeneratedSchedule> {
let channel_id = channel.id();
let tz: Tz = channel let tz: Tz = channel
.timezone() .timezone()
.parse() .parse()
@@ -304,6 +150,7 @@ impl ScheduleEngineService {
.schedule_query .schedule_query
.find_playback_history(channel_id) .find_playback_history(channel_id)
.await?; .await?;
let latest_schedule = self.schedule_query.find_latest(channel_id).await?; let latest_schedule = self.schedule_query.find_latest(channel_id).await?;
let generation = latest_schedule let generation = latest_schedule
.as_ref() .as_ref()
@@ -315,9 +162,7 @@ impl ScheduleEngineService {
.find_last_slot_per_block(channel_id) .find_last_slot_per_block(channel_id)
.await?; .await?;
let valid_from = from; let start_date = valid_from.with_timezone(&tz).date_naive();
let valid_until = from + Duration::hours(duration_hours as i64);
let start_date = from.with_timezone(&tz).date_naive();
let end_date = valid_until.with_timezone(&tz).date_naive(); let end_date = valid_until.with_timezone(&tz).date_naive();
let mut slots: Vec<ScheduledSlot> = Vec::new(); let mut slots: Vec<ScheduledSlot> = Vec::new();
@@ -325,7 +170,6 @@ impl ScheduleEngineService {
while current_date <= end_date { while current_date <= end_date {
let weekday = Weekday::from(current_date.weekday()); let weekday = Weekday::from(current_date.weekday());
for block in config.blocks_for(weekday) { for block in config.blocks_for(weekday) {
let naive_start = current_date.and_time(block.start_time()); let naive_start = current_date.and_time(block.start_time());