1mod criterion;
11mod ctrm;
12mod paramount;
13mod pixelogic;
14pub mod vocab;
15
16use crate::disc::{DiscTitle, Stream};
17use crate::sector::SectorReader;
18use crate::udf::UdfFs;
19
20#[derive(Debug, Clone)]
26#[allow(dead_code)]
27pub struct StreamLabel {
28 pub stream_number: u16,
30 pub stream_type: StreamLabelType,
32 pub language: String,
34 pub name: String,
36 pub purpose: LabelPurpose,
38 pub qualifier: LabelQualifier,
40 pub codec_hint: String,
42 pub variant: String,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum StreamLabelType {
48 Audio,
49 Subtitle,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum LabelPurpose {
54 Normal,
55 Commentary,
56 Descriptive,
57 Score,
58 Ime,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum LabelQualifier {
63 None,
64 Sdh,
65 DescriptiveService,
66 Forced,
67}
68
69type DetectFn = fn(&UdfFs) -> bool;
75type ParseFn = fn(&mut dyn SectorReader, &UdfFs) -> Option<Vec<StreamLabel>>;
76
77const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
78 ("paramount", paramount::detect, paramount::parse),
79 ("criterion", criterion::detect, criterion::parse),
80 ("pixelogic", pixelogic::detect, pixelogic::parse),
81 ("ctrm", ctrm::detect, ctrm::parse),
82 ];
84
85pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle]) {
88 let labels = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extract(reader, udf)))
89 .unwrap_or_default();
90 if labels.is_empty() {
91 return;
92 }
93
94 for title in titles.iter_mut() {
95 let mut audio_idx: u16 = 0;
96 let mut sub_idx: u16 = 0;
97
98 for stream in &mut title.streams {
99 match stream {
100 Stream::Audio(a) => {
101 audio_idx += 1;
102 if let Some(label) = labels.iter().find(|l| {
103 l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
104 }) {
105 a.purpose = label.purpose;
107
108 let mut parts = Vec::new();
111 if !label.variant.is_empty() {
112 parts.push(format!("({})", label.variant));
113 }
114 if !label.codec_hint.is_empty() {
115 parts.push(label.codec_hint.clone());
116 }
117 if !parts.is_empty() {
118 a.label = parts.join(" ");
119 } else if !label.name.is_empty() && label.purpose == LabelPurpose::Normal {
120 a.label = label.name.clone();
124 }
125 }
126 }
127 Stream::Subtitle(s) => {
128 sub_idx += 1;
129 if let Some(label) = labels.iter().find(|l| {
130 l.stream_type == StreamLabelType::Subtitle && l.stream_number == sub_idx
131 }) {
132 s.qualifier = label.qualifier;
133 if label.qualifier == LabelQualifier::Forced {
134 s.forced = true;
135 }
136 }
137 }
138 _ => {}
139 }
140 }
141 }
142}
143
144pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
148 use crate::disc::Stream;
149
150 for title in titles.iter_mut() {
151 for stream in &mut title.streams {
152 match stream {
153 Stream::Audio(a) if a.label.is_empty() => {
154 a.label = generate_audio_label(&a.codec, &a.channels, a.secondary);
155 }
156 Stream::Video(v) if v.label.is_empty() => {
157 v.label =
158 generate_video_label(&v.codec, v.resolution.pixels(), &v.hdr, v.secondary);
159 }
160 Stream::Subtitle(s) if s.forced => {
161 }
164 _ => {}
165 }
166 }
167 }
168}
169
170fn generate_video_label(
171 codec: &crate::disc::Codec,
172 pixels: (u32, u32),
173 hdr: &crate::disc::HdrFormat,
174 secondary: bool,
175) -> String {
176 use crate::disc::HdrFormat;
177
178 if secondary {
179 return match hdr {
183 HdrFormat::DolbyVision => "Dolby Vision EL".to_string(),
184 _ => String::new(),
185 };
186 }
187
188 let mut parts = Vec::new();
189
190 parts.push(codec.name().to_string());
192
193 let (w, h) = pixels;
195 let res = if w >= 7680 {
196 "8K"
197 } else if w >= 3840 {
198 "4K"
199 } else if w >= 1920 {
200 "1080p"
201 } else if w >= 1280 {
202 "720p"
203 } else if h >= 576 {
204 "576p"
205 } else if h >= 480 {
206 "480p"
207 } else {
208 ""
209 };
210 if !res.is_empty() {
211 parts.push(res.into());
212 }
213
214 match hdr {
216 HdrFormat::Sdr => {}
217 _ => parts.push(hdr.name().to_string()),
218 }
219
220 parts.join(" ")
221}
222
223fn generate_audio_label(
224 codec: &crate::disc::Codec,
225 channels: &crate::disc::AudioChannels,
226 _secondary: bool,
227) -> String {
228 use crate::disc::{AudioChannels, Codec};
229
230 let codec_name = match codec {
233 Codec::TrueHd => "Dolby TrueHD",
234 Codec::Ac3 => "Dolby Digital",
235 Codec::Ac3Plus => "Dolby Digital Plus",
236 Codec::DtsHdMa => "DTS-HD Master Audio",
237 Codec::DtsHdHr => "DTS-HD High Resolution",
238 Codec::Dts => "DTS",
239 Codec::Lpcm => "LPCM",
240 Codec::Aac => "AAC",
241 Codec::Mp2 => "MPEG Audio",
242 Codec::Mp3 => "MP3",
243 Codec::Flac => "FLAC",
244 Codec::Opus => "Opus",
245 _ => return String::new(),
246 };
247
248 let channel_str = match channels {
250 AudioChannels::Mono => "1.0",
251 AudioChannels::Stereo => "2.0",
252 AudioChannels::Stereo21 => "2.1",
253 AudioChannels::Quad => "4.0",
254 AudioChannels::Surround50 => "5.0",
255 AudioChannels::Surround51 => "5.1",
256 AudioChannels::Surround61 => "6.1",
257 AudioChannels::Surround71 => "7.1",
258 AudioChannels::Unknown => "",
259 };
260
261 if channel_str.is_empty() {
264 codec_name.to_string()
265 } else {
266 format!("{} {}", codec_name, channel_str)
267 }
268}
269
270fn extract(reader: &mut dyn SectorReader, udf: &UdfFs) -> Vec<StreamLabel> {
271 for (_name, detect, parse) in PARSERS {
272 if detect(udf) {
273 if let Some(labels) = parse(reader, udf) {
274 return labels;
275 }
276 }
277 }
278 Vec::new()
279}
280
281pub(crate) fn jar_file_exists(udf: &UdfFs, filename: &str) -> bool {
285 find_jar_file(udf, filename).is_some()
286}
287
288pub(crate) fn find_jar_file(udf: &UdfFs, filename: &str) -> Option<String> {
290 let jar_dir = udf.find_dir("/BDMV/JAR")?;
291 for entry in &jar_dir.entries {
292 if entry.is_dir {
293 let path = format!("/BDMV/JAR/{}/{}", entry.name, filename);
294 for child in &entry.entries {
296 if !child.is_dir && child.name.eq_ignore_ascii_case(filename) {
297 return Some(path);
298 }
299 }
300 }
301 }
302 None
303}
304
305pub(crate) fn read_jar_file(
307 reader: &mut dyn SectorReader,
308 udf: &UdfFs,
309 filename: &str,
310) -> Option<Vec<u8>> {
311 let path = find_jar_file(udf, filename)?;
312 udf.read_file(reader, &path).ok().filter(|d| !d.is_empty())
313}