qbz-rip.log is deliberately NOT an EAC log. An EAC or XLD log is trusted for what it PROVES — test-and-copy, a drive read-offset correction, an AccurateRip cross-check against thousands of other rips of the same disc — and QBZ does none of the three. A file that imitated the format without the method would be worse than no file: it invites a claim this ripper cannot back, and the people who read rip logs are exactly the people who check. So the header says what it is and is not in the first block, not in small print. What remains is true and useful: which disc (DiscID and TOC fingerprint, both recomputable by anyone holding it), which drive, and per track the audio md5 that already lives in the file's own STREAMINFO — verifiable with flac -t without QBZ. Verified end to end: metaflac --show-md5sum prints exactly what the log claims, and flac -t passes on every track. "0 read errors" is a real promise and is stated as one: an unreadable sector ABORTS the rip rather than being written as silence, which is stronger than a burst-mode EAC rip. Where the OS did the reading there is no drive to name, and the log says that instead of printing a volume as though it were hardware. A partial rip declares itself partial: three tracks with no such line read as the whole album. cover.jpg keeps the source's EXTENSION — renaming a PNG to .jpg is how a picture stops opening. Neither sidecar can fail the rip: the audio is the deliverable, and a missing extra is a blemish on a folder of correct files. Five tests, including that the log states all three things it does not do, that the logged digest IS the file's own, and the calendar arithmetic that is the only thing here that could be quietly wrong for years. cc
57 lines
2.0 KiB
Rust
57 lines
2.0 KiB
Rust
//! Rip ONE track off the disc in the drive, tagged, into a directory.
|
|
//! `cargo run -p qbz-rip --example rip_one -- <track#> <destdir>`
|
|
fn main() {
|
|
let mut a = std::env::args().skip(1);
|
|
let want: u32 = a.next().expect("track number").parse().unwrap();
|
|
let dest = std::path::PathBuf::from(a.next().expect("destination"));
|
|
|
|
let dev = qbz_disc::list_devices().into_iter().next().expect("a drive");
|
|
let toc = qbz_disc::read_toc(&dev).expect("toc");
|
|
let starts: Vec<u32> = toc.audio_tracks().map(|t| t.start_lsn).collect();
|
|
let id = qbz_disc::discid::disc_id(&starts, toc.leadout_lsn).expect("disc id");
|
|
println!("disc id {id}");
|
|
|
|
let t = toc
|
|
.audio_tracks()
|
|
.find(|t| t.number as u32 == want)
|
|
.expect("that track");
|
|
println!("track {} — {} sectors, {}s", t.number, t.sectors, t.duration_secs());
|
|
|
|
let plan = qbz_rip::RipPlan {
|
|
destination: dest,
|
|
album: "Fear Inoculum".into(),
|
|
album_artist: "Tool".into(),
|
|
year: Some(2019),
|
|
tracks: vec![qbz_rip::RipTrack {
|
|
number: t.number as u32,
|
|
title: "Chocolate Chip Trip".into(),
|
|
artist: "Tool".into(),
|
|
source: qbz_rip::RipSource::Cd {
|
|
device: dev.clone(),
|
|
start_lsn: t.start_lsn,
|
|
sectors: t.sectors,
|
|
},
|
|
}],
|
|
disc_id: Some(id.clone()),
|
|
toc_fingerprint: Some(toc.fingerprint()),
|
|
disc_track_count: toc.audio_tracks().count(),
|
|
cover: None,
|
|
};
|
|
let started = std::time::Instant::now();
|
|
let mut last = -1i32;
|
|
let out = qbz_rip::rip(&plan, |p| {
|
|
let pct = (p.fraction * 100.0) as i32;
|
|
if pct / 10 != last / 10 {
|
|
println!(" {pct}%");
|
|
last = pct;
|
|
}
|
|
true
|
|
})
|
|
.expect("rip");
|
|
println!("wrote {:?} in {:.1}s", out, started.elapsed().as_secs_f32());
|
|
for p in &out {
|
|
let md = std::fs::metadata(p).unwrap();
|
|
println!(" {} bytes", md.len());
|
|
}
|
|
}
|