Audit pass against the CLAUDE.md "no English text in library code" rule.
Found 9 call sites that violated the contract by stuffing English into
io::Error::new(kind, "…") or by abusing Error::DeviceNotFound { path }
as a free-form description field. Each is now a typed Error variant.
New variants and codes: ScsiInterfaceUnavailable (E1004), DeviceLocked
(E1005), IoKitPluginFailed (E1006), UnsupportedPlatform (E2003),
PlatformNotImplemented (E2004), MapfileInvalid (E6011), DiscUrlNotDirect
(E9009).
labels::apply() previously pushed Commentary/Descriptive/Score/IME and
" (Secondary)" English literals into AudioStream.label, leaking into
MKV titles + autorip UI. AudioStream now exposes structured `purpose:
LabelPurpose`, SubtitleStream `qualifier: LabelQualifier`. Callers
translate to localized text. label keeps codec-formatting only.
API hygiene: 11 mux/* modules dropped from `pub` to `pub(crate)` —
their *types* are still re-exported from lib.rs, but the modules were
leaking low-level EBML/TS/network primitives. Stream trait gets a real
rustdoc explaining read-vs-write split. lib.rs grouped re-exports into
documented sections. ScanOptions::with_keydb() removed (one-method-per-
action rule); use struct literal.
Dead-code sweep: removed lookahead.rs (orphan, never declared as mod),
tsreader.rs (TsDemuxReader unused), ebml::{write_int,read_vint,SEEK_*},
ts::{scan_first/last_pts,scan_duration,SCAN_HEAD/TAIL_SIZE,take/set_
remainder}, MkvMuxer codec_private_slots/filled fields and
fill_codec_private method (deferred-codecPrivate path never used since
the v0.10 PES rewrite). cargo clippy --all-targets -D warnings clean.
Tests: new error::tests for variant codes + Display "no English" guard +
io::ErrorKind mapping. 233 lib tests, all green (was 230).
Breaking: ScanOptions::with_keydb removed; mux/* modules pub(crate);
AudioStream and SubtitleStream gained required fields; UnsupportedDrive
{ product_revision: "Renesas not yet implemented" } no longer produced
(use PlatformNotImplemented).
134 lines
4.9 KiB
Rust
134 lines
4.9 KiB
Rust
//! DVD title scanning — IFO parsing, stream mapping, VOB extent building.
|
|
|
|
use super::*;
|
|
use crate::ifo;
|
|
use crate::sector::SectorReader;
|
|
use crate::udf;
|
|
|
|
impl Disc {
|
|
/// Scan DVD titles from IFO files (VIDEO_TS.IFO + VTS_XX_0.IFO).
|
|
pub(super) fn scan_dvd_titles(
|
|
reader: &mut dyn SectorReader,
|
|
udf_fs: &udf::UdfFs,
|
|
) -> Vec<DiscTitle> {
|
|
let dvd_info = match ifo::parse_vmg(reader, udf_fs) {
|
|
Ok(info) => info,
|
|
Err(_) => return Vec::new(),
|
|
};
|
|
|
|
let mut titles = Vec::new();
|
|
let mut title_number: u16 = 0;
|
|
|
|
for ts in &dvd_info.title_sets {
|
|
let video_stream = Stream::Video(VideoStream {
|
|
pid: 0xE0, // DVD video PID (standard MPEG PS video stream)
|
|
codec: ts.video.codec,
|
|
resolution: ts.video.resolution,
|
|
frame_rate: match ts.video.standard.as_str() {
|
|
"PAL" => FrameRate::F25,
|
|
_ => FrameRate::F29_97,
|
|
},
|
|
hdr: HdrFormat::Sdr,
|
|
color_space: ColorSpace::Bt709,
|
|
secondary: false,
|
|
label: String::new(),
|
|
});
|
|
|
|
// Map DvdAudioAttr to Stream::Audio
|
|
let audio_streams: Vec<Stream> = ts
|
|
.audio_streams
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, a)| {
|
|
let codec = a.codec;
|
|
Stream::Audio(AudioStream {
|
|
pid: 0xBD00 + i as u16, // DVD private stream 1 sub-IDs
|
|
codec,
|
|
channels: AudioChannels::from_count(a.channels),
|
|
language: a.language.clone(),
|
|
sample_rate: SampleRate::from_hz(a.sample_rate),
|
|
secondary: false,
|
|
purpose: crate::disc::LabelPurpose::Normal,
|
|
label: String::new(),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for dvd_title in &ts.titles {
|
|
title_number += 1;
|
|
|
|
// Build extents from cell sector ranges (absolute = vob_start + cell offset)
|
|
let extents: Vec<Extent> = dvd_title
|
|
.cells
|
|
.iter()
|
|
.map(|cell| {
|
|
let start = ts.vob_start_sector.saturating_add(cell.first_sector);
|
|
let count = cell
|
|
.last_sector
|
|
.saturating_sub(cell.first_sector)
|
|
.saturating_add(1);
|
|
Extent {
|
|
start_lba: start,
|
|
sector_count: count,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let size_bytes: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum();
|
|
|
|
// Build pre-formatted palette codec_data for VobSub subtitle streams
|
|
let codec_data = dvd_title
|
|
.palette
|
|
.as_ref()
|
|
.map(|pal| crate::mux::codec::dvdsub::format_palette(pal));
|
|
|
|
// Map DvdSubtitleAttr to Stream::Subtitle
|
|
let subtitle_streams: Vec<Stream> = ts
|
|
.subtitle_streams
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, s)| {
|
|
Stream::Subtitle(SubtitleStream {
|
|
pid: 0x20 + i as u16, // DVD sub-stream IDs 0x20-0x3F
|
|
codec: Codec::DvdSub,
|
|
language: s.language.clone(),
|
|
forced: false,
|
|
qualifier: crate::disc::LabelQualifier::None,
|
|
codec_data: codec_data.clone(),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let mut streams = vec![video_stream.clone()];
|
|
streams.extend(audio_streams.iter().cloned());
|
|
streams.extend(subtitle_streams);
|
|
|
|
let chapters: Vec<Chapter> = dvd_title
|
|
.chapter_times
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &t)| Chapter {
|
|
time_secs: t,
|
|
name: format!("Chapter {}", i + 1),
|
|
})
|
|
.collect();
|
|
|
|
titles.push(DiscTitle {
|
|
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
|
playlist_id: title_number,
|
|
duration_secs: dvd_title.duration_secs,
|
|
size_bytes,
|
|
clips: Vec::new(),
|
|
streams,
|
|
chapters,
|
|
extents,
|
|
content_format: ContentFormat::MpegPs,
|
|
codec_privates: Vec::new(),
|
|
});
|
|
}
|
|
}
|
|
|
|
titles
|
|
}
|
|
}
|