Files
arr/crates/arr-probe/src/error.rs
T
Miguel PalhasandClaude Opus 5 4e880413ae
ci / web (pull_request) Successful in 6s
ci / rust (pull_request) Successful in 1m30s
e2e / e2e (pull_request) Successful in 1m35s
fix(probe): do not hide prober failures or cover art
Review on #59. select_feature skipped every probe error, so a missing
ffprobe or a timeout reported "no video file" for a torrent that has
one; only per-file content failures are skipped now. And an audio file
with embedded artwork reports the cover as a video stream, which made a
soundtrack look importable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:39:25 +01:00

119 lines
3.9 KiB
Rust

//! Errors the `ffprobe` wrapper can produce.
use std::path::PathBuf;
/// Result alias for every fallible operation in this crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Everything that can go wrong probing a file.
///
/// Nothing here has a default. A file `ffprobe` cannot read is an error, never
/// a `ProbedFile` full of zeroes — importing on top of a default-valued struct
/// would silently record a 0-byte SDR file with no audio and pass every policy
/// rule that inspects a field it does not have (DESIGN.md §5.6, §5.7).
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// `ffprobe` could not be started at all: not installed, or not on `PATH`.
#[error("could not run {binary}")]
Spawn {
/// The binary that was attempted.
binary: String,
/// The underlying spawn failure.
#[source]
source: std::io::Error,
},
/// `ffprobe` started but exited non-zero — an unreadable or corrupt file.
#[error("ffprobe rejected {path}: {stderr}")]
Rejected {
/// The file that was probed.
path: PathBuf,
/// Exit status, absent when the process was killed by a signal.
status: Option<i32>,
/// `ffprobe`'s own diagnostics, truncated to something loggable.
stderr: String,
},
/// `ffprobe` did not finish within the configured timeout.
#[error("ffprobe timed out probing {path}")]
Timeout {
/// The file that was probed.
path: PathBuf,
},
/// Reading the child process failed after it was spawned.
#[error("reading ffprobe output for {path} failed")]
Io {
/// The file that was probed.
path: PathBuf,
/// The underlying IO failure.
#[source]
source: std::io::Error,
},
/// `ffprobe` printed something that is not the JSON shape expected.
#[error("ffprobe output for {path} did not parse")]
Decode {
/// The file that was probed.
path: PathBuf,
/// The deserialisation failure.
#[source]
source: serde_json::Error,
},
/// `ffprobe` read the file, but as a still image or as text.
///
/// ffmpeg's `tty` demuxer parses any text file at all as ANSI art video,
/// and the image demuxers turn a poster into a one-frame film. Both would
/// otherwise pass for importable media.
#[error("{path} is not a video file: ffprobe read it as {format}")]
NotVideo {
/// The file that was probed.
path: PathBuf,
/// The container format `ffprobe` matched.
format: String,
},
/// `ffprobe` read the file but it carries no video stream.
#[error("{path} has no video stream")]
NoVideoStream {
/// The file that was probed.
path: PathBuf,
},
/// A container that reports neither a duration nor a stream long enough to
/// derive one. The runtime sanity check has nothing to work with.
#[error("{path} reports no duration")]
NoDuration {
/// The file that was probed.
path: PathBuf,
},
/// Feature selection was handed a set of paths with no readable video file
/// among them.
#[error("no video file among the candidates")]
NoCandidates,
}
impl Error {
/// Whether this failure is a fact about one file rather than about the
/// prober.
///
/// Feature selection walks a torrent full of `.nfo`, `.jpg` and `.txt`
/// files, so a file that is not importable media is expected and gets
/// skipped. A missing binary, a timeout or unparseable output is not about
/// the file at all, and swallowing it would report "no video file" for a
/// torrent that has one.
#[must_use]
pub const fn is_about_the_file(&self) -> bool {
matches!(
self,
Self::Rejected { .. }
| Self::NotVideo { .. }
| Self::NoVideoStream { .. }
| Self::NoDuration { .. }
)
}
}