expand MCP tools: browse_library, stats, analyze, preview, suggest, config tools (#17)

This commit is contained in:
2026-07-12 14:34:27 +02:00
parent 6dbf7ab98c
commit 6b0d060945
5 changed files with 1267 additions and 25 deletions

View File

@@ -180,6 +180,211 @@ impl ScheduleEngineService {
Ok(schedule)
}
pub async fn preview_schedule(
&self,
channel_id: ChannelId,
from: DateTime<Utc>,
duration_hours: u32,
) -> DomainResult<GeneratedSchedule> {
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 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<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(
&self,
channel_id: ChannelId,
config: &crate::models::ScheduleConfig,
from: DateTime<Utc>,
duration_hours: u32,
) -> DomainResult<GeneratedSchedule> {
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 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<ScheduledSlot> = Vec::new();
let mut current_date = start_date;
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());
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 fn get_current_broadcast(
schedule: &GeneratedSchedule,
now: DateTime<Utc>,