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:
@@ -0,0 +1,88 @@
|
||||
//! Parser for `language_streams.txt` — Warner CTRM CSV format.
|
||||
//!
|
||||
//! Found at: `BDMV/JAR/*/language_streams.txt`
|
||||
//!
|
||||
//! Format:
|
||||
//! ```text
|
||||
//! playlist_id, type, stream_num, language, variant
|
||||
//! 100, audio_production, 1, eng, atmos
|
||||
//! 100, subtitle_narrative, 7, fra,
|
||||
//! ```
|
||||
|
||||
use crate::drive::DriveSession;
|
||||
use crate::udf::{UdfFs, DirEntry};
|
||||
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier};
|
||||
|
||||
pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||
// Search BDMV/JAR/*/ for language_streams.txt
|
||||
let jar_dir = udf.find_dir("/BDMV/JAR")?;
|
||||
let data = find_and_read(session, udf, jar_dir, "language_streams.txt")?;
|
||||
let text = std::str::from_utf8(&data).ok()?;
|
||||
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') { continue; }
|
||||
|
||||
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
|
||||
if parts.len() < 4 { continue; }
|
||||
|
||||
let _playlist_id = parts[0]; // could filter by playlist later
|
||||
let type_str = parts[1];
|
||||
let stream_num: u16 = match parts[2].parse() {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let language = parts[3].to_string();
|
||||
let variant = if parts.len() > 4 { parts[4].to_string() } else { String::new() };
|
||||
|
||||
let (stream_type, purpose, qualifier) = match type_str {
|
||||
"audio_production" => (StreamLabelType::Audio, LabelPurpose::Normal, LabelQualifier::None),
|
||||
"audio_commentary" => (StreamLabelType::Audio, LabelPurpose::Commentary, LabelQualifier::None),
|
||||
"audio_ime" => (StreamLabelType::Audio, LabelPurpose::Ime, LabelQualifier::None),
|
||||
"subtitle_production" => (StreamLabelType::Subtitle, LabelPurpose::Normal, LabelQualifier::None),
|
||||
"subtitle_commentary" => (StreamLabelType::Subtitle, LabelPurpose::Commentary, LabelQualifier::None),
|
||||
"subtitle_narrative" => (StreamLabelType::Subtitle, LabelPurpose::Normal, LabelQualifier::Forced),
|
||||
"subtitle_dual" => (StreamLabelType::Subtitle, LabelPurpose::Normal, LabelQualifier::None),
|
||||
"subtitle_bonus" => (StreamLabelType::Subtitle, LabelPurpose::Normal, LabelQualifier::None),
|
||||
"subtitle_ime" => (StreamLabelType::Subtitle, LabelPurpose::Ime, LabelQualifier::None),
|
||||
"subtitle_ime_narrative" => (StreamLabelType::Subtitle, LabelPurpose::Ime, LabelQualifier::Forced),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Skip feature_override_default lines
|
||||
if type_str == "feature_override_default" { continue; }
|
||||
|
||||
let codec_hint = variant.clone(); // "atmos", "eda", etc.
|
||||
|
||||
labels.push(StreamLabel {
|
||||
stream_number: stream_num,
|
||||
stream_type,
|
||||
language,
|
||||
name: String::new(),
|
||||
purpose,
|
||||
qualifier,
|
||||
codec_hint,
|
||||
region: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if labels.is_empty() { return None; }
|
||||
Some(labels)
|
||||
}
|
||||
|
||||
/// Search subdirectories of a parent for a file by name.
|
||||
fn find_and_read(session: &mut DriveSession, udf: &UdfFs, parent: &DirEntry, filename: &str) -> Option<Vec<u8>> {
|
||||
for entry in &parent.entries {
|
||||
if entry.is_dir {
|
||||
let sub_path = format!("/BDMV/JAR/{}/{}", entry.name, filename);
|
||||
if let Ok(data) = udf.read_file(session, &sub_path) {
|
||||
if !data.is_empty() {
|
||||
return Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user