From aebe6a256bf3152dbc17b7818c4f8512f6d032a6 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:27:47 -0700 Subject: [PATCH] Fix JAR label matching: by language+codec for label format, by index for TextField Labels like eng_MLP_ now match to the correct stream by checking language and codec hint against the stream's properties. Fixes Barbie where labels were swapped (TrueHD label was on DD stream). TextField format (Civil War) still uses index matching. TrackLabel now carries language + codec_hint for structured matching. --- src/disc.rs | 78 ++++++++++++++++++++++++++++++++++++++++------------- src/jar.rs | 45 ++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/disc.rs b/src/disc.rs index 67d2bf4..d71b223 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -580,35 +580,63 @@ impl Disc { } /// Merge JAR labels into title streams. - /// Matches by position — JAR audio labels correspond to audio streams in order, - /// JAR subtitle labels correspond to subtitle streams in order. + /// + /// 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 fn apply_jar_labels(titles: &mut [Title], jar: &crate::jar::JarLabels) { if jar.audio.is_empty() && jar.subtitle.is_empty() { return; } - for title in titles.iter_mut() { - let mut audio_idx = 0; - let mut sub_idx = 0; + // Check if labels have language+codec info (label format) + let has_content_match = jar.audio.iter().any(|l| !l.language.is_empty() && !l.codec_hint.is_empty()); - for stream in &mut title.streams { - match stream { - Stream::Audio(a) => { - if let Some(label) = jar.audio.get(audio_idx) { - if !label.description.is_empty() { - a.label = label.description.clone(); - } + for title in titles.iter_mut() { + if has_content_match { + // Match by language + codec + Self::apply_labels_by_content(title, jar); + } else { + // Match by index + Self::apply_labels_by_index(title, jar); + } + } + } + + /// Match labels to streams by language + codec hint. + fn apply_labels_by_content(title: &mut Title, jar: &crate::jar::JarLabels) { + // For each JAR audio label, find the matching stream + let mut used = vec![false; jar.audio.len()]; + + for stream in &mut title.streams { + if let Stream::Audio(a) = stream { + // Find a JAR label matching this stream's language + codec + for (i, label) in jar.audio.iter().enumerate() { + if used[i] { continue; } + if label.language == a.language && codec_matches(&label.codec_hint, a.codec) { + if !label.description.is_empty() { + a.label = label.description.clone(); } - audio_idx += 1; + used[i] = true; + break; } - Stream::Subtitle(_s) => { - // TODO: JAR subtitle labels could set forced flag or description - sub_idx += 1; - } - _ => {} } } - let _ = sub_idx; // suppress warning + } + } + + /// Match labels to streams by STN index position. + fn apply_labels_by_index(title: &mut Title, jar: &crate::jar::JarLabels) { + let mut audio_idx = 0; + for stream in &mut title.streams { + if let Stream::Audio(a) = stream { + if let Some(label) = jar.audio.get(audio_idx) { + if !label.description.is_empty() { + a.label = label.description.clone(); + } + } + audio_idx += 1; + } } } @@ -892,6 +920,18 @@ fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048 // ─── Format helpers ──────────────────────────────────────────────────────── +/// Check if a JAR codec hint matches a stream codec. +fn codec_matches(hint: &str, codec: Codec) -> bool { + match hint { + "MLP" => codec == Codec::TrueHd, + "AC3" => codec == Codec::Ac3 || codec == Codec::Ac3Plus, + "DTS" => codec == Codec::Dts || codec == Codec::DtsHdMa || codec == Codec::DtsHdHr, + "LPCM" => codec == Codec::Lpcm, + "ADES" => codec == Codec::Ac3, // descriptive audio is usually DD + _ => false, + } +} + fn format_resolution(video_format: u8, _video_rate: u8) -> String { match video_format { 1 => "480i".into(), diff --git a/src/jar.rs b/src/jar.rs index 69c5be7..4639036 100644 --- a/src/jar.rs +++ b/src/jar.rs @@ -30,6 +30,10 @@ pub struct JarLabels { pub struct TrackLabel { /// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)") pub description: String, + /// ISO 639-2 language code if available (e.g. "eng", "fra") + pub language: String, + /// Codec hint if available (e.g. "MLP"=TrueHD, "AC3"=DD, "DTS") + pub codec_hint: String, /// The raw string from the class file pub raw: String, } @@ -99,8 +103,12 @@ fn try_textfield_format(strings: &[String]) -> Option { subtitle.sort_by_key(|s| s.0); Some(JarLabels { - audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(), - subtitle: subtitle.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(), + audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel { + description: desc, language: String::new(), codec_hint: String::new(), raw, + }).collect(), + subtitle: subtitle.into_iter().map(|(_, desc, raw)| TrackLabel { + description: desc, language: String::new(), codec_hint: String::new(), raw, + }).collect(), playlists, }) } @@ -118,9 +126,15 @@ fn try_label_format(strings: &[String]) -> Option { for s in strings { if let Some(label) = parse_label_string(s) { if label.is_audio && !audio.iter().any(|a: &TrackLabel| a.raw == label.raw) { - audio.push(TrackLabel { description: label.description, raw: label.raw }); + audio.push(TrackLabel { + description: label.description, language: label.language, + codec_hint: label.codec_hint, raw: label.raw, + }); } else if label.is_subtitle && !subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) { - subtitle.push(TrackLabel { description: label.description, raw: label.raw }); + subtitle.push(TrackLabel { + description: label.description, language: label.language, + codec_hint: label.codec_hint, raw: label.raw, + }); } } @@ -136,6 +150,8 @@ fn try_label_format(strings: &[String]) -> Option { struct ParsedLabel { description: String, + language: String, + codec_hint: String, raw: String, is_audio: bool, is_subtitle: bool, @@ -154,25 +170,28 @@ fn parse_label_string(s: &str) -> Option { let hint = parts[1]; let variant = if parts.len() > 2 { parts[2] } else { "" }; - let (description, is_audio, is_subtitle) = match hint { - "MLP" => ("TrueHD".to_string(), true, false), + let (description, codec_hint, is_audio, is_subtitle) = match hint { + "MLP" => ("TrueHD".to_string(), "MLP".to_string(), true, false), "AC3" => { let d = if variant.is_empty() { "compatibility".to_string() } else { variant.to_string() }; - (d, true, false) + (d, "AC3".to_string(), true, false) } - "DTS" => ("DTS".to_string(), true, false), - "LPCM" => ("LPCM".to_string(), true, false), + "DTS" => ("DTS".to_string(), "DTS".to_string(), true, false), + "LPCM" => ("LPCM".to_string(), "LPCM".to_string(), true, false), "ADES" => { let d = if variant.is_empty() { "Descriptive Audio".to_string() } else { format!("Descriptive Audio ({})", variant) }; - (d, true, false) + (d, "ADES".to_string(), true, false) } - h if h.starts_with("AudioStream") => (String::new(), true, false), - h if h.starts_with("PGStream") => (String::new(), false, true), + h if h.starts_with("AudioStream") => (String::new(), String::new(), true, false), + h if h.starts_with("PGStream") => (String::new(), String::new(), false, true), _ => return None, }; - Some(ParsedLabel { description, raw: s.to_string(), is_audio, is_subtitle }) + Some(ParsedLabel { + description, language: language.to_string(), codec_hint, + raw: s.to_string(), is_audio, is_subtitle, + }) } // ── Format 3: Playlist-only ─────────────────────────────────────────────────