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>
119 lines
3.9 KiB
Rust
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 { .. }
|
|
)
|
|
}
|
|
}
|