244 lines
7.1 KiB
Rust
244 lines
7.1 KiB
Rust
//! Real `ffprobe` runs against the committed fixture clips (DESIGN.md §12).
|
|
//!
|
|
//! The clips are generated by `scripts/make-probe-fixtures.sh`. Every
|
|
//! assertion here is about output `ffprobe` actually produced, not a recorded
|
|
//! JSON document, because the shape of that output is exactly what breaks
|
|
//! between ffmpeg releases.
|
|
|
|
// Same per-target quirk as elsewhere in the workspace: an integration test
|
|
// links the library's dependencies without using them directly.
|
|
use {serde as _, serde_json as _, thiserror as _, tracing as _};
|
|
|
|
use std::{path::PathBuf, time::Duration};
|
|
|
|
use arr_core::{DolbyVisionProfile, HdrFormat, Language, Resolution};
|
|
use arr_probe::{Error, Prober, RuntimeMatch};
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_feature_reports_every_track_it_carries() {
|
|
let file = Prober::new()
|
|
.probe(fixture("movie-1080p.mkv"))
|
|
.await
|
|
.expect("the fixture is readable");
|
|
|
|
assert_eq!(file.media.resolution, Resolution::R1080p);
|
|
assert_eq!(file.video_codec, "h264");
|
|
assert_eq!(file.media.hdr, HdrFormat::Sdr);
|
|
assert!(file.size > 0);
|
|
assert!(file.duration >= Duration::from_secs(3));
|
|
|
|
// A file never claims a source; that stays a claim of the release name.
|
|
assert_eq!(file.media.source, None);
|
|
|
|
let languages: Vec<&Language> = file
|
|
.media
|
|
.audio_tracks
|
|
.iter()
|
|
.map(|track| &track.language)
|
|
.collect();
|
|
assert_eq!(
|
|
languages,
|
|
vec![
|
|
&Language::Other("en".to_owned()),
|
|
&Language::PortugueseBrazil,
|
|
&Language::PortuguesePortugal,
|
|
]
|
|
);
|
|
|
|
let brazilian = &file.media.audio_tracks[1];
|
|
assert_eq!(brazilian.title.as_deref(), Some("Português (Brasil)"));
|
|
assert_eq!(
|
|
brazilian.handler_name.as_deref(),
|
|
Some("Portuguese (Brazil)")
|
|
);
|
|
|
|
let subtitles: Vec<&Language> = file
|
|
.media
|
|
.subtitle_tracks
|
|
.iter()
|
|
.map(|track| &track.language)
|
|
.collect();
|
|
assert_eq!(
|
|
subtitles,
|
|
vec![
|
|
&Language::Other("en".to_owned()),
|
|
// `por` with nothing else to go on. Not guessed either way.
|
|
&Language::PortugueseUnverified,
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn hdr10_comes_from_the_transfer_function() {
|
|
let file = Prober::new()
|
|
.probe(fixture("hdr10-2160p.mkv"))
|
|
.await
|
|
.expect("the fixture is readable");
|
|
|
|
assert_eq!(file.media.resolution, Resolution::R2160p);
|
|
assert_eq!(file.video_codec, "hevc");
|
|
assert_eq!(file.media.hdr, HdrFormat::Hdr10);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dolby_vision_profile_5_is_reported_as_a_number() {
|
|
let file = Prober::new()
|
|
.probe(fixture("dv-profile5.mp4"))
|
|
.await
|
|
.expect("the fixture is readable");
|
|
|
|
assert_eq!(
|
|
file.media.hdr,
|
|
HdrFormat::DolbyVision(DolbyVisionProfile {
|
|
profile: 5,
|
|
compatibility_id: Some(0),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dolby_vision_profile_8_1_keeps_its_compatibility_id() {
|
|
let file = Prober::new()
|
|
.probe(fixture("dv-profile8-1.mp4"))
|
|
.await
|
|
.expect("the fixture is readable");
|
|
|
|
assert_eq!(
|
|
file.media.hdr,
|
|
HdrFormat::DolbyVision(DolbyVisionProfile {
|
|
profile: 8,
|
|
compatibility_id: Some(1),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_text_file_is_not_media() {
|
|
let error = Prober::new()
|
|
.probe(fixture("notes.nfo"))
|
|
.await
|
|
.expect_err("a text file is not media");
|
|
|
|
assert!(matches!(error, Error::NotVideo { .. }), "{error:?}");
|
|
}
|
|
|
|
/// Cover art is a video stream as far as ffprobe is concerned.
|
|
#[tokio::test]
|
|
async fn an_audio_file_with_artwork_is_not_video() {
|
|
let error = Prober::new()
|
|
.probe(fixture("audio-with-cover.m4a"))
|
|
.await
|
|
.expect_err("an attached picture is not a video stream");
|
|
|
|
assert!(matches!(error, Error::NoVideoStream { .. }), "{error:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_missing_file_is_an_error() {
|
|
let error = Prober::new()
|
|
.probe(fixture("does-not-exist.mkv"))
|
|
.await
|
|
.expect_err("nothing to probe");
|
|
|
|
assert!(matches!(error, Error::Rejected { .. }), "{error:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_missing_ffprobe_is_an_error() {
|
|
let error = Prober::new()
|
|
.with_binary("ffprobe-that-does-not-exist")
|
|
.probe(fixture("movie-1080p.mkv"))
|
|
.await
|
|
.expect_err("no binary to run");
|
|
|
|
assert!(matches!(error, Error::Spawn { .. }), "{error:?}");
|
|
}
|
|
|
|
/// The extras reel is the larger file on disk and the shorter one on the
|
|
/// clock. Size alone picks it; the runtime check does not.
|
|
#[tokio::test]
|
|
async fn runtime_beats_size_when_picking_the_feature() {
|
|
let selection = Prober::new()
|
|
.select_feature(
|
|
[
|
|
fixture("extras-360p.mkv"),
|
|
fixture("movie-1080p.mkv"),
|
|
fixture("notes.nfo"),
|
|
],
|
|
Some(Duration::from_secs(3)),
|
|
)
|
|
.await
|
|
.expect("one of them is a video file");
|
|
|
|
assert_eq!(selection.feature.path, fixture("movie-1080p.mkv"));
|
|
assert_eq!(selection.runtime, RuntimeMatch::Matched);
|
|
assert_eq!(selection.others.len(), 1);
|
|
assert_eq!(selection.others[0].path, fixture("extras-360p.mkv"));
|
|
assert!(selection.others[0].size > selection.feature.size);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn without_an_expected_runtime_the_largest_file_wins() {
|
|
let selection = Prober::new()
|
|
.select_feature(
|
|
[fixture("movie-1080p.mkv"), fixture("extras-360p.mkv")],
|
|
None,
|
|
)
|
|
.await
|
|
.expect("both are video files");
|
|
|
|
assert_eq!(selection.feature.path, fixture("extras-360p.mkv"));
|
|
assert_eq!(selection.runtime, RuntimeMatch::Unchecked);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn nothing_within_tolerance_still_returns_the_largest_file() {
|
|
let expected = Duration::from_hours(2);
|
|
let selection = Prober::new()
|
|
.select_feature(
|
|
[fixture("movie-1080p.mkv"), fixture("extras-360p.mkv")],
|
|
Some(expected),
|
|
)
|
|
.await
|
|
.expect("both are video files");
|
|
|
|
assert_eq!(selection.feature.path, fixture("extras-360p.mkv"));
|
|
assert!(
|
|
matches!(
|
|
selection.runtime,
|
|
RuntimeMatch::Mismatched { expected: e, .. } if e == expected
|
|
),
|
|
"{:?}",
|
|
selection.runtime
|
|
);
|
|
}
|
|
|
|
/// A prober that cannot run is not the same fact as a torrent without video,
|
|
/// and must not be reported as one.
|
|
#[tokio::test]
|
|
async fn a_broken_prober_fails_the_selection() {
|
|
let error = Prober::new()
|
|
.with_binary("ffprobe-that-does-not-exist")
|
|
.select_feature([fixture("movie-1080p.mkv")], None)
|
|
.await
|
|
.expect_err("nothing could be probed");
|
|
|
|
assert!(matches!(error, Error::Spawn { .. }), "{error:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_torrent_with_no_video_file_selects_nothing() {
|
|
let error = Prober::new()
|
|
.select_feature([fixture("notes.nfo")], None)
|
|
.await
|
|
.expect_err("no video among the candidates");
|
|
|
|
assert!(matches!(error, Error::NoCandidates), "{error:?}");
|
|
}
|