Add labels module: 4 disc file parsers for stream labels

src/labels/ with 4 parsers tried in order:
1. language_streams.txt (Warner CTRM CSV)
2. menu_base.prop (Warner CTRM properties)
3. streamproperties.xml + playbackconfig.xml (Criterion XML)
4. bluray_project.bin (Pixelogic binary tokens)

Disc::scan() calls labels::extract() → apply_disc_labels().
If no disc files found, streams keep MPLS data as-is.
No JAR fallback — disc files or nothing.

Covers 4/8 discs with JARs (Dunkirk UHD, V for Vendetta BD,
Being There, Barbie). Remaining 3 (Civil War, Dune, V for
Vendetta UHD) have no disc config files.
This commit is contained in:
MattJackson
2026-04-07 18:42:33 -07:00
parent 8a95787426
commit ba44f7d928
7 changed files with 714 additions and 2 deletions
+63 -2
View File
@@ -421,9 +421,12 @@ impl Disc {
// Step 4: Read disc title from META/DL/bdmt_eng.xml
let meta_title = Self::read_meta_title(session, &udf_fs);
// Step 5: Extract JAR track labels and merge into streams
// Step 5: Extract stream labels from disc config files
let disc_labels = crate::labels::extract(session, &udf_fs);
Self::apply_disc_labels(&mut titles, &disc_labels);
// JAR labels (for playlist purpose markers only, not stream labels)
let jar_labels = Self::read_jar_labels(session, &udf_fs);
Self::apply_jar_labels(&mut titles, &jar_labels);
// Step 6: Detect AACS encryption
let encrypted = udf_fs.find_dir("/AACS").is_some()
@@ -584,6 +587,64 @@ impl Disc {
/// Two matching strategies:
/// 1. By language+codec (label format like eng_MLP_) — matches stream by content
/// 2. By index (TextField format) — Nth label → Nth stream
/// Apply disc config file labels to streams by stream number.
/// Matches by STN index — label stream_number N → Nth audio/subtitle stream.
fn apply_disc_labels(titles: &mut [Title], labels: &[crate::labels::StreamLabel]) {
use crate::labels::{StreamLabelType, LabelPurpose, LabelQualifier};
for title in titles.iter_mut() {
let mut audio_idx: u16 = 0;
let mut sub_idx: u16 = 0;
for stream in &mut title.streams {
match stream {
Stream::Audio(a) => {
audio_idx += 1;
// Find label with matching stream number
if let Some(label) = labels.iter().find(|l|
l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
) {
// Build label string from purpose + region
let mut parts = Vec::new();
match label.purpose {
LabelPurpose::Commentary => parts.push("Commentary".to_string()),
LabelPurpose::Descriptive => parts.push("Descriptive Audio".to_string()),
LabelPurpose::Score => parts.push("Score".to_string()),
LabelPurpose::Ime => parts.push("IME".to_string()),
LabelPurpose::Normal => {}
}
if !label.region.is_empty() { parts.push(format!("({})", label.region)); }
if !label.codec_hint.is_empty() && label.codec_hint != "MLP" && label.codec_hint != "AC3" {
parts.push(label.codec_hint.clone());
}
if !label.name.is_empty() && parts.is_empty() {
a.label = label.name.clone();
} else if !parts.is_empty() {
a.label = parts.join(" ");
}
}
}
Stream::Subtitle(s) => {
sub_idx += 1;
if let Some(label) = labels.iter().find(|l|
l.stream_type == StreamLabelType::Subtitle && l.stream_number == sub_idx
) {
if label.qualifier == LabelQualifier::Sdh {
// Set a label for SDH
s.forced = false;
// TODO: add sdh field to SubtitleStream
}
if label.qualifier == LabelQualifier::Forced {
s.forced = true;
}
}
}
_ => {}
}
}
}
}
fn apply_jar_labels(titles: &mut [Title], jar: &crate::jar::JarLabels) {
if jar.audio.is_empty() && jar.subtitle.is_empty() {
return;