fix: clippy warnings in wrapup compute + renderer
Some checks failed
CI / Check / Test (push) Has been cancelled

This commit is contained in:
2026-06-02 23:49:39 +02:00
parent e69f77a99f
commit 9e13f04e9c
6 changed files with 65 additions and 58 deletions

View File

@@ -10,17 +10,11 @@ pub fn render_genre_chart(
let mut buf = vec![0u8; (width * height * 3) as usize]; let mut buf = vec![0u8; (width * height * 3) as usize];
{ {
let root = let root = BitMapBackend::with_buffer(&mut buf, (width, height)).into_drawing_area();
BitMapBackend::with_buffer(&mut buf, (width, height)).into_drawing_area();
root.fill(&RGBColor(26, 26, 36)) root.fill(&RGBColor(26, 26, 36))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let max_count = report let max_count = report.top_genres.iter().map(|g| g.count).max().unwrap_or(1);
.top_genres
.iter()
.map(|g| g.count)
.max()
.unwrap_or(1);
let mut chart = ChartBuilder::on(&root) let mut chart = ChartBuilder::on(&root)
.margin(40) .margin(40)
@@ -36,7 +30,7 @@ pub fn render_genre_chart(
.configure_mesh() .configure_mesh()
.disable_mesh() .disable_mesh()
.label_style(("sans-serif", 14, &WHITE)) .label_style(("sans-serif", 14, &WHITE))
.axis_style(&RGBColor(100, 100, 100)) .axis_style(RGBColor(100, 100, 100))
.draw() .draw()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;

View File

@@ -6,14 +6,12 @@ pub async fn stitch_slides(
slides: &[Vec<u8>], slides: &[Vec<u8>],
config: &VideoRenderConfig, config: &VideoRenderConfig,
) -> Result<Vec<u8>, DomainError> { ) -> Result<Vec<u8>, DomainError> {
let dir = let dir = tempfile::tempdir().map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
tempfile::tempdir().map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
// Write slide PNGs // Write slide PNGs
for (i, png) in slides.iter().enumerate() { for (i, png) in slides.iter().enumerate() {
let path = dir.path().join(format!("slide_{:04}.png", i)); let path = dir.path().join(format!("slide_{:04}.png", i));
std::fs::write(&path, png) std::fs::write(&path, png).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
} }
let output_path = dir.path().join("output.mp4"); let output_path = dir.path().join("output.mp4");

View File

@@ -7,6 +7,7 @@ use domain::errors::DomainError;
use domain::models::wrapup::WrapUpReport; use domain::models::wrapup::WrapUpReport;
use domain::ports::{VideoRenderConfig, WrapUpVideoRenderer}; use domain::ports::{VideoRenderConfig, WrapUpVideoRenderer};
#[derive(Default)]
pub struct FfmpegWrapUpRenderer; pub struct FfmpegWrapUpRenderer;
impl FfmpegWrapUpRenderer { impl FfmpegWrapUpRenderer {
@@ -25,10 +26,8 @@ impl WrapUpVideoRenderer for FfmpegWrapUpRenderer {
) -> Result<Vec<u8>, DomainError> { ) -> Result<Vec<u8>, DomainError> {
let (width, height) = config.resolution; let (width, height) = config.resolution;
let renderer = slides::SlideRenderer::new( let renderer =
config.font_path.as_deref(), slides::SlideRenderer::new(config.font_path.as_deref(), config.logo_path.as_deref())?;
config.logo_path.as_deref(),
)?;
// 1. Generate slide images // 1. Generate slide images
let mut slide_pngs = Vec::new(); let mut slide_pngs = Vec::new();

View File

@@ -144,7 +144,13 @@ impl SlideRenderer {
} }
// rating distribution bars // rating distribution bars
let max_count = report.rating_distribution.iter().copied().max().unwrap_or(1).max(1); let max_count = report
.rating_distribution
.iter()
.copied()
.max()
.unwrap_or(1)
.max(1);
let bar_area_top = (h / 2) as i32; let bar_area_top = (h / 2) as i32;
let bar_h = 36u32; let bar_h = 36u32;
let bar_gap = 16u32; let bar_gap = 16u32;
@@ -165,11 +171,7 @@ impl SlideRenderer {
// filled bar // filled bar
let fill_w = ((count as f32 / max_count as f32) * max_bar_w as f32) as u32; let fill_w = ((count as f32 / max_count as f32) * max_bar_w as f32) as u32;
if fill_w > 0 { if fill_w > 0 {
draw_filled_rect_mut( draw_filled_rect_mut(&mut img, Rect::at(margin_x, y).of_size(fill_w, bar_h), GOLD);
&mut img,
Rect::at(margin_x, y).of_size(fill_w, bar_h),
GOLD,
);
} }
// count label // count label
let count_s = count.to_string(); let count_s = count.to_string();
@@ -336,11 +338,8 @@ fn fill(w: u32, h: u32) -> RgbaImage {
fn to_png(img: &RgbaImage) -> Result<Vec<u8>, DomainError> { fn to_png(img: &RgbaImage) -> Result<Vec<u8>, DomainError> {
let mut buf = Vec::new(); let mut buf = Vec::new();
img.write_to( img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
&mut std::io::Cursor::new(&mut buf), .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
image::ImageFormat::Png,
)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(buf) Ok(buf)
} }
@@ -355,11 +354,11 @@ fn load_system_font() -> Result<FontArc, DomainError> {
"/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/Helvetica.ttc",
]; ];
for path in &candidates { for path in &candidates {
if let Ok(bytes) = std::fs::read(path) { if let Ok(bytes) = std::fs::read(path)
if let Ok(font) = FontArc::try_from_vec(bytes) { && let Ok(font) = FontArc::try_from_vec(bytes)
tracing::info!("loaded system font: {path}"); {
return Ok(font); tracing::info!("loaded system font: {path}");
} return Ok(font);
} }
} }
Err(DomainError::InfrastructureError( Err(DomainError::InfrastructureError(

View File

@@ -22,7 +22,11 @@ pub async fn execute(
Ok(build_report(query.scope, query.date_range, &rows)) Ok(build_report(query.scope, query.date_range, &rows))
} }
fn build_report(scope: WrapUpScope, date_range: DateRange, rows: &[WrapUpMovieRow]) -> WrapUpReport { fn build_report(
scope: WrapUpScope,
date_range: DateRange,
rows: &[WrapUpMovieRow],
) -> WrapUpReport {
let total_movies = rows.len() as u32; let total_movies = rows.len() as u32;
let total_watch_time_minutes: u32 = rows.iter().filter_map(|r| r.runtime_minutes).sum(); let total_watch_time_minutes: u32 = rows.iter().filter_map(|r| r.runtime_minutes).sum();
@@ -179,7 +183,10 @@ fn compute_busiest_day(rows: &[WrapUpMovieRow]) -> Option<String> {
} }
fn compute_runtime_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<MovieRef>) { fn compute_runtime_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<MovieRef>) {
let with_runtime: Vec<_> = rows.iter().filter(|r| r.runtime_minutes.is_some()).collect(); let with_runtime: Vec<_> = rows
.iter()
.filter(|r| r.runtime_minutes.is_some())
.collect();
let longest = with_runtime let longest = with_runtime
.iter() .iter()
.max_by_key(|r| r.runtime_minutes.unwrap_or(0)) .max_by_key(|r| r.runtime_minutes.unwrap_or(0))
@@ -192,22 +199,20 @@ fn compute_runtime_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Optio
} }
fn compute_rating_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<MovieRef>) { fn compute_rating_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<MovieRef>) {
let highest = rows.iter().max_by_key(|r| r.rating).map(|r| movie_ref(r)); let highest = rows.iter().max_by_key(|r| r.rating).map(movie_ref);
let lowest = rows.iter().min_by_key(|r| r.rating).map(|r| movie_ref(r)); let lowest = rows.iter().min_by_key(|r| r.rating).map(movie_ref);
(highest, lowest) (highest, lowest)
} }
fn compute_chronological_extremes( fn compute_chronological_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<MovieRef>) {
rows: &[WrapUpMovieRow],
) -> (Option<MovieRef>, Option<MovieRef>) {
let first = rows let first = rows
.iter() .iter()
.min_by_key(|r| r.watched_at) .min_by_key(|r| r.watched_at)
.map(|r| movie_ref(r)); .map(movie_ref);
let last = rows let last = rows
.iter() .iter()
.max_by_key(|r| r.watched_at) .max_by_key(|r| r.watched_at)
.map(|r| movie_ref(r)); .map(movie_ref);
(first, last) (first, last)
} }
@@ -215,11 +220,11 @@ fn compute_year_extremes(rows: &[WrapUpMovieRow]) -> (Option<MovieRef>, Option<M
let oldest = rows let oldest = rows
.iter() .iter()
.min_by_key(|r| r.release_year) .min_by_key(|r| r.release_year)
.map(|r| movie_ref(r)); .map(movie_ref);
let newest = rows let newest = rows
.iter() .iter()
.max_by_key(|r| r.release_year) .max_by_key(|r| r.release_year)
.map(|r| movie_ref(r)); .map(movie_ref);
(oldest, newest) (oldest, newest)
} }
@@ -246,7 +251,11 @@ fn compute_director_stats(rows: &[WrapUpMovieRow]) -> (Vec<PersonStat>, u32) {
} }
}) })
.collect(); .collect();
stats.sort_by(|a, b| b.count.cmp(&a.count).then(b.avg_rating.total_cmp(&a.avg_rating))); stats.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then(b.avg_rating.total_cmp(&a.avg_rating))
});
stats.truncate(5); stats.truncate(5);
(stats, diversity) (stats, diversity)
} }
@@ -257,10 +266,7 @@ fn compute_actor_stats(rows: &[WrapUpMovieRow]) -> (Vec<PersonStat>, u32, Vec<St
for r in rows { for r in rows {
for (i, (name, billing)) in r.cast_names.iter().enumerate() { for (i, (name, billing)) in r.cast_names.iter().enumerate() {
if *billing <= 3 { if *billing <= 3 {
actor_movies actor_movies.entry(name.clone()).or_default().push(r.rating);
.entry(name.clone())
.or_default()
.push(r.rating);
if let Some(path) = r.cast_profile_paths.get(i) { if let Some(path) = r.cast_profile_paths.get(i) {
actor_profiles actor_profiles
.entry(name.clone()) .entry(name.clone())
@@ -282,7 +288,11 @@ fn compute_actor_stats(rows: &[WrapUpMovieRow]) -> (Vec<PersonStat>, u32, Vec<St
} }
}) })
.collect(); .collect();
stats.sort_by(|a, b| b.count.cmp(&a.count).then(b.avg_rating.total_cmp(&a.avg_rating))); stats.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then(b.avg_rating.total_cmp(&a.avg_rating))
});
stats.truncate(5); stats.truncate(5);
let profile_paths: Vec<String> = stats let profile_paths: Vec<String> = stats
.iter() .iter()
@@ -316,7 +326,7 @@ fn compute_genre_stats(
} }
}) })
.collect(); .collect();
stats.sort_by(|a, b| b.count.cmp(&a.count)); stats.sort_by_key(|s| std::cmp::Reverse(s.count));
let highest = stats let highest = stats
.iter() .iter()
.max_by(|a, b| a.avg_rating.total_cmp(&b.avg_rating)) .max_by(|a, b| a.avg_rating.total_cmp(&b.avg_rating))
@@ -341,7 +351,7 @@ fn compute_keyword_stats(rows: &[WrapUpMovieRow]) -> Vec<KeywordStat> {
.into_iter() .into_iter()
.map(|(keyword, count)| KeywordStat { keyword, count }) .map(|(keyword, count)| KeywordStat { keyword, count })
.collect(); .collect();
stats.sort_by(|a, b| b.count.cmp(&a.count)); stats.sort_by_key(|s| std::cmp::Reverse(s.count));
stats.truncate(20); stats.truncate(20);
stats stats
} }
@@ -371,7 +381,7 @@ fn compute_language_stats(rows: &[WrapUpMovieRow]) -> Vec<LangStat> {
.into_iter() .into_iter()
.map(|(language, count)| LangStat { language, count }) .map(|(language, count)| LangStat { language, count })
.collect(); .collect();
stats.sort_by(|a, b| b.count.cmp(&a.count)); stats.sort_by_key(|s| std::cmp::Reverse(s.count));
stats stats
} }

