fix: resolve preview clip rel_path against all libraries

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<Library> 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) <noreply@anthropic.com>
This commit is contained in:
Cameron
2026-04-18 10:03:32 -04:00
parent 791cb8a7d1
commit 3613af847f
2 changed files with 28 additions and 13 deletions

View File

@@ -74,8 +74,11 @@ impl AppState {
let video_playlist_manager = let video_playlist_manager =
VideoPlaylistManager::new(video_path.clone(), playlist_generator.start()); VideoPlaylistManager::new(video_path.clone(), playlist_generator.start());
let preview_clip_generator = let preview_clip_generator = PreviewClipGenerator::new(
PreviewClipGenerator::new(preview_clips_path.clone(), base_path.clone(), preview_dao); preview_clips_path.clone(),
libraries_vec.clone(),
preview_dao,
);
Self { Self {
stream_manager, stream_manager,

View File

@@ -1,5 +1,6 @@
use crate::database::PreviewDao; use crate::database::PreviewDao;
use crate::is_video; use crate::is_video;
use crate::libraries::Library;
use crate::otel::global_tracer; use crate::otel::global_tracer;
use crate::video::ffmpeg::generate_preview_clip; use crate::video::ffmpeg::generate_preview_clip;
use actix::prelude::*; use actix::prelude::*;
@@ -500,23 +501,40 @@ pub struct GeneratePreviewClipMessage {
pub struct PreviewClipGenerator { pub struct PreviewClipGenerator {
semaphore: Arc<Semaphore>, semaphore: Arc<Semaphore>,
preview_clips_dir: String, preview_clips_dir: String,
base_path: String, libraries: Vec<Library>,
preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>, preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>,
} }
impl PreviewClipGenerator { impl PreviewClipGenerator {
pub fn new( pub fn new(
preview_clips_dir: String, preview_clips_dir: String,
base_path: String, libraries: Vec<Library>,
preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>, preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>,
) -> Self { ) -> Self {
PreviewClipGenerator { PreviewClipGenerator {
semaphore: Arc::new(Semaphore::new(2)), semaphore: Arc::new(Semaphore::new(2)),
preview_clips_dir, preview_clips_dir,
base_path, libraries,
preview_dao, 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 { impl Actor for PreviewClipGenerator {
@@ -533,9 +551,10 @@ impl Handler<GeneratePreviewClipMessage> for PreviewClipGenerator {
) -> Self::Result { ) -> Self::Result {
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
let preview_clips_dir = self.preview_clips_dir.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 preview_dao = self.preview_dao.clone();
let video_path = msg.video_path; 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 { Box::pin(async move {
let permit = semaphore let permit = semaphore
@@ -543,13 +562,6 @@ impl Handler<GeneratePreviewClipMessage> for PreviewClipGenerator {
.await .await
.expect("Unable to acquire preview semaphore"); .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 // Update status to processing
{ {
let otel_ctx = opentelemetry::Context::current(); let otel_ctx = opentelemetry::Context::current();