Files
arr/crates/arr-probe/src/ffprobe.rs
T
Miguel Palhas e852f42a9c feat(arr): name subtitle codecs and dispositions
SubtitleTrack now carries the codec, split text formats from bitmap
ones per DESIGN.md §15, plus the forced and SDH dispositions ffprobe
reports. Without the forced flag a file carrying only a forced track
read as satisfied for that language.
2026-08-24 22:32:01 +01:00

93 lines
2.7 KiB
Rust

//! The slice of `ffprobe`'s JSON output this crate reads.
//!
//! Everything is optional because `ffprobe` omits fields rather than nulling
//! them, and which ones it omits depends on the container.
use std::collections::BTreeMap;
use serde::Deserialize;
/// One `ffprobe -print_format json -show_format -show_streams` document.
#[derive(Debug, Deserialize)]
pub(crate) struct Output {
#[serde(default)]
pub format: Format,
#[serde(default)]
pub streams: Vec<Stream>,
}
/// Container-level facts.
#[derive(Debug, Default, Deserialize)]
pub(crate) struct Format {
#[serde(rename = "format_name")]
pub name: Option<String>,
pub duration: Option<String>,
pub size: Option<String>,
}
/// One stream of any kind. `codec_type` says which.
#[derive(Debug, Deserialize)]
pub(crate) struct Stream {
pub codec_type: Option<String>,
pub codec_name: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub color_transfer: Option<String>,
pub duration: Option<String>,
#[serde(default)]
pub disposition: Disposition,
#[serde(default)]
pub tags: Tags,
#[serde(default)]
pub side_data_list: Vec<SideData>,
}
impl Stream {
pub fn is_kind(&self, kind: &str) -> bool {
self.codec_type.as_deref() == Some(kind)
}
/// A real video stream, not the cover art bolted onto an audio file.
pub fn is_video(&self) -> bool {
self.is_kind("video") && self.disposition.attached_pic == 0
}
}
/// Stream flags. `attached_pic` stops an MP3 or FLAC's embedded artwork
/// passing for video; `forced` and `hearing_impaired` are §15's track
/// dispositions.
#[derive(Debug, Default, Deserialize)]
pub(crate) struct Disposition {
#[serde(default)]
pub attached_pic: u8,
#[serde(default)]
pub forced: u8,
#[serde(rename = "hearing_impaired", default)]
pub hearing_impaired: u8,
}
/// Stream metadata. Matroska writes `HANDLER_NAME`, MP4 writes `handler_name`,
/// so every lookup here is case-insensitive.
#[derive(Debug, Default, Deserialize)]
pub(crate) struct Tags(BTreeMap<String, String>);
impl Tags {
pub fn get(&self, key: &str) -> Option<&str> {
self.0
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(key))
.map(|(_, value)| value.as_str())
.filter(|value| !value.is_empty())
}
}
/// A stream side-data block. Only the Dolby Vision configuration record and
/// the HDR10+ marker are read; the rest are ignored.
#[derive(Debug, Deserialize)]
pub(crate) struct SideData {
#[serde(rename = "side_data_type")]
pub kind: Option<String>,
pub dv_profile: Option<u8>,
pub dv_bl_signal_compatibility_id: Option<u8>,
}