diff --git a/src/state.rs b/src/state.rs index d901a66..78b98ad 100644 --- a/src/state.rs +++ b/src/state.rs @@ -74,8 +74,11 @@ impl AppState { let video_playlist_manager = VideoPlaylistManager::new(video_path.clone(), playlist_generator.start()); - let preview_clip_generator = - PreviewClipGenerator::new(preview_clips_path.clone(), base_path.clone(), preview_dao); + let preview_clip_generator = PreviewClipGenerator::new( + preview_clips_path.clone(), + libraries_vec.clone(), + preview_dao, + ); Self { stream_manager, diff --git a/src/video/actors.rs b/src/video/actors.rs index e90bbe1..8af2482 100644 --- a/src/video/actors.rs +++ b/src/video/actors.rs @@ -1,5 +1,6 @@ use crate::database::PreviewDao; use crate::is_video; +use crate::libraries::Library; use crate::otel::global_tracer; use crate::video::ffmpeg::generate_preview_clip; use actix::prelude::*; @@ -500,23 +501,40 @@ pub struct GeneratePreviewClipMessage { pub struct PreviewClipGenerator { semaphore: Arc, preview_clips_dir: String, - base_path: String, + libraries: Vec, preview_dao: Arc>>, } impl PreviewClipGenerator { pub fn new( preview_clips_dir: String, - base_path: String, + libraries: Vec, preview_dao: Arc>>, ) -> Self { PreviewClipGenerator { semaphore: Arc::new(Semaphore::new(2)), preview_clips_dir, - base_path, + libraries, preview_dao, } } + + /// Strip whichever library root actually contains `video_path`. + /// Falls back to the first library if none match, so we never + /// accidentally emit the absolute input path as the output path + /// (which ffmpeg rejects as "cannot edit existing files in place"). + fn relativize(&self, video_path: &str) -> String { + for lib in &self.libraries { + if let Some(stripped) = video_path.strip_prefix(&lib.root_path) { + return stripped + .trim_start_matches(['/', '\\']) + .replace('\\', "/"); + } + } + video_path + .trim_start_matches(['/', '\\']) + .replace('\\', "/") + } } impl Actor for PreviewClipGenerator { @@ -533,9 +551,10 @@ impl Handler for PreviewClipGenerator { ) -> Self::Result { let semaphore = self.semaphore.clone(); let preview_clips_dir = self.preview_clips_dir.clone(); - let base_path = self.base_path.clone(); let preview_dao = self.preview_dao.clone(); let video_path = msg.video_path; + // Resolve against whichever library actually owns this video. + let relative_path = self.relativize(&video_path); Box::pin(async move { let permit = semaphore @@ -543,13 +562,6 @@ impl Handler for PreviewClipGenerator { .await .expect("Unable to acquire preview semaphore"); - // Compute relative path (from BASE_PATH) for DB operations, consistent with EXIF convention - let relative_path = video_path - .strip_prefix(&base_path) - .unwrap_or(&video_path) - .trim_start_matches(['/', '\\']) - .to_string(); - // Update status to processing { let otel_ctx = opentelemetry::Context::current();