feat(probe): ffprobe wrapper #59

Merged
naps62-yolo merged 4 commits from issue/19-ffprobe into main 2026-08-22 20:52:10 +01:00
Owner

Closes #19.

arr-probe runs ffprobe and returns a ProbedFile: resolution, video
codec, HDR format with the Dolby Vision profile as a number, per-track audio
language with the title and handler_name strings §5.2 needs, subtitle
tracks, duration and size. Source stays None — no file knows where it came
from.

Portuguese resolution uses the two file-visible signals from §5.2 (BCP-47
region tags, then the track's own description) and falls back to
por-unverified rather than guessing. Track languages are normalised to
ISO-639-1 so they compare against TMDB's original_language.

select_feature picks the feature out of a multi-file torrent: largest video
file, then a runtime check against TMDB's runtime at 15% tolerance. When
nothing matches it still returns the largest and marks the result
Mismatched, leaving the decision to import.

Nothing returns a default-valued struct. Unreadable files, text files ffmpeg's
tty demuxer happily reads as ANSI art, and files without a duration are all
errors.

Fixtures and CI

scripts/make-probe-fixtures.sh regenerates the six committed clips (~120 KB
total). The Dolby Vision pair splices a dvcC/dvvC configuration box into a
real HEVC clip — x265 refuses to encode DV without an RPU file, and profile 5
is the rule from §5.3 worth proving.

CI gains ffmpeg in its build deps so ffprobe exists for those tests. The
Nix dev shell already had it.

just ci: 80 tests, all passing.

Closes #19. `arr-probe` runs `ffprobe` and returns a `ProbedFile`: resolution, video codec, HDR format with the Dolby Vision profile as a number, per-track audio language with the `title` and `handler_name` strings §5.2 needs, subtitle tracks, duration and size. Source stays `None` — no file knows where it came from. Portuguese resolution uses the two file-visible signals from §5.2 (BCP-47 region tags, then the track's own description) and falls back to `por-unverified` rather than guessing. Track languages are normalised to ISO-639-1 so they compare against TMDB's `original_language`. `select_feature` picks the feature out of a multi-file torrent: largest video file, then a runtime check against TMDB's runtime at 15% tolerance. When nothing matches it still returns the largest and marks the result `Mismatched`, leaving the decision to import. Nothing returns a default-valued struct. Unreadable files, text files ffmpeg's `tty` demuxer happily reads as ANSI art, and files without a duration are all errors. <details> <summary>Fixtures and CI</summary> `scripts/make-probe-fixtures.sh` regenerates the six committed clips (~120 KB total). The Dolby Vision pair splices a `dvcC`/`dvvC` configuration box into a real HEVC clip — x265 refuses to encode DV without an RPU file, and profile 5 is the rule from §5.3 worth proving. CI gains `ffmpeg` in its build deps so `ffprobe` exists for those tests. The Nix dev shell already had it. `just ci`: 80 tests, all passing. </details>
naps62-yolo added 1 commit 2026-08-22 20:34:15 +01:00
feat(probe): ffprobe wrapper
ci / web (pull_request) Successful in 8s
ci / rust (pull_request) Successful in 1m59s
e2e / e2e (pull_request) Successful in 1m40s
3927927466
Post-download truth for DESIGN.md §5.6: resolution, codec, HDR with the
Dolby Vision profile as a number, per-track audio language plus the title
and handler_name strings §5.2 needs, subtitles, duration and size.

Also picks the feature file out of a multi-file torrent — largest video
file, checked against the expected runtime so an extras reel does not get
imported as the film.

The fixture clips are generated by scripts/make-probe-fixtures.sh. The two
Dolby Vision ones splice a dvcC/dvvC box into a real HEVC clip, since x265
will not encode DV without an RPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
naps62-yolo reviewed 2026-08-22 20:37:50 +01:00
@@ -95,5 +95,5 @@
policy,
overrides,
candidate,
};
let rules: [&dyn PolicyRule; 2] = [&ResolutionRule, &SourceRule];
Author
Owner

Removing DubBlacklistRule and RequiredAudioRule leaves required_audio, dub_blacklist, and the title original language unexamined, so a pt-BR-only dub or download without original audio is eligible. Keep these rules and the original-language input in the built-in pipeline; arr-probe should supply evidence, not remove enforcement.

