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 ─────────────────────────────────────────────────