fix: clippy warnings in wrapup compute + renderer
Some checks failed
CI / Check / Test (push) Has been cancelled
Some checks failed
CI / Check / Test (push) Has been cancelled
This commit is contained in:
@@ -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()))?;
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,10 +338,7 @@ 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),
|
|
||||||
image::ImageFormat::Png,
|
|
||||||
)
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
@@ -355,13 +354,13 @@ 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}");
|
tracing::info!("loaded system font: {path}");
|
||||||
return Ok(font);
|
return Ok(font);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"no system font found; set font_path in VideoRenderConfig or WRAPUP_FONT_PATH env"
|
"no system font found; set font_path in VideoRenderConfig or WRAPUP_FONT_PATH env"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user