Files
libfreemkv/src/labels/mod.rs
T
MattJackson 8e7778c572 Simplify labels API: one call, labels on streams or nothing
labels::apply(session, udf, titles) does everything internally.
Disc::scan() is one line: crate::labels::apply().
No intermediate variables, no fallback logic in disc.rs.

App reads disc.titles[].streams — labels already applied if
disc had config files, empty otherwise. MPLS data always there.
2026-04-07 18:46:29 -07:00

124 lines
4.3 KiB
Rust

//! Stream label extraction from BD-J disc files.
//!
//! Searches the disc UDF filesystem for known config files that contain
//! stream labels (language, purpose, codec, forced flags). Four formats
//! supported, tried in order. If found, labels are applied directly
//! to the title streams. If not found, streams keep MPLS data as-is.
mod language_streams;
mod menu_base;
mod stream_properties;
mod bluray_project;
use crate::drive::DriveSession;
use crate::udf::UdfFs;
use crate::disc::{Title, Stream};
/// A stream label extracted from disc config files.
#[derive(Debug, Clone)]
pub struct StreamLabel {
/// STN index (1-based)
pub stream_number: u16,
/// Audio or Subtitle
pub stream_type: StreamLabelType,
/// ISO 639-2 language code
pub language: String,
/// Display name (e.g. "AudioEnglishDolby", "English Dolby Atmos")
pub name: String,
/// Stream purpose
pub purpose: LabelPurpose,
/// Additional qualifier
pub qualifier: LabelQualifier,
/// Codec hint from config (e.g. "MLP", "AC3", "atmos")
pub codec_hint: String,
/// Regional variant (e.g. "US", "UK", "CF", "CS")
pub region: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StreamLabelType {
Audio,
Subtitle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelPurpose {
Normal,
Commentary,
Descriptive,
Score,
Ime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelQualifier {
None,
Sdh,
DescriptiveService,
Forced,
}
/// Search disc for config files, extract labels, apply to streams.
/// If no config files found, streams are left unchanged.
pub fn apply(session: &mut DriveSession, udf: &UdfFs, titles: &mut [Title]) {
let labels = extract(session, udf);
if labels.is_empty() { return; }
for title in titles.iter_mut() {
let mut audio_idx: u16 = 0;
let mut sub_idx: u16 = 0;
for stream in &mut title.streams {
match stream {
Stream::Audio(a) => {
audio_idx += 1;
if let Some(label) = labels.iter().find(|l|
l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
) {
let mut parts = Vec::new();
match label.purpose {
LabelPurpose::Commentary => parts.push("Commentary".to_string()),
LabelPurpose::Descriptive => parts.push("Descriptive Audio".to_string()),
LabelPurpose::Score => parts.push("Score".to_string()),
LabelPurpose::Ime => parts.push("IME".to_string()),
LabelPurpose::Normal => {}
}
if !label.region.is_empty() {
parts.push(format!("({})", label.region));
}
if !label.codec_hint.is_empty()
&& !matches!(label.codec_hint.as_str(), "MLP" | "AC3" | "DTS")
{
parts.push(label.codec_hint.clone());
}
if !parts.is_empty() {
a.label = parts.join(" ");
} else if !label.name.is_empty() {
a.label = label.name.clone();
}
}
}
Stream::Subtitle(s) => {
sub_idx += 1;
if let Some(label) = labels.iter().find(|l|
l.stream_type == StreamLabelType::Subtitle && l.stream_number == sub_idx
) {
if label.qualifier == LabelQualifier::Forced {
s.forced = true;
}
}
}
_ => {}
}
}
}
}
fn extract(session: &mut DriveSession, udf: &UdfFs) -> Vec<StreamLabel> {
if let Some(labels) = language_streams::parse(session, udf) { return labels; }
if let Some(labels) = menu_base::parse(session, udf) { return labels; }
if let Some(labels) = stream_properties::parse(session, udf) { return labels; }
if let Some(labels) = bluray_project::parse(session, udf) { return labels; }
Vec::new()
}