use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, xml};
use crate::sector::SectorSource;
use crate::udf::UdfFs;
use std::collections::HashMap;
pub fn detect(udf: &UdfFs) -> bool {
super::jar_file_exists(udf, "streamproperties.xml")
}
pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult> {
let sp_data = super::read_jar_file(reader, udf, "streamproperties.xml")?;
let sp_text = std::str::from_utf8(&sp_data).ok()?;
let stream_infos = parse_stream_infos(sp_text);
if stream_infos.is_empty() {
return None;
}
let mut stream_map: HashMap<String, u16> = HashMap::new();
if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml") {
if let Ok(pc_text) = std::str::from_utf8(&pc_data) {
parse_playback_config(pc_text, &mut stream_map);
}
}
let stream_nums = assign_stream_numbers(&stream_infos, &stream_map);
let mut labels = Vec::new();
for (info, &stream_num) in stream_infos.iter().zip(stream_nums.iter()) {
labels.push(StreamLabel {
stream_number: stream_num,
stream_type: info.stream_type,
language: info.language.clone(),
name: String::new(),
purpose: info.purpose,
qualifier: info.qualifier,
codec_hint: String::new(),
variant: info.variant.clone(),
});
}
if labels.is_empty() {
return None;
}
Some(ParseResult::high(labels))
}
fn assign_stream_numbers(infos: &[StreamInfo], stream_map: &HashMap<String, u16>) -> Vec<u16> {
let mut taken_audio: Vec<u16> = Vec::new();
let mut taken_sub: Vec<u16> = Vec::new();
for info in infos {
if let Some(&n) = stream_map.get(&info.id) {
match info.stream_type {
StreamLabelType::Audio => taken_audio.push(n),
StreamLabelType::Subtitle => taken_sub.push(n),
}
}
}
let mut audio_idx: u16 = 1;
let mut sub_idx: u16 = 1;
let mut out = Vec::with_capacity(infos.len());
for info in infos {
let n = match stream_map.get(&info.id).copied() {
Some(n) => n,
None => {
let (idx, taken) = match info.stream_type {
StreamLabelType::Audio => (&mut audio_idx, &taken_audio),
StreamLabelType::Subtitle => (&mut sub_idx, &taken_sub),
};
while taken.contains(idx) {
*idx = idx.saturating_add(1);
}
let n = *idx;
*idx = idx.saturating_add(1);
n
}
};
out.push(n);
}
out
}
struct StreamInfo {
id: String,
stream_type: StreamLabelType,
language: String,
variant: String,
purpose: LabelPurpose,
qualifier: LabelQualifier,
}
fn parse_stream_infos(text: &str) -> Vec<StreamInfo> {
let mut infos = Vec::new();
for (tag_name, stream_type) in [
("AudioStreamInfos", StreamLabelType::Audio),
("SubtitleStreamInfos", StreamLabelType::Subtitle),
] {
let mut from = 0;
while let Some((start, end)) = xml::find_element(text, tag_name, from) {
let block = &text[start..end];
let id = xml::text(block, "ID").unwrap_or_default();
let lang_id = xml::text(block, "LangInfoID").unwrap_or_default();
let content = xml::text(block, "Content").unwrap_or_default();
let qualifier_str = xml::text(block, "Qualifier").unwrap_or_default();
let (language, variant) = if lang_id.contains('_') {
let parts: Vec<&str> = lang_id.splitn(2, '_').collect();
(parts[0].to_lowercase(), parts[1].to_string())
} else {
(lang_id.to_lowercase(), String::new())
};
let purpose = if content.eq_ignore_ascii_case("COMMENTARY") {
LabelPurpose::Commentary
} else {
LabelPurpose::Normal
};
let qualifier = match qualifier_str.to_ascii_uppercase().as_str() {
"SDH" => LabelQualifier::Sdh,
"DS" => LabelQualifier::DescriptiveService,
_ => LabelQualifier::None,
};
infos.push(StreamInfo {
id,
stream_type,
language,
variant,
purpose,
qualifier,
});
from = end;
}
}
infos
}
fn parse_playback_config(text: &str, map: &mut HashMap<String, u16>) {
for tag_name in ["AudioStreams", "SubtitlesStreams"] {
let mut from = 0;
while let Some((start, end)) = xml::find_element(text, tag_name, from) {
let block = &text[start..end];
if let (Some(stream_id_str), Some(info_id)) = (
xml::text(block, "StreamID"),
xml::text(block, "StreamInfo_ID"),
) {
if let Ok(stream_num) = stream_id_str.parse::<u16>() {
if stream_num != 0 {
map.insert(info_id, stream_num);
}
}
}
from = end;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn info(id: &str, t: StreamLabelType) -> StreamInfo {
StreamInfo {
id: id.into(),
stream_type: t,
language: "eng".into(),
variant: String::new(),
purpose: LabelPurpose::Normal,
qualifier: LabelQualifier::None,
}
}
#[test]
fn fallback_numbers_dense_when_map_empty() {
let infos = vec![
info("a0", StreamLabelType::Audio),
info("a1", StreamLabelType::Audio),
info("s0", StreamLabelType::Subtitle),
];
let nums = assign_stream_numbers(&infos, &HashMap::new());
assert_eq!(nums, vec![1, 2, 1]);
}
#[test]
fn fallback_does_not_collide_with_partial_map() {
let mut map = HashMap::new();
map.insert("a1".to_string(), 1u16);
let infos = vec![
info("a0", StreamLabelType::Audio), info("a1", StreamLabelType::Audio), info("a2", StreamLabelType::Audio), ];
let nums = assign_stream_numbers(&infos, &map);
assert_eq!(nums, vec![2, 1, 3]);
let mut sorted = nums.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), 3, "stream numbers must be unique");
}
#[test]
fn map_fully_drives_numbers_when_complete() {
let mut map = HashMap::new();
map.insert("a0".to_string(), 5u16);
map.insert("a1".to_string(), 9u16);
let infos = vec![
info("a0", StreamLabelType::Audio),
info("a1", StreamLabelType::Audio),
];
assert_eq!(assign_stream_numbers(&infos, &map), vec![5, 9]);
}
#[test]
fn audio_and_subtitle_counters_are_independent() {
let infos = vec![
info("a0", StreamLabelType::Audio),
info("s0", StreamLabelType::Subtitle),
info("a1", StreamLabelType::Audio),
info("s1", StreamLabelType::Subtitle),
];
let nums = assign_stream_numbers(&infos, &HashMap::new());
assert_eq!(nums[0], 1); assert_eq!(nums[1], 1); assert_eq!(nums[2], 2); assert_eq!(nums[3], 2); }
#[test]
fn map_zero_stream_num_is_skipped() {
let mut map = HashMap::new();
map.insert("a0".to_string(), 0u16); let infos = vec![info("a0", StreamLabelType::Audio)];
let nums = assign_stream_numbers(&infos, &map);
assert_eq!(nums[0], 0);
}
#[test]
fn taken_sets_are_per_type_not_global() {
let mut map = HashMap::new();
map.insert("s0".to_string(), 2u16);
let infos = vec![
info("a0", StreamLabelType::Audio), info("s0", StreamLabelType::Subtitle), ];
let nums = assign_stream_numbers(&infos, &map);
assert_eq!(nums[0], 1);
assert_eq!(nums[1], 2);
}
#[test]
fn assign_stream_numbers_saturation_on_overflow() {
let mut map = HashMap::new();
for n in 1u16..=500 {
map.insert(format!("taken_{}", n), n);
}
let mut infos: Vec<StreamInfo> = (1u16..=500)
.map(|n| StreamInfo {
id: format!("taken_{}", n),
stream_type: StreamLabelType::Audio,
language: "eng".into(),
variant: String::new(),
purpose: LabelPurpose::Normal,
qualifier: LabelQualifier::None,
})
.collect();
infos.push(StreamInfo {
id: "unmapped".into(),
stream_type: StreamLabelType::Audio,
language: "eng".into(),
variant: String::new(),
purpose: LabelPurpose::Normal,
qualifier: LabelQualifier::None,
});
let nums = assign_stream_numbers(&infos, &map);
assert_eq!(nums.len(), 501);
assert!(nums[500] > 500);
}
#[test]
fn parse_stream_infos_commentary_case_insensitive() {
let xml = r#"<root>
<AudioStreamInfos>
<ID>a1</ID>
<LangInfoID>eng</LangInfoID>
<Content>commentary</Content>
<Qualifier></Qualifier>
</AudioStreamInfos>
</root>"#;
let infos = parse_stream_infos(xml);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].purpose, LabelPurpose::Commentary);
}
#[test]
fn parse_stream_infos_lang_variant_split() {
let xml = r#"<root>
<AudioStreamInfos>
<ID>a1</ID>
<LangInfoID>por_BP</LangInfoID>
<Content>Normal</Content>
<Qualifier></Qualifier>
</AudioStreamInfos>
</root>"#;
let infos = parse_stream_infos(xml);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].language, "por");
assert_eq!(infos[0].variant, "BP");
}
#[test]
fn parse_stream_infos_qualifier_sdh_case_insensitive() {
let xml = r#"<root>
<SubtitleStreamInfos>
<ID>s1</ID>
<LangInfoID>eng</LangInfoID>
<Content>Normal</Content>
<Qualifier>sdh</Qualifier>
</SubtitleStreamInfos>
</root>"#;
let infos = parse_stream_infos(xml);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].qualifier, LabelQualifier::Sdh);
}
#[test]
fn parse_stream_infos_qualifier_descriptive_service() {
let xml = r#"<root>
<AudioStreamInfos>
<ID>a1</ID>
<LangInfoID>eng</LangInfoID>
<Content>Normal</Content>
<Qualifier>DS</Qualifier>
</AudioStreamInfos>
</root>"#;
let infos = parse_stream_infos(xml);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].qualifier, LabelQualifier::DescriptiveService);
}
#[test]
fn parse_playback_config_zero_stream_id_skipped() {
let xml = r#"<root>
<AudioStreams>
<StreamID>0</StreamID>
<StreamInfo_ID>bad_id</StreamInfo_ID>
</AudioStreams>
<AudioStreams>
<StreamID>2</StreamID>
<StreamInfo_ID>good_id</StreamInfo_ID>
</AudioStreams>
</root>"#;
let mut map = HashMap::new();
parse_playback_config(xml, &mut map);
assert!(!map.contains_key("bad_id"), "zero StreamID must be skipped");
assert_eq!(map.get("good_id").copied(), Some(2));
}
#[test]
fn parse_playback_config_subtitle_streams_parsed() {
let xml = r#"<root>
<SubtitlesStreams>
<StreamID>3</StreamID>
<StreamInfo_ID>sub1</StreamInfo_ID>
</SubtitlesStreams>
</root>"#;
let mut map = HashMap::new();
parse_playback_config(xml, &mut map);
assert_eq!(map.get("sub1").copied(), Some(3));
}
#[test]
fn parse_stream_infos_language_lowercased() {
let xml = r#"<root>
<AudioStreamInfos>
<ID>a1</ID>
<LangInfoID>ENG</LangInfoID>
<Content>Normal</Content>
<Qualifier></Qualifier>
</AudioStreamInfos>
</root>"#;
let infos = parse_stream_infos(xml);
assert_eq!(infos[0].language, "eng");
}
}