refactor: extract build_schedule helper, deduplicate generate/preview/preview_config
This commit is contained in:
@@ -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());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user