- Doc comments on DriveSession, find_drives, all Error variants, Result type - 24 format! strings inlined (clippy pedantic) - 25 long hex literals with separators (0xFFFFFFFF → 0xFFFF_FFFF) - README install example updated to 0.8
183 lines
5.5 KiB
Rust
183 lines
5.5 KiB
Rust
//! Criterion Collection — `streamproperties.xml` + `playbackconfig.xml`
|
|
//!
|
|
//! Clean structured XML with Content/Qualifier per stream and
|
|
//! stream number mapping via playbackconfig.
|
|
|
|
use super::{LabelPurpose, LabelQualifier, StreamLabel, StreamLabelType};
|
|
use crate::sector::SectorReader;
|
|
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 SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
|
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;
|
|
}
|
|
|
|
// Stream number mapping from playbackconfig.xml
|
|
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 mut labels = Vec::new();
|
|
let mut audio_idx: u16 = 1;
|
|
let mut sub_idx: u16 = 1;
|
|
|
|
for info in &stream_infos {
|
|
let stream_num =
|
|
stream_map
|
|
.get(&info.id)
|
|
.copied()
|
|
.unwrap_or_else(|| match info.stream_type {
|
|
StreamLabelType::Audio => {
|
|
let n = audio_idx;
|
|
audio_idx += 1;
|
|
n
|
|
}
|
|
StreamLabelType::Subtitle => {
|
|
let n = sub_idx;
|
|
sub_idx += 1;
|
|
n
|
|
}
|
|
});
|
|
|
|
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(labels)
|
|
}
|
|
|
|
struct StreamInfo {
|
|
id: String,
|
|
stream_type: StreamLabelType,
|
|
language: String,
|
|
variant: String,
|
|
purpose: LabelPurpose,
|
|
qualifier: LabelQualifier,
|
|
}
|
|
|
|
fn parse_stream_infos(xml: &str) -> Vec<StreamInfo> {
|
|
let mut infos = Vec::new();
|
|
let mut pos = 0;
|
|
|
|
while pos < xml.len() {
|
|
let (tag, stream_type) = if let Some(p) = xml[pos..].find("<AudioStreamInfos>") {
|
|
(p + pos, StreamLabelType::Audio)
|
|
} else if let Some(p) = xml[pos..].find("<SubtitleStreamInfos>") {
|
|
(p + pos, StreamLabelType::Subtitle)
|
|
} else {
|
|
break;
|
|
};
|
|
|
|
let end_tag = match stream_type {
|
|
StreamLabelType::Audio => "</AudioStreamInfos>",
|
|
StreamLabelType::Subtitle => "</SubtitleStreamInfos>",
|
|
};
|
|
|
|
let block_end = match xml[tag..].find(end_tag) {
|
|
Some(p) => tag + p + end_tag.len(),
|
|
None => break,
|
|
};
|
|
|
|
let block = &xml[tag..block_end];
|
|
let id = extract_tag(block, "ID").unwrap_or_default();
|
|
let lang_id = extract_tag(block, "LangInfoID").unwrap_or_default();
|
|
let content = extract_tag(block, "Content").unwrap_or_default();
|
|
let qualifier_str = extract_tag(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 = match content.as_str() {
|
|
"COMMENTARY" => LabelPurpose::Commentary,
|
|
_ => LabelPurpose::Normal,
|
|
};
|
|
|
|
let qualifier = match qualifier_str.as_str() {
|
|
"SDH" => LabelQualifier::Sdh,
|
|
"DS" => LabelQualifier::DescriptiveService,
|
|
_ => LabelQualifier::None,
|
|
};
|
|
|
|
infos.push(StreamInfo {
|
|
id,
|
|
stream_type,
|
|
language,
|
|
variant,
|
|
purpose,
|
|
qualifier,
|
|
});
|
|
pos = block_end;
|
|
}
|
|
infos
|
|
}
|
|
|
|
fn parse_playback_config(xml: &str, map: &mut HashMap<String, u16>) {
|
|
let mut pos = 0;
|
|
while pos < xml.len() {
|
|
let tag_start = if let Some(p) = xml[pos..].find("<AudioStreams>") {
|
|
Some(p + pos)
|
|
} else {
|
|
xml[pos..].find("<SubtitlesStreams>").map(|p| p + pos)
|
|
};
|
|
|
|
let tag_start = match tag_start {
|
|
Some(p) => p,
|
|
None => break,
|
|
};
|
|
|
|
let block_end = xml[tag_start..]
|
|
.find("</AudioStreams>")
|
|
.or_else(|| xml[tag_start..].find("</SubtitlesStreams>"))
|
|
.map(|p| tag_start + p + 20)
|
|
.unwrap_or(xml.len());
|
|
|
|
let block = &xml[tag_start..block_end];
|
|
|
|
if let (Some(stream_id_str), Some(info_id)) = (
|
|
extract_tag(block, "StreamID"),
|
|
extract_tag(block, "StreamInfo_ID"),
|
|
) {
|
|
if let Ok(stream_num) = stream_id_str.parse::<u16>() {
|
|
map.insert(info_id, stream_num);
|
|
}
|
|
}
|
|
|
|
pos = block_end;
|
|
}
|
|
}
|
|
|
|
fn extract_tag(xml: &str, tag: &str) -> Option<String> {
|
|
let open = format!("<{tag}>");
|
|
let close = format!("</{tag}>");
|
|
let start = xml.find(&open)? + open.len();
|
|
let end = xml[start..].find(&close)? + start;
|
|
Some(xml[start..end].trim().to_string())
|
|
}
|