Watch selected folders and reconcile bounded changes with media freshness guards

This commit is contained in:
karl committed 2026-10-13 02:30:38 +02:00
1 parent 99748bd99f
commit 657de3de86
22 files changed
+2022 -149

No files matched your search

+56 -2
View File
@@ -93,6 +93,29 @@ mod tests {
Ok(())
}
#[test]
fn owned_frame_scratch_validates_parent_and_cleans_batch() -> Result<()> {
let root = tempfile::tempdir()?;
let parent = root.path().join("owned scratch");
let limits = MediaLimits {
scratch_directory: Some(parent.clone()),
..Default::default()
};
let batch = frame_scratch(&limits)?;
assert_eq!(batch.path().parent(), Some(parent.as_path()));
let batch_path = batch.path().to_owned();
fs::write(batch.path().join("frame.png"), b"owned temporary bytes")?;
drop(batch);
assert!(!batch_path.exists());
assert!(parent.is_dir());
let relative = MediaLimits {
scratch_directory: Some(PathBuf::from("relative")),
..Default::default()
};
assert!(frame_scratch(&relative).is_err());
Ok(())
}
#[test]
fn identity_rejects_changes_and_oversized_input() -> Result<()> {
let temp = tempfile::tempdir()?;
@@ -170,10 +193,19 @@ mod tests {
])
.arg(&video);
run(command, Duration::from_secs(30), 4096, 65536)?;
let limits = MediaLimits::default();
let scratch = temp.path().join("owned scratch");
let limits = MediaLimits {
scratch_directory: Some(scratch.clone()),
..Default::default()
};
let probe = probe(&tools, &video, &limits)?;
assert!(probe.has_video && probe.has_audio);
let frames = decode_frames(&tools, &video, &probe, 2000, false, &limits)?;
assert!(frames.directory().starts_with(&scratch));
assert!(frames
.frames
.iter()
.all(|frame| frame.path.starts_with(&scratch)));
assert_eq!(frames.frames.len(), 1);
assert!((1900..=2100).contains(&frames.frames[0].timestamp_ms));
let audio = decode_audio(
@@ -312,6 +344,10 @@ pub struct MediaLimits {
pub audio_overlap_ms: u32,
pub max_audio_windows: usize,
pub command_timeout: Duration,
/// Owned decoded-frame scratch parent. Desktop callers place this below
/// excluded application data to avoid indexing their own temporary frames.
/// None preserves the normal operating-system temporary directory.
pub scratch_directory: Option<PathBuf>,
}
impl Default for MediaLimits {
fn default() -> Self {
@@ -325,6 +361,7 @@ impl Default for MediaLimits {
audio_overlap_ms: 2000,
max_audio_windows: 600,
command_timeout: Duration::from_secs(45),
scratch_directory: None,
}
}
}
@@ -615,6 +652,23 @@ impl FrameBatch {
self.directory.path()
}
}
fn frame_scratch(limits: &MediaLimits) -> Result<tempfile::TempDir> {
match &limits.scratch_directory {
Some(parent) => {
ensure!(
parent.is_absolute(),
"Media scratch directory must be absolute"
);
fs::create_dir_all(parent).context("Create owned media scratch directory")?;
tempfile::Builder::new()
.prefix("searchi-frames-")
.tempdir_in(parent)
.context("Create owned frame batch")
}
None => tempfile::tempdir().context("Create frame batch"),
}
}
pub fn decode_frames(
tools: &FfmpegTools,
path: &Path,
@@ -623,7 +677,7 @@ pub fn decode_frames(
include_scene: bool,
limits: &MediaLimits,
) -> Result<FrameBatch> {
let directory = tempfile::tempdir()?;
let directory = frame_scratch(limits)?;
let mut command = Command::new(&tools.ffmpeg);
command.args(["-protocol_whitelist", "file,pipe"]);
command.args(["-nostdin","-hide_banner","-loglevel","info","-threads","1","-hwaccel","none","-copyts","-ss"])