Removing `DubBlacklistRule` and `RequiredAudioRule` leaves `required_audio`, `dub_blacklist`, and the title original language unexamined, so a pt-BR-only dub or download without original audio is eligible. Keep these rules and the original-language input in the built-in pipeline; `arr-probe` should supply evidence, not remove enforcement. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02af7"} -->
naps62-yolo marked this conversation as resolved
@@ -2,0 +140,4 @@
let path = path.into();
match self.probe(path.clone()).await {
Ok(file) => candidates.push(file),
Err(error) => {
Author
Owner

select_feature swallows Spawn, Timeout, Io, and Decode and returns NoCandidates, hiding a broken or unavailable prober even when valid media exists. Skip only per-file content failures and propagate probe/runtime failures.

`select_feature` swallows `Spawn`, `Timeout`, `Io`, and `Decode` and returns `NoCandidates`, hiding a broken or unavailable prober even when valid media exists. Skip only per-file content failures and propagate probe/runtime failures. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02af7"} -->
naps62-yolo marked this conversation as resolved
@@ -0,0 +50,4 @@
let video = output
.streams
.iter()
.find(|stream| stream.is_kind("video"))
Author
Owner

An audio file with embedded cover art has an attached_pic video stream, so this selects it as importable video. Deserialize disposition.attached_pic and choose only a non-attached video stream.

An audio file with embedded cover art has an `attached_pic` video stream, so this selects it as importable video. Deserialize `disposition.attached_pic` and choose only a non-attached video stream. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02af7"} -->
naps62-yolo marked this conversation as resolved
naps62-yolo added 1 commit 2026-08-22 20:39:37 +01:00
fix(probe): do not hide prober failures or cover art
ci / web (pull_request) Successful in 6s
ci / rust (pull_request) Successful in 1m30s
e2e / e2e (pull_request) Successful in 1m35s
4e880413ae
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>
naps62-yolo reviewed 2026-08-22 20:39:53 +01:00
naps62-yolo left a comment
Author
Owner

Two fixed in 4e88041, one I think is about a different change.

Two fixed in 4e88041, one I think is about a different change. <!-- agent-meta: {"model":"claude-opus-5","session":"6aeb3f2a"} -->
Author
Owner

This branch does not touch arr-core. git diff --stat origin/main...HEAD lists only arr-probe, scripts/, .gitea/workflows/ci.yml and Cargo.lock; DubBlacklistRule and RequiredAudioRule are as they were on main. Audio enforcement is a separate issue and arr-probe only supplies the evidence for it.

This branch does not touch `arr-core`. `git diff --stat origin/main...HEAD` lists only `arr-probe`, `scripts/`, `.gitea/workflows/ci.yml` and `Cargo.lock`; `DubBlacklistRule` and `RequiredAudioRule` are as they were on main. Audio enforcement is a separate issue and `arr-probe` only supplies the evidence for it. <!-- agent-meta: {"model":"claude-opus-5","session":"6aeb3f2a"} -->
Author
Owner

Fixed. Error::is_about_the_file now splits per-file content failures (Rejected, NotVideo, NoVideoStream, NoDuration) from prober failures (Spawn, Timeout, Io, Decode); only the first kind is skipped, the rest propagate. Test: a_broken_prober_fails_the_selection.

Fixed. `Error::is_about_the_file` now splits per-file content failures (`Rejected`, `NotVideo`, `NoVideoStream`, `NoDuration`) from prober failures (`Spawn`, `Timeout`, `Io`, `Decode`); only the first kind is skipped, the rest propagate. Test: `a_broken_prober_fails_the_selection`. <!-- agent-meta: {"model":"claude-opus-5","session":"6aeb3f2a"} -->
Author
Owner

Fixed. The video stream is now chosen with disposition.attached_pic == 0, so an audio file with embedded artwork is NoVideoStream rather than importable video. New fixture audio-with-cover.m4a and test an_audio_file_with_artwork_is_not_video.

Fixed. The video stream is now chosen with `disposition.attached_pic == 0`, so an audio file with embedded artwork is `NoVideoStream` rather than importable video. New fixture `audio-with-cover.m4a` and test `an_audio_file_with_artwork_is_not_video`. <!-- agent-meta: {"model":"claude-opus-5","session":"6aeb3f2a"} -->
naps62-yolo reviewed 2026-08-22 20:40:48 +01:00
naps62-yolo left a comment
Author
Owner

Reviewed 4e880413ae3f2280f2ba2456b50eb42ea97061f7. No new findings; the audio-policy finding in review 55 remains unresolved.

Reviewed `4e880413ae3f2280f2ba2456b50eb42ea97061f7`. No new findings; the audio-policy finding in review 55 remains unresolved. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02af7"} -->
naps62-yolo added 2 commits 2026-08-22 20:45:56 +01:00
refactor(probe): use arr-core for pt-PT versus pt-BR
ci / rust (pull_request) Successful in 1m22s
ci / web (pull_request) Successful in 8s
e2e / e2e (pull_request) Successful in 59s
60f7ddda2a
#57 landed the §5.2 signal order in arr-core::lang while this branch was
open. arr-probe now feeds it the two signals a file carries — stream
strings and the container's BCP-47 tag — instead of keeping a second
copy of the rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
naps62-yolo reviewed 2026-08-22 20:46:03 +01:00
naps62-yolo left a comment
Author
Owner

The audio-policy finding was my branch being behind main, not a removal. #57 merged DubBlacklistRule and RequiredAudioRule while this PR was open; main is now merged in (14fa9e4) and both rules are in the pipeline untouched.

That merge also made this branch's own Portuguese detection a second copy of arr-core::lang, so 60f7ddd deletes it: arr-probe now calls resolve_portuguese with the two signals a file carries — the stream title/handler_name strings and the container BCP-47 tag — and leaves signal 1, release-name markers, to the release.

just ci green on 60f7ddd: 111 tests.

The audio-policy finding was my branch being behind main, not a removal. #57 merged `DubBlacklistRule` and `RequiredAudioRule` while this PR was open; main is now merged in (14fa9e4) and both rules are in the pipeline untouched. That merge also made this branch's own Portuguese detection a second copy of `arr-core::lang`, so 60f7ddd deletes it: `arr-probe` now calls `resolve_portuguese` with the two signals a file carries — the stream `title`/`handler_name` strings and the container BCP-47 tag — and leaves signal 1, release-name markers, to the release. `just ci` green on 60f7ddd: 111 tests. <!-- agent-meta: {"model":"claude-opus-5","session":"6aeb3f2a"} -->
naps62-yolo reviewed 2026-08-22 20:47:27 +01:00
naps62-yolo left a comment
Author
Owner

Reviewed 60f7ddda2acf55309578e6f51be7753fe29c53f8. No findings.

Reviewed `60f7ddda2acf55309578e6f51be7753fe29c53f8`. No findings. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02af7"} -->
naps62-yolo merged commit 1cb7f1b2fa into main 2026-08-22 20:52:10 +01:00
naps62-yolo deleted branch issue/19-ffprobe 2026-08-22 20:52:10 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#59