use crate::context::AppContext; use crate::wrapup::{compute, queries::ComputeWrapUpQuery}; use domain::errors::DomainError; use domain::events::DomainEvent; use domain::models::wrapup::{DateRange, WrapUpReport, WrapUpScope, WrapUpStatus}; use domain::ports::{VideoRenderAssets, VideoRenderConfig}; use domain::value_objects::WrapUpId; pub async fn execute( ctx: &AppContext, wrapup_id: WrapUpId, user_id: Option, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate, ) -> Result<(), DomainError> { ctx.repos .wrapup_repo .update_status(&wrapup_id, &WrapUpStatus::Generating, None) .await?; let scope = match user_id { Some(uid) => WrapUpScope::User(uid), None => WrapUpScope::Global, }; let query = ComputeWrapUpQuery { scope, date_range: DateRange { start: start_date, end: end_date, }, }; match compute::execute(ctx, query).await { Ok(report) => { let json = serde_json::to_string(&report) .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; ctx.repos .wrapup_repo .set_complete(&wrapup_id, &json) .await?; if let Some(ref renderer) = ctx.services.video_renderer { let poster_images = resolve_images(ctx, &report.poster_paths, "poster").await; let cast_images = resolve_images(ctx, &report.top_cast_profile_paths, "cast").await; let wc = &ctx.config.wrapup; let config = VideoRenderConfig { slide_duration_secs: 4, transition_duration_secs: 0.8, resolution: (1080, 1920), ffmpeg_path: wc.ffmpeg_path.clone(), font_path: wc.font_path.clone(), logo_path: wc.logo_path.clone(), bg_dir: wc.bg_dir.clone(), }; let assets = VideoRenderAssets { poster_images, cast_images, }; match renderer.render(&report, assets, &config).await { Ok(video_bytes) => { let video_key = format!("wrapups/{}/video.mp4", wrapup_id.value()); if let Err(e) = ctx .services .image_storage .store(&video_key, &video_bytes) .await { tracing::warn!("failed to store wrapup video: {e}"); } } Err(e) => { tracing::warn!("video render failed (non-fatal): {e}"); } } } ctx.services .event_publisher .publish(&DomainEvent::WrapUpCompleted { wrapup_id }) .await?; Ok(()) } Err(e) => { ctx.repos .wrapup_repo .update_status(&wrapup_id, &WrapUpStatus::Failed, Some(&e.to_string())) .await?; Err(e) } } } async fn resolve_images( ctx: &AppContext, paths: &[String], label: &str, ) -> Vec<(String, Vec)> { let mut images = Vec::new(); for path in paths.iter().take(20) { match ctx.services.image_storage.get(path).await { Ok(bytes) => images.push((path.clone(), bytes)), Err(e) => tracing::debug!("{label} fetch skipped for {path}: {e}"), } } tracing::info!("resolved {}/{} {label} images", images.len(), paths.len()); images }