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.
This commit is contained in:
+54
-14
@@ -580,20 +580,56 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Merge JAR labels into title streams.
|
/// 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) {
|
fn apply_jar_labels(titles: &mut [Title], jar: &crate::jar::JarLabels) {
|
||||||
if jar.audio.is_empty() && jar.subtitle.is_empty() {
|
if jar.audio.is_empty() && jar.subtitle.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 title in titles.iter_mut() {
|
for title in titles.iter_mut() {
|
||||||
let mut audio_idx = 0;
|
if has_content_match {
|
||||||
let mut sub_idx = 0;
|
// 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 {
|
for stream in &mut title.streams {
|
||||||
match stream {
|
if let Stream::Audio(a) = stream {
|
||||||
Stream::Audio(a) => {
|
// 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();
|
||||||
|
}
|
||||||
|
used[i] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 let Some(label) = jar.audio.get(audio_idx) {
|
||||||
if !label.description.is_empty() {
|
if !label.description.is_empty() {
|
||||||
a.label = label.description.clone();
|
a.label = label.description.clone();
|
||||||
@@ -601,14 +637,6 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
audio_idx += 1;
|
audio_idx += 1;
|
||||||
}
|
}
|
||||||
Stream::Subtitle(_s) => {
|
|
||||||
// TODO: JAR subtitle labels could set forced flag or description
|
|
||||||
sub_idx += 1;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = sub_idx; // suppress warning
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,6 +920,18 @@ fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048
|
|||||||
|
|
||||||
// ─── Format helpers ────────────────────────────────────────────────────────
|
// ─── 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 {
|
fn format_resolution(video_format: u8, _video_rate: u8) -> String {
|
||||||
match video_format {
|
match video_format {
|
||||||
1 => "480i".into(),
|
1 => "480i".into(),
|
||||||
|
|||||||
+32
-13
@@ -30,6 +30,10 @@ pub struct JarLabels {
|
|||||||
pub struct TrackLabel {
|
pub struct TrackLabel {
|
||||||
/// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)")
|
/// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)")
|
||||||
pub description: String,
|
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
|
/// The raw string from the class file
|
||||||
pub raw: String,
|
pub raw: String,
|
||||||
}
|
}
|
||||||
@@ -99,8 +103,12 @@ fn try_textfield_format(strings: &[String]) -> Option<JarLabels> {
|
|||||||
subtitle.sort_by_key(|s| s.0);
|
subtitle.sort_by_key(|s| s.0);
|
||||||
|
|
||||||
Some(JarLabels {
|
Some(JarLabels {
|
||||||
audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(),
|
audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel {
|
||||||
subtitle: subtitle.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(),
|
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,
|
playlists,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -118,9 +126,15 @@ fn try_label_format(strings: &[String]) -> Option<JarLabels> {
|
|||||||
for s in strings {
|
for s in strings {
|
||||||
if let Some(label) = parse_label_string(s) {
|
if let Some(label) = parse_label_string(s) {
|
||||||
if label.is_audio && !audio.iter().any(|a: &TrackLabel| a.raw == label.raw) {
|
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) {
|
} 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<JarLabels> {
|
|||||||
|
|
||||||
struct ParsedLabel {
|
struct ParsedLabel {
|
||||||
description: String,
|
description: String,
|
||||||
|
language: String,
|
||||||
|
codec_hint: String,
|
||||||
raw: String,
|
raw: String,
|
||||||
is_audio: bool,
|
is_audio: bool,
|
||||||
is_subtitle: bool,
|
is_subtitle: bool,
|
||||||
@@ -154,25 +170,28 @@ fn parse_label_string(s: &str) -> Option<ParsedLabel> {
|
|||||||
let hint = parts[1];
|
let hint = parts[1];
|
||||||
let variant = if parts.len() > 2 { parts[2] } else { "" };
|
let variant = if parts.len() > 2 { parts[2] } else { "" };
|
||||||
|
|
||||||
let (description, is_audio, is_subtitle) = match hint {
|
let (description, codec_hint, is_audio, is_subtitle) = match hint {
|
||||||
"MLP" => ("TrueHD".to_string(), true, false),
|
"MLP" => ("TrueHD".to_string(), "MLP".to_string(), true, false),
|
||||||
"AC3" => {
|
"AC3" => {
|
||||||
let d = if variant.is_empty() { "compatibility".to_string() } else { variant.to_string() };
|
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),
|
"DTS" => ("DTS".to_string(), "DTS".to_string(), true, false),
|
||||||
"LPCM" => ("LPCM".to_string(), true, false),
|
"LPCM" => ("LPCM".to_string(), "LPCM".to_string(), true, false),
|
||||||
"ADES" => {
|
"ADES" => {
|
||||||
let d = if variant.is_empty() { "Descriptive Audio".to_string() }
|
let d = if variant.is_empty() { "Descriptive Audio".to_string() }
|
||||||
else { format!("Descriptive Audio ({})", variant) };
|
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("AudioStream") => (String::new(), String::new(), true, false),
|
||||||
h if h.starts_with("PGStream") => (String::new(), false, true),
|
h if h.starts_with("PGStream") => (String::new(), String::new(), false, true),
|
||||||
_ => return None,
|
_ => 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 ─────────────────────────────────────────────────
|
// ── Format 3: Playlist-only ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user