From 21e624da6bbfaf2a2893210e4070b0c1954c8a91 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 26 Apr 2026 01:06:13 -0400 Subject: [PATCH] fix(video): sentinel for failed HLS encodes to stop retry loop Previously a corrupt source (e.g. truncated mp4 with no moov atom) would be re-queued on every directory scan: cleanup_partial_hls wipes the temp playlist on ffmpeg failure, leaving no .m3u8 to short-circuit the next pass. Mirrors the thumbnail .unsupported sentinel pattern: on ffmpeg failure, write .m3u8.unsupported, and treat its presence as "done" in both the ScanDirectoryMessage filter and the QueueVideosMessage check. Delete the sentinel to force a retry. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/video/actors.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/video/actors.rs b/src/video/actors.rs index a461efe..c7300ef 100644 --- a/src/video/actors.rs +++ b/src/video/actors.rs @@ -55,6 +55,16 @@ pub fn playlist_file_for(playlist_dir: &str, video_path: &Path) -> PathBuf { PathBuf::from(format!("{}/{}.m3u8", playlist_dir, filename)) } +/// Sentinel path written next to a would-be playlist when ffmpeg cannot +/// transcode the source (e.g. truncated mp4 with no moov atom). Its presence +/// causes future scans to skip the file instead of re-running ffmpeg every +/// pass. Delete the `.unsupported` file to force a retry. +pub fn playlist_unsupported_sentinel(playlist_file: &Path) -> PathBuf { + let mut s = playlist_file.as_os_str().to_owned(); + s.push(".unsupported"); + PathBuf::from(s) +} + pub async fn create_playlist(video_path: &str, playlist_file: &str) -> Result { if Path::new(playlist_file).exists() { debug!("Playlist already exists: {}", playlist_file); @@ -319,7 +329,10 @@ impl Handler for VideoPlaylistManager { .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) .filter(is_video) - .filter(|e| !playlist_file_for(&playlist_dir_str, e.path()).exists()) + .filter(|e| { + let playlist = playlist_file_for(&playlist_dir_str, e.path()); + !playlist.exists() && !playlist_unsupported_sentinel(&playlist).exists() + }) .collect::>(); let scan_dir_name = msg.directory.clone(); @@ -393,7 +406,8 @@ impl Handler for VideoPlaylistManager { let playlist_generator = self.playlist_generator.clone(); for video_path in msg.video_paths { - if playlist_file_for(&playlist_dir_str, &video_path).exists() { + let playlist = playlist_file_for(&playlist_dir_str, &video_path); + if playlist.exists() || playlist_unsupported_sentinel(&playlist).exists() { continue; } let path_str = video_path.to_string_lossy().to_string(); @@ -683,6 +697,20 @@ impl Handler for PlaylistGenerator { }; error!("ffmpeg failed for {}: {}", video_file, detail); cleanup_partial_hls(&playlist_tmp, playlist_path.as_str(), video_stem).await; + let sentinel = playlist_unsupported_sentinel(Path::new(&playlist_file)); + if let Err(se) = tokio::fs::write(&sentinel, b"").await { + warn!( + "Failed to write playlist sentinel {}: {}", + sentinel.display(), + se + ); + } else { + info!( + "Wrote playlist sentinel {} so future scans skip {}", + sentinel.display(), + video_file + ); + } span.set_status(Status::error(detail.clone())); Err(std::io::Error::other(detail)) }