diff --git a/src/context.rs b/src/context.rs index c65e480..729446d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -126,10 +126,7 @@ impl Context { for job in jobs { if !job_queue .iter() - .any(|existing_job| - existing_job.kind == job.kind && - Arc::ptr_eq(&existing_job.video, &job.video) - ) { + .any(|existing_job| existing_job.kind == job.kind) { job_queue.push(job); } } @@ -362,6 +359,12 @@ impl Context { } fn queue_playlist_jobs(&self, playlist: &Arc, queue: &mut Vec) { + if playlist.banner.as_ref().is_some_and(|banner| banner.assets.is_none()) { + let job_kind = JobKind::PlaylistFastTasks(Arc::clone(playlist)); + let job = Job::new(job_kind); + queue.push(job); + } + for video in &playlist.videos { self.queue_video_jobs(video, queue); } @@ -411,7 +414,8 @@ impl Context { if video.versions.iter().any(|version| version.video_meta.is_none()) || (video.poster.is_none() && !video.versions.is_empty()) || video.poster.as_ref().is_some_and(|poster| poster.assets.is_none()) { - let job = Job::new(JobKind::FastTasks, Arc::clone(video)); + let job_kind = JobKind::VideoFastTasks(Arc::clone(video)); + let job = Job::new(job_kind); queue.push(job); } @@ -436,7 +440,7 @@ impl Context { .iter_mut() .filter(|job| { if high_priority_only { - if let JobKind::Format(_) = job.kind { + if let JobKind::VideoFormat { .. } = job.kind { return false; } } diff --git a/src/editor/endpoints/index.rs b/src/editor/endpoints/index.rs index 6520b96..c794913 100644 --- a/src/editor/endpoints/index.rs +++ b/src/editor/endpoints/index.rs @@ -5,7 +5,7 @@ use actix_web::http::header::ContentType; use actix_web::web::Data; use indoc::formatdoc; -use crate::{Context, SiteContent}; +use crate::{Context, JobKind, SiteContent}; use crate::editor::widgets::layout; pub async fn index(context: Data>) -> HttpResponse { @@ -52,11 +52,14 @@ pub async fn index(context: Data>) -> HttpResponse { .unwrap() .iter() .map(|job| { - format!( - "{video} {status}", - status = if job.assigned { "assigned" } else { "idle" }, - video = job.video.label() - ) + let label = match &job.kind { + JobKind::PlaylistFastTasks(playlist) => playlist.label(), + JobKind::VideoFastTasks(video) => video.label(), + JobKind::VideoFormat { video, .. } => video.label(), + }; + let status = if job.assigned { "assigned" } else { "idle" }; + + format!("{label} {status}") }) .collect::>() .join("\n"); diff --git a/src/editor/endpoints/playlist.rs b/src/editor/endpoints/playlist.rs index 29a0390..8f903f3 100644 --- a/src/editor/endpoints/playlist.rs +++ b/src/editor/endpoints/playlist.rs @@ -17,6 +17,8 @@ use crate::{ ContextPath, Field, FileMeta, + Job, + JobKind, Playlist }; use crate::editor::endpoints; @@ -643,16 +645,18 @@ pub async fn upload_banner( playlist.write_manifest(&context.site_dir); }; - if context.update_playlist(mutation, &path.path).is_err() { - fs::remove_file(&storage_path).unwrap(); - let http_response = not_found(&context, &format!("Playlist {} not found.", &path.path)); - return Ok(Either::Right(http_response)); - }; - - // TODO: Would need some architectural changes. - // Maybe implement together with new job model? (which maybe does not use a job queue at all?) - // let job = Job::new(JobKind::Poster, playlist.clone()); - // context.job_queue.lock().unwrap().push(job); + match context.update_playlist(mutation, &path.path) { + Ok(playlist) => { + let job_kind = JobKind::PlaylistFastTasks(Arc::clone(&playlist)); + let job = Job::new(job_kind); + context.job_queue.lock().unwrap().push(job); + } + Err(()) => { + fs::remove_file(&storage_path).unwrap(); + let http_response = not_found(&context, &format!("Playlist {} not found.", &path.path)); + return Ok(Either::Right(http_response)); + } + } } } diff --git a/src/editor/endpoints/video.rs b/src/editor/endpoints/video.rs index ff6140a..106b708 100644 --- a/src/editor/endpoints/video.rs +++ b/src/editor/endpoints/video.rs @@ -120,7 +120,8 @@ pub async fn downsize( let video_format_string = format!("video:h264:mp4:speed:1.0:{}", form.width); let video_format = VideoFormat::parse(&video_format_string).unwrap(); - let job = Job::new(JobKind::Format(video_format), Arc::clone(&video)); + let job_kind = JobKind::VideoFormat { format: video_format, video: Arc::clone(&video) }; + let job = Job::new(job_kind); context.job_queue.lock().unwrap().push(job); let redirect_url = format!("/video/{}", &path.path); @@ -611,7 +612,8 @@ pub async fn upload_poster( } }; - let job = Job::new(JobKind::FastTasks, Arc::clone(&video)); + let job_kind = JobKind::VideoFastTasks(Arc::clone(&video)); + let job = Job::new(job_kind); context.job_queue.lock().unwrap().push(job); let redirect_url = format!("/video/{}", path.path); @@ -683,7 +685,7 @@ pub async fn upload_video( f = web::block(move || f.write_all(&chunk).map(|_| f)).await??; } - match &container { + let video = match &container { GetAnyResult::Collection(_) => { let hyper_dir = HyperDir::read(storage_path.parent().unwrap()); let video = Video::read_dir(&context, &hyper_dir); @@ -698,11 +700,7 @@ pub async fn upload_video( return Ok(Either::Right(not_found(&context, &format!("Collection {} not found.", &path.path)))); } - let job = Job::new(JobKind::FastTasks, Arc::clone(&video)); - context.job_queue.lock().unwrap().push(job); - - let redirect_url = format!("/video/{}", &video.path); - return Ok(Either::Left(Redirect::to(redirect_url).see_other())) + video } GetAnyResult::Playlist(_) => { let hyper_dir = HyperDir::read(storage_path.parent().unwrap()); @@ -718,11 +716,7 @@ pub async fn upload_video( return Ok(Either::Right(not_found(&context, &format!("Playlist {} not found.", &path.path)))); } - let job = Job::new(JobKind::FastTasks, Arc::clone(&video)); - context.job_queue.lock().unwrap().push(job); - - let redirect_url = format!("/video/{}", &video.path); - return Ok(Either::Left(Redirect::to(redirect_url).see_other())) + video } GetAnyResult::Video(_) => { let file_meta = FileMeta::new(&storage_path); @@ -732,20 +726,21 @@ pub async fn upload_video( video.versions.push(VideoFile::new(file_meta, file_name, None)); }; - let video = match context.update_video(mutation, &path.path) { + match context.update_video(mutation, &path.path) { Ok(video) => video, // TODO: Can only happen in weird time of check vs. time of use edge cases Err(()) => return Ok(Either::Right(not_found(&context, &format!("Video {} not found.", &path.path)))) - }; - - let job = Job::new(JobKind::FastTasks, Arc::clone(&video)); - context.job_queue.lock().unwrap().push(job); - - let redirect_url = format!("/video/{}", &video.path); - return Ok(Either::Left(Redirect::to(redirect_url).see_other())) + } } GetAnyResult::None => unreachable!() - } + }; + + let job_kind = JobKind::VideoFastTasks(Arc::clone(&video)); + let job = Job::new(job_kind); + context.job_queue.lock().unwrap().push(job); + + let redirect_url = format!("/video/{}", &video.path); + return Ok(Either::Left(Redirect::to(redirect_url).see_other())) } } } diff --git a/src/image_processor.rs b/src/image_processor.rs index ddb89a8..4f7b644 100644 --- a/src/image_processor.rs +++ b/src/image_processor.rs @@ -9,6 +9,10 @@ use crate::util::nanoid; pub enum ResizeMode { + /// Perform a crop to a rectangle with the minimum target aspect ratio (if needed), + /// then resize to a maximum width. + /// Aspect ratio is width / height, e.g. 16/9 = 1.7777777 + CoverCroppedRectangle { max_width: u32, min_aspect: f32 }, /// Perform a crop to a rectangle with the target aspect ratio (if needed), /// then resize to a maximum width. /// Aspect ratio is width / height, e.g. 16/9 = 1.7777777 @@ -79,6 +83,18 @@ impl ImageProcessor { resize_mode: ResizeMode ) -> (String, (u32, u32)) { match resize_mode { + ResizeMode::CoverCroppedRectangle { max_width, min_aspect } => { + let found_aspect = self.source_width as f32 / self.source_height as f32; + if found_aspect < min_aspect { + // too tall, reduce height + let new_height = (self.source_width as f32 / min_aspect).floor() as u32; + let y = (self.source_height - new_height) / 2; + let cropped_image = self.source_image.crop_imm(0, y, self.source_width, new_height); + self.resize_and_export(context, max_width, &cropped_image) + } else { + self.resize_and_export(context, max_width, &self.source_image) + } + } ResizeMode::CoverRectangle { aspect, max_width } => { let found_aspect = self.source_width as f32 / self.source_height as f32; if found_aspect < aspect { diff --git a/src/poster.rs b/src/poster.rs index 9c6a2c8..a12cbec 100644 --- a/src/poster.rs +++ b/src/poster.rs @@ -38,12 +38,18 @@ pub struct PosterFile { } impl PosterAssets { - pub fn write_manifest(&self, context: &Context, path: &str, poster: &PosterFile) { - let poster_path = format!("{}/{}", path, poster.file_name); + pub fn write_cache_manifest( + &self, + context: &Context, + file_meta: &FileMeta, + file_name: &str, + path: &str + ) { + let poster_path = format!("{}/{}", path, file_name); let cache_item = CacheItem::PosterAssets(PosterAssetsCached { assets: self.clone(), - file_meta: poster.file_meta.clone(), + file_meta: file_meta.clone(), path: poster_path.clone() }); diff --git a/src/processing.rs b/src/processing.rs index 1418347..5ba9305 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use serde_derive::Serialize; -use crate::{Video, VideoFormat}; +use crate::{Playlist, Video, VideoFormat}; use crate::util::{nanoid, timestamp}; #[derive(Clone, Debug)] @@ -11,14 +11,17 @@ pub struct Job { pub assigned: bool, pub created: u128, pub id: String, - pub kind: JobKind, - pub video: Arc