diff --git a/crates/domain/src/services/schedule/mod.rs b/crates/domain/src/services/schedule/mod.rs index edd8911..17f40df 100644 --- a/crates/domain/src/services/schedule/mod.rs +++ b/crates/domain/src/services/schedule/mod.rs @@ -64,116 +64,34 @@ impl ScheduleEngineService { channel_id: ChannelId, from: DateTime, ) -> DomainResult { - let channel = self - .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 channel = self.load_channel(channel_id).await?; let valid_until = from + Duration::days(SCHEDULE_DURATION_DAYS); - let start_date = from.with_timezone(&tz).date_naive(); - let end_date = valid_until.with_timezone(&tz).date_naive(); - - let mut slots: Vec = 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()); + let mut schedule = self + .build_schedule(&channel, channel.schedule_config(), from, valid_until) + .await?; if let Some(gap_filter) = channel.gap_filler() { let filler_items = self.query_gap_fillers(gap_filter).await?; 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?; for slot in schedule.slots() { 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?; } @@ -186,100 +104,10 @@ impl ScheduleEngineService { from: DateTime, duration_hours: u32, ) -> DomainResult { - let channel = self - .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 channel = self.load_channel(channel_id).await?; 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 mut slots: Vec = 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, - )) + self.build_schedule(&channel, channel.schedule_config(), from, valid_until) + .await } pub async fn preview_config( @@ -289,12 +117,30 @@ impl ScheduleEngineService { from: DateTime, duration_hours: u32, ) -> DomainResult { - let channel = self - .channel_query + let channel = self.load_channel(channel_id).await?; + 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 { + self.channel_query .find_by_id(channel_id) .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, + valid_until: DateTime, + ) -> DomainResult { + let channel_id = channel.id(); let tz: Tz = channel .timezone() .parse() @@ -304,6 +150,7 @@ impl ScheduleEngineService { .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() @@ -315,9 +162,7 @@ impl ScheduleEngineService { .find_last_slot_per_block(channel_id) .await?; - let valid_from = from; - let valid_until = from + Duration::hours(duration_hours as i64); - let start_date = from.with_timezone(&tz).date_naive(); + let start_date = valid_from.with_timezone(&tz).date_naive(); let end_date = valid_until.with_timezone(&tz).date_naive(); let mut slots: Vec = Vec::new(); @@ -325,7 +170,6 @@ impl ScheduleEngineService { while current_date <= end_date { let weekday = Weekday::from(current_date.weekday()); - for block in config.blocks_for(weekday) { let naive_start = current_date.and_time(block.start_time());