From 54a1df60b8d9ea6fe25758b8848fb9ebf4cd46e6 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 18 Apr 2026 10:03:32 -0400 Subject: [PATCH] fix: resolve preview clip rel_path against all libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PreviewClipGenerator stripped a single base_path, so videos in a non-primary library ended up with the absolute path as 'relative'. On Windows, PathBuf::from(preview_clips_dir).join(absolute) replaces with the absolute path, and .with_extension("mp4") on a .mp4 input yields the input path — ffmpeg then errors out with 'cannot edit existing files in place'. The generator now holds Vec and strips whichever root actually contains the video, with separator normalization to match the rest of the code. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/state.rs | 7 +++++-- src/video/actors.rs | 34 +++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 13 deletions(-) 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();