View File

@@ -34,7 +34,10 @@ pub async fn execute(
Ok(report) => { Ok(report) => {
let json = serde_json::to_string(&report) let json = serde_json::to_string(&report)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
ctx.repos.wrapup_repo.set_complete(&wrapup_id, &json).await?; ctx.repos
.wrapup_repo
.set_complete(&wrapup_id, &json)
.await?;
// Optionally render video (non-fatal) // Optionally render video (non-fatal)
if let Some(ref renderer) = ctx.services.video_renderer { if let Some(ref renderer) = ctx.services.video_renderer {
@@ -51,7 +54,12 @@ pub async fn execute(
match renderer.render(&report, poster_images, &config).await { match renderer.render(&report, poster_images, &config).await {
Ok(video_bytes) => { Ok(video_bytes) => {
let video_key = format!("wrapups/{}/video.mp4", wrapup_id.value()); let video_key = format!("wrapups/{}/video.mp4", wrapup_id.value());
if let Err(e) = ctx.services.image_storage.store(&video_key, &video_bytes).await { if let Err(e) = ctx
.services
.image_storage
.store(&video_key, &video_bytes)
.await
{
tracing::warn!("failed to store wrapup video: {e}"); tracing::warn!("failed to store wrapup video: {e}");
} }
} }
@@ -80,9 +88,8 @@ pub async fn execute(
async fn resolve_poster_images(ctx: &AppContext, report: &WrapUpReport) -> Vec<(String, Vec<u8>)> { async fn resolve_poster_images(ctx: &AppContext, report: &WrapUpReport) -> Vec<(String, Vec<u8>)> {
let mut images = Vec::new(); let mut images = Vec::new();
for path in report.poster_paths.iter().take(20) { for path in report.poster_paths.iter().take(20) {
match ctx.services.image_storage.get(path).await { if let Ok(bytes) = ctx.services.image_storage.get(path).await {
Ok(bytes) => images.push((path.clone(), bytes)), images.push((path.clone(), bytes));
Err(_) => {}
} }
} }
images images