Simplify labels API: one call, labels on streams or nothing

labels::apply(session, udf, titles) does everything internally.
Disc::scan() is one line: crate::labels::apply().
No intermediate variables, no fallback logic in disc.rs.

App reads disc.titles[].streams — labels already applied if
disc had config files, empty otherwise. MPLS data always there.
This commit is contained in:
MattJackson
2026-04-07 18:46:29 -07:00
parent 7f7a66e039
commit 8e7778c572
2 changed files with 65 additions and 31 deletions
+2 -3
View File
@@ -421,9 +421,8 @@ 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 stream labels from disc config files
let disc_labels = crate::labels::extract(session, &udf_fs);
Self::apply_disc_labels(&mut titles, &disc_labels);
// Step 5: Enhance streams with disc config file labels (if available)
crate::labels::apply(session, &udf_fs, &mut titles);
// JAR labels (for playlist purpose markers only, not stream labels)
let jar_labels = Self::read_jar_labels(session, &udf_fs);
+63 -28
View File
@@ -2,12 +2,8 @@
//!
//! Searches the disc UDF filesystem for known config files that contain
//! stream labels (language, purpose, codec, forced flags). Four formats
//! supported, tried in order:
//!
//! 1. `language_streams.txt` — Warner CTRM CSV format
//! 2. `menu_base.prop` — Warner CTRM properties format
//! 3. `streamproperties.xml` + `playbackconfig.xml` — Criterion XML format
//! 4. `bluray_project.bin` — Pixelogic binary format
//! supported, tried in order. If found, labels are applied directly
//! to the title streams. If not found, streams keep MPLS data as-is.
mod language_streams;
mod menu_base;
@@ -16,6 +12,7 @@ mod bluray_project;
use crate::drive::DriveSession;
use crate::udf::UdfFs;
use crate::disc::{Title, Stream};
/// A stream label extracted from disc config files.
#[derive(Debug, Clone)]
@@ -46,43 +43,81 @@ pub enum StreamLabelType {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelPurpose {
/// Normal dialogue track
Normal,
/// Audio commentary
Commentary,
/// Descriptive audio (visually impaired)
Descriptive,
/// Music score only
Score,
/// In-movie experience
Ime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelQualifier {
None,
/// Subtitles for deaf and hard of hearing
Sdh,
/// Descriptive service
DescriptiveService,
/// Forced/narrative subtitle
Forced,
}
/// Try all parsers in order, return first successful result.
pub fn extract(session: &mut DriveSession, udf: &UdfFs) -> Vec<StreamLabel> {
// Try each format in order
if let Some(labels) = language_streams::parse(session, udf) {
return labels;
}
if let Some(labels) = menu_base::parse(session, udf) {
return labels;
}
if let Some(labels) = stream_properties::parse(session, udf) {
return labels;
}
if let Some(labels) = bluray_project::parse(session, udf) {
return labels;
/// Search disc for config files, extract labels, apply to streams.
/// If no config files found, streams are left unchanged.
pub fn apply(session: &mut DriveSession, udf: &UdfFs, titles: &mut [Title]) {
let labels = extract(session, udf);
if labels.is_empty() { return; }
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;
if let Some(label) = labels.iter().find(|l|
l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
) {
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()
&& !matches!(label.codec_hint.as_str(), "MLP" | "AC3" | "DTS")
{
parts.push(label.codec_hint.clone());
}
if !parts.is_empty() {
a.label = parts.join(" ");
} else if !label.name.is_empty() {
a.label = label.name.clone();
}
}
}
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::Forced {
s.forced = true;
}
}
}
_ => {}
}
}
}
}
fn extract(session: &mut DriveSession, udf: &UdfFs) -> Vec<StreamLabel> {
if let Some(labels) = language_streams::parse(session, udf) { return labels; }
if let Some(labels) = menu_base::parse(session, udf) { return labels; }
if let Some(labels) = stream_properties::parse(session, udf) { return labels; }
if let Some(labels) = bluray_project::parse(session, udf) { return labels; }
Vec::new()
}