Rewrite JAR parser: multi-format with TextField support
- Extract all class strings once, try format parsers in chain
- Format 1: TextField,Audio{N} (A24/Lionsgate) — fixes Civil War
- Format 2: eng_MLP_ label strings (Warner UHD) — Barbie, Dune
- Format 3: playlist-only (MAIN_FEATURE etc.)
- Simplified TrackLabel to just description + raw
- 5 unit tests for both formats
- Tested on 12 disc captures: Civil War now gets 3 audio + 2 sub labels
Known issues: Barbie labels swapped, Dune returns 0 labels (different format?)
This commit is contained in:
+221
-161
@@ -2,29 +2,24 @@
|
|||||||
//!
|
//!
|
||||||
//! Blu-ray discs with BD-J menus store the menu application as Java JAR files
|
//! Blu-ray discs with BD-J menus store the menu application as Java JAR files
|
||||||
//! in BDMV/JAR/. These contain .class files with string constants that label
|
//! in BDMV/JAR/. These contain .class files with string constants that label
|
||||||
//! audio and subtitle tracks (e.g. "eng_ADES_US_" = Descriptive Audio US).
|
//! audio and subtitle tracks.
|
||||||
//!
|
//!
|
||||||
//! with no way to tell them apart (e.g. 3x "AC-3 5.1 English").
|
//! Multiple label formats exist across studios:
|
||||||
|
//! - Label format: "eng_MLP_", "fra_AudioStream3" (Warner UHD, e.g. Barbie, Dune)
|
||||||
|
//! - TextField format: "TextField,Audio1,English Dolby Atmos,..." (A24/Lionsgate, e.g. Civil War)
|
||||||
//!
|
//!
|
||||||
//! The JAR is a ZIP file. Each .class file has a Java constant pool containing
|
//! Labels match to streams by STN index — the Nth audio label in the JAR
|
||||||
//! UTF-8 string literals. We scan for patterns like:
|
//! corresponds to the Nth audio stream in the MPLS STN table. This is defined
|
||||||
//! {lang}_{codec}_{variant}_ → audio track labels
|
//! by the BD-J API where stream numbers = STN indices (1-based).
|
||||||
//! {lang}_PGStream{n} → subtitle track labels
|
|
||||||
//! MAIN_FEATURE → playlist identification
|
|
||||||
//! FORCED_TRAILER → forced content identification
|
|
||||||
//!
|
|
||||||
//! This is best-effort: if the JAR doesn't contain labels or the format
|
|
||||||
//! is unexpected, we return empty results. The caller falls back to
|
|
||||||
//! showing streams without labels.
|
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
/// Labels extracted from a BD-J JAR file.
|
/// Labels extracted from a BD-J JAR file.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct JarLabels {
|
pub struct JarLabels {
|
||||||
/// Audio track labels in STN order: (language, codec_hint, variant, raw_label)
|
/// Audio track labels in STN order
|
||||||
pub audio: Vec<TrackLabel>,
|
pub audio: Vec<TrackLabel>,
|
||||||
/// Subtitle track labels: (language, stream_index, raw_label)
|
/// Subtitle track labels in STN order
|
||||||
pub subtitle: Vec<TrackLabel>,
|
pub subtitle: Vec<TrackLabel>,
|
||||||
/// Playlist purpose labels (MAIN_FEATURE, FORCED_TRAILER, etc.)
|
/// Playlist purpose labels (MAIN_FEATURE, FORCED_TRAILER, etc.)
|
||||||
pub playlists: Vec<String>,
|
pub playlists: Vec<String>,
|
||||||
@@ -33,155 +28,211 @@ pub struct JarLabels {
|
|||||||
/// A parsed track label from the JAR.
|
/// A parsed track label from the JAR.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TrackLabel {
|
pub struct TrackLabel {
|
||||||
/// ISO 639-2 language code (e.g. "eng", "fra")
|
/// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)")
|
||||||
pub language: String,
|
|
||||||
/// Codec or type hint (e.g. "MLP", "AC3", "ADES", "PGStream")
|
|
||||||
pub hint: String,
|
|
||||||
/// Variant or region (e.g. "US", "UK", "3", "4")
|
|
||||||
pub variant: String,
|
|
||||||
/// Human-readable description derived from the label
|
|
||||||
pub description: String,
|
pub description: String,
|
||||||
/// The raw string from the class file
|
/// The raw string from the class file
|
||||||
pub raw: String,
|
pub raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackLabel {
|
/// Extract track labels from a JAR file (raw ZIP bytes).
|
||||||
/// Parse a label string like "eng_ADES_US_" or "dan_PGStream4"
|
///
|
||||||
fn parse(s: &str) -> Option<Self> {
|
/// Tries multiple format parsers. Returns None if no labels found.
|
||||||
let clean = s.trim_end_matches('_');
|
pub fn extract_labels(jar_data: &[u8]) -> Option<JarLabels> {
|
||||||
let parts: Vec<&str> = clean.splitn(3, '_').collect();
|
let strings = extract_all_jar_strings(jar_data)?;
|
||||||
if parts.len() < 2 {
|
|
||||||
|
// Try each format parser in order
|
||||||
|
try_textfield_format(&strings)
|
||||||
|
.or_else(|| try_label_format(&strings))
|
||||||
|
.or_else(|| try_playlist_only(&strings))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format 1: TextField (A24/Lionsgate) ─────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Pattern: "TextField,Audio{N},{description},..."
|
||||||
|
// "TextField,Subtitle{N},{description},..."
|
||||||
|
// Found in: Civil War, and similar A24/Lionsgate discs
|
||||||
|
|
||||||
|
fn try_textfield_format(strings: &[String]) -> Option<JarLabels> {
|
||||||
|
let mut audio: Vec<(u32, String, String)> = Vec::new(); // (index, description, raw)
|
||||||
|
let mut subtitle: Vec<(u32, String, String)> = Vec::new();
|
||||||
|
let mut playlists = Vec::new();
|
||||||
|
|
||||||
|
for s in strings {
|
||||||
|
if s.starts_with("TextField,Audio") {
|
||||||
|
// TextField,Audio{N},{description},...
|
||||||
|
let parts: Vec<&str> = s.splitn(4, ',').collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
let key = parts[1]; // "Audio1", "Audio2", etc.
|
||||||
|
let desc = parts[2].to_string();
|
||||||
|
if let Some(num_str) = key.strip_prefix("Audio") {
|
||||||
|
if let Ok(idx) = num_str.parse::<u32>() {
|
||||||
|
if !desc.is_empty() {
|
||||||
|
audio.push((idx, desc, s.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if s.starts_with("TextField,Subtitle") {
|
||||||
|
let parts: Vec<&str> = s.splitn(4, ',').collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
let key = parts[1];
|
||||||
|
let desc = parts[2].to_string();
|
||||||
|
if let Some(num_str) = key.strip_prefix("Subtitle") {
|
||||||
|
if let Ok(idx) = num_str.parse::<u32>() {
|
||||||
|
if !desc.eq_ignore_ascii_case("None") && !desc.is_empty() {
|
||||||
|
subtitle.push((idx, desc, s.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_playlist_label(s, &mut playlists);
|
||||||
|
}
|
||||||
|
|
||||||
|
if audio.is_empty() && subtitle.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let language = parts[0].to_string();
|
// Sort by index to ensure STN order
|
||||||
// Language should be 2-3 lowercase letters
|
audio.sort_by_key(|a| a.0);
|
||||||
|
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(),
|
||||||
|
playlists,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format 2: Label strings (Warner UHD) ────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Pattern: "eng_MLP_", "fra_AudioStream3", "dan_PGStream4"
|
||||||
|
// Found in: Barbie, Dune Part Two, and similar Warner UHD discs
|
||||||
|
|
||||||
|
fn try_label_format(strings: &[String]) -> Option<JarLabels> {
|
||||||
|
let mut audio = Vec::new();
|
||||||
|
let mut subtitle = Vec::new();
|
||||||
|
let mut playlists = Vec::new();
|
||||||
|
|
||||||
|
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 });
|
||||||
|
} else if label.is_subtitle && !subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) {
|
||||||
|
subtitle.push(TrackLabel { description: label.description, raw: label.raw });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_playlist_label(s, &mut playlists);
|
||||||
|
}
|
||||||
|
|
||||||
|
if audio.is_empty() && subtitle.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(JarLabels { audio, subtitle, playlists })
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParsedLabel {
|
||||||
|
description: String,
|
||||||
|
raw: String,
|
||||||
|
is_audio: bool,
|
||||||
|
is_subtitle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_label_string(s: &str) -> Option<ParsedLabel> {
|
||||||
|
let clean = s.trim_end_matches('_');
|
||||||
|
let parts: Vec<&str> = clean.splitn(3, '_').collect();
|
||||||
|
if parts.len() < 2 { return None; }
|
||||||
|
|
||||||
|
let language = parts[0];
|
||||||
if language.len() < 2 || language.len() > 3 || !language.chars().all(|c| c.is_ascii_lowercase()) {
|
if language.len() < 2 || language.len() > 3 || !language.chars().all(|c| c.is_ascii_lowercase()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hint = parts[1].to_string();
|
let hint = parts[1];
|
||||||
let variant = if parts.len() > 2 { parts[2].to_string() } else { String::new() };
|
let variant = if parts.len() > 2 { parts[2] } else { "" };
|
||||||
|
|
||||||
let description = match hint.as_str() {
|
let (description, is_audio, is_subtitle) = match hint {
|
||||||
"MLP" => "TrueHD".to_string(),
|
"MLP" => ("TrueHD".to_string(), true, false),
|
||||||
"AC3" => {
|
"AC3" => {
|
||||||
if variant.is_empty() { "compatibility".to_string() }
|
let d = if variant.is_empty() { "compatibility".to_string() } else { variant.to_string() };
|
||||||
else { variant.clone() }
|
(d, true, false)
|
||||||
}
|
}
|
||||||
"DTS" => "DTS".to_string(),
|
"DTS" => ("DTS".to_string(), true, false),
|
||||||
"LPCM" => "LPCM".to_string(),
|
"LPCM" => ("LPCM".to_string(), true, false),
|
||||||
"ADES" => {
|
"ADES" => {
|
||||||
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)
|
||||||
}
|
}
|
||||||
h if h.starts_with("AudioStream") => String::new(), // generic, no extra info
|
h if h.starts_with("AudioStream") => (String::new(), true, false),
|
||||||
h if h.starts_with("PGStream") => String::new(), // generic subtitle
|
h if h.starts_with("PGStream") => (String::new(), false, true),
|
||||||
_ => String::new(),
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(TrackLabel {
|
Some(ParsedLabel { description, raw: s.to_string(), is_audio, is_subtitle })
|
||||||
language,
|
|
||||||
hint,
|
|
||||||
variant,
|
|
||||||
description,
|
|
||||||
raw: s.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is this an audio track label?
|
|
||||||
fn is_audio(&self) -> bool {
|
|
||||||
matches!(self.hint.as_str(), "MLP" | "AC3" | "DTS" | "LPCM" | "ADES")
|
|
||||||
|| self.hint.starts_with("AudioStream")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is this a subtitle track label?
|
|
||||||
fn is_subtitle(&self) -> bool {
|
|
||||||
self.hint.starts_with("PGStream")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract track labels from a JAR file (raw ZIP bytes).
|
// ── Format 3: Playlist-only ─────────────────────────────────────────────────
|
||||||
///
|
//
|
||||||
/// Returns None if the JAR can't be parsed or contains no labels.
|
// Some JARs have no track labels but do have playlist purpose markers.
|
||||||
/// This is best-effort — failure is not an error.
|
|
||||||
pub fn extract_labels(jar_data: &[u8]) -> Option<JarLabels> {
|
|
||||||
let cursor = std::io::Cursor::new(jar_data);
|
|
||||||
let mut archive = zip::ZipArchive::new(cursor).ok()?;
|
|
||||||
|
|
||||||
let mut all_audio = Vec::new();
|
fn try_playlist_only(strings: &[String]) -> Option<JarLabels> {
|
||||||
let mut all_subtitle = Vec::new();
|
let mut playlists = Vec::new();
|
||||||
let mut all_playlists = Vec::new();
|
for s in strings {
|
||||||
|
collect_playlist_label(s, &mut playlists);
|
||||||
for i in 0..archive.len() {
|
|
||||||
let mut file = archive.by_index(i).ok()?;
|
|
||||||
if !file.name().ends_with(".class") {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
if playlists.is_empty() { return None; }
|
||||||
|
Some(JarLabels { audio: Vec::new(), subtitle: Vec::new(), playlists })
|
||||||
|
}
|
||||||
|
|
||||||
let mut data = Vec::new();
|
// ── Common helpers ──────────────────────────────────────────────────────────
|
||||||
file.read_to_end(&mut data).ok()?;
|
|
||||||
|
|
||||||
let strings = extract_class_strings(&data);
|
fn collect_playlist_label(s: &str, playlists: &mut Vec<String>) {
|
||||||
|
if matches!(s.as_ref(),
|
||||||
for s in &strings {
|
|
||||||
// Track labels: {lang}_{type}_{variant}_
|
|
||||||
if let Some(label) = TrackLabel::parse(s) {
|
|
||||||
if label.is_audio() && !all_audio.iter().any(|a: &TrackLabel| a.raw == label.raw) {
|
|
||||||
all_audio.push(label);
|
|
||||||
} else if label.is_subtitle() && !all_subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) {
|
|
||||||
all_subtitle.push(label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playlist purpose labels
|
|
||||||
if matches!(s.as_str(),
|
|
||||||
"MAIN_FEATURE" | "MAIN_FEATURE_INTRO" |
|
"MAIN_FEATURE" | "MAIN_FEATURE_INTRO" |
|
||||||
"FORCED_TRAILER" | "INTL_FORCED_TRAILER" |
|
"FORCED_TRAILER" | "INTL_FORCED_TRAILER" |
|
||||||
"commentary_extras" | "extras"
|
"commentary_extras" | "extras"
|
||||||
) {
|
) {
|
||||||
if !all_playlists.contains(s) {
|
if !playlists.iter().any(|p| p == s) {
|
||||||
all_playlists.push(s.clone());
|
playlists.push(s.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if all_audio.is_empty() && all_subtitle.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(JarLabels {
|
|
||||||
audio: all_audio,
|
|
||||||
subtitle: all_subtitle,
|
|
||||||
playlists: all_playlists,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract all UTF-8 string constants from a Java .class file's constant pool.
|
/// Extract all UTF-8 string constants from all .class files in a JAR.
|
||||||
///
|
fn extract_all_jar_strings(jar_data: &[u8]) -> Option<Vec<String>> {
|
||||||
/// Java class file format:
|
let cursor = std::io::Cursor::new(jar_data);
|
||||||
/// [0:4] magic (0xCAFEBABE)
|
let mut archive = zip::ZipArchive::new(cursor).ok()?;
|
||||||
/// [4:6] minor version
|
|
||||||
/// [6:8] major version
|
|
||||||
/// [8:10] constant_pool_count
|
|
||||||
/// [10:] constant_pool entries
|
|
||||||
///
|
|
||||||
/// CONSTANT_Utf8 (tag=1): u8 tag + u16 length + bytes
|
|
||||||
/// We only extract these — they contain all string literals.
|
|
||||||
fn extract_class_strings(data: &[u8]) -> Vec<String> {
|
|
||||||
let mut strings = Vec::new();
|
|
||||||
|
|
||||||
// Verify Java class magic
|
let mut all_strings = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i).ok()?;
|
||||||
|
if !file.name().ends_with(".class") { continue; }
|
||||||
|
|
||||||
|
let mut data = Vec::new();
|
||||||
|
file.read_to_end(&mut data).ok()?;
|
||||||
|
|
||||||
|
extract_class_strings(&data, &mut all_strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_strings.is_empty() { return None; }
|
||||||
|
Some(all_strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract UTF-8 string constants from a Java .class file's constant pool.
|
||||||
|
fn extract_class_strings(data: &[u8], out: &mut Vec<String>) {
|
||||||
if data.len() < 10 || &data[0..4] != &[0xCA, 0xFE, 0xBA, 0xBE] {
|
if data.len() < 10 || &data[0..4] != &[0xCA, 0xFE, 0xBA, 0xBE] {
|
||||||
return strings;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cp_count = ((data[8] as u16) << 8 | data[9] as u16) as usize;
|
let cp_count = ((data[8] as u16) << 8 | data[9] as u16) as usize;
|
||||||
let mut pos = 10;
|
let mut pos = 10;
|
||||||
|
let mut entry = 1;
|
||||||
|
|
||||||
// Parse constant pool entries
|
|
||||||
let mut entry = 1; // constant pool is 1-indexed
|
|
||||||
while entry < cp_count && pos < data.len() {
|
while entry < cp_count && pos < data.len() {
|
||||||
let tag = data[pos];
|
let tag = data[pos];
|
||||||
pos += 1;
|
pos += 1;
|
||||||
@@ -195,32 +246,21 @@ fn extract_class_strings(data: &[u8]) -> Vec<String> {
|
|||||||
if pos + len > data.len() { break; }
|
if pos + len > data.len() { break; }
|
||||||
|
|
||||||
if let Ok(s) = std::str::from_utf8(&data[pos..pos + len]) {
|
if let Ok(s) = std::str::from_utf8(&data[pos..pos + len]) {
|
||||||
// Only keep strings that look like track labels
|
if s.len() >= 3 && s.len() <= 500 {
|
||||||
// Must contain underscore and be reasonable length
|
out.push(s.to_string());
|
||||||
if s.len() >= 5 && s.len() <= 100 && s.contains('_') {
|
|
||||||
strings.push(s.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos += len;
|
pos += len;
|
||||||
}
|
}
|
||||||
// CONSTANT_Integer, CONSTANT_Float
|
|
||||||
3 | 4 => { pos += 4; }
|
3 | 4 => { pos += 4; }
|
||||||
// CONSTANT_Long, CONSTANT_Double (take 2 entries)
|
|
||||||
5 | 6 => { pos += 8; entry += 1; }
|
5 | 6 => { pos += 8; entry += 1; }
|
||||||
// CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType
|
|
||||||
7 | 8 | 16 => { pos += 2; }
|
7 | 8 | 16 => { pos += 2; }
|
||||||
// CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_InterfaceMethodref,
|
|
||||||
// CONSTANT_NameAndType, CONSTANT_InvokeDynamic
|
|
||||||
9 | 10 | 11 | 12 | 18 => { pos += 4; }
|
9 | 10 | 11 | 12 | 18 => { pos += 4; }
|
||||||
// CONSTANT_MethodHandle
|
|
||||||
15 => { pos += 3; }
|
15 => { pos += 3; }
|
||||||
// Unknown tag — can't continue parsing safely
|
|
||||||
_ => { break; }
|
_ => { break; }
|
||||||
}
|
}
|
||||||
entry += 1;
|
entry += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
strings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -228,36 +268,56 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_audio_labels() {
|
fn test_textfield_audio() {
|
||||||
let l = TrackLabel::parse("eng_MLP_").unwrap();
|
let strings = vec![
|
||||||
assert_eq!(l.language, "eng");
|
"TextField,Audio1,English Dolby Atmos,Font,296,763,275,25,left".to_string(),
|
||||||
assert_eq!(l.hint, "MLP");
|
"TextField,Audio2,English Descriptive Audio,Font,296,803,275,25,left".to_string(),
|
||||||
assert_eq!(l.description, "TrueHD");
|
"TextField,Audio3,Spanish 5.1 Dolby Digital,Font,296,843,275,25,left".to_string(),
|
||||||
assert!(l.is_audio());
|
];
|
||||||
|
let labels = try_textfield_format(&strings).unwrap();
|
||||||
let l = TrackLabel::parse("eng_ADES_US_").unwrap();
|
assert_eq!(labels.audio.len(), 3);
|
||||||
assert_eq!(l.language, "eng");
|
assert_eq!(labels.audio[0].description, "English Dolby Atmos");
|
||||||
assert_eq!(l.hint, "ADES");
|
assert_eq!(labels.audio[1].description, "English Descriptive Audio");
|
||||||
assert_eq!(l.variant, "US");
|
assert_eq!(labels.audio[2].description, "Spanish 5.1 Dolby Digital");
|
||||||
assert_eq!(l.description, "Descriptive Audio (US)");
|
|
||||||
assert!(l.is_audio());
|
|
||||||
|
|
||||||
let l = TrackLabel::parse("fra_AudioStream3").unwrap();
|
|
||||||
assert_eq!(l.language, "fra");
|
|
||||||
assert!(l.is_audio());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_subtitle_labels() {
|
fn test_textfield_subtitle_skips_none() {
|
||||||
let l = TrackLabel::parse("dan_PGStream4").unwrap();
|
let strings = vec![
|
||||||
assert_eq!(l.language, "dan");
|
"TextField,Subtitle0,None,Font,1312,843,275,25,left".to_string(),
|
||||||
assert!(l.is_subtitle());
|
"TextField,Subtitle1,English SDH,Font,1312,763,275,25,left".to_string(),
|
||||||
|
"TextField,Subtitle2,Spanish,Font,1312,803,275,25,left".to_string(),
|
||||||
|
];
|
||||||
|
let labels = try_textfield_format(&strings).unwrap();
|
||||||
|
assert_eq!(labels.subtitle.len(), 2);
|
||||||
|
assert_eq!(labels.subtitle[0].description, "English SDH");
|
||||||
|
assert_eq!(labels.subtitle[1].description, "Spanish");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_reject_non_labels() {
|
fn test_label_format_audio() {
|
||||||
assert!(TrackLabel::parse("substring").is_none());
|
let strings = vec![
|
||||||
assert!(TrackLabel::parse("equals").is_none());
|
"eng_MLP_".to_string(),
|
||||||
assert!(TrackLabel::parse("Code").is_none());
|
"eng_ADES_US_".to_string(),
|
||||||
|
"fra_AudioStream3".to_string(),
|
||||||
|
];
|
||||||
|
let labels = try_label_format(&strings).unwrap();
|
||||||
|
assert_eq!(labels.audio.len(), 3);
|
||||||
|
assert_eq!(labels.audio[0].description, "TrueHD");
|
||||||
|
assert_eq!(labels.audio[1].description, "Descriptive Audio (US)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_label_format_subtitle() {
|
||||||
|
let strings = vec!["dan_PGStream4".to_string()];
|
||||||
|
let labels = try_label_format(&strings).unwrap();
|
||||||
|
assert_eq!(labels.subtitle.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_labels() {
|
||||||
|
let strings = vec!["java/lang/Object".to_string(), "toString".to_string()];
|
||||||
|
assert!(try_textfield_format(&strings).is_none());
|
||||||
|
assert!(try_label_format(&strings).is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user