1#![warn(missing_docs)]
3mod access;
4mod ape;
5mod cue;
6mod decoded;
7mod discovery;
8mod eject;
9mod image;
10mod process;
11mod stream;
12use cd_da_reader::{AudioSectorReader, CdReader, Toc};
13pub use discovery::{Drive, Library};
14use std::sync::{Arc, Mutex};
15pub use stream::{Chunk, TrackReader};
16
17pub const SAMPLE_RATE: u32 = 44_100;
19pub const FRAMES_PER_SECTOR: u64 = 588;
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Track {
24 pub number: u8,
26 pub frames: u64,
28 start: u32,
29}
30impl Track {
31 pub fn duration_ms(&self) -> u64 {
33 self.frames * 1000 / u64::from(SAMPLE_RATE)
34 }
35}
36#[derive(Clone)]
38pub struct Disc {
39 pub token: String,
41 pub tracks: Vec<Track>,
43 pub(crate) path: String,
44 layout: Vec<(u8, u32, bool)>,
45 leadout: u32,
46 gate: Arc<Mutex<()>>,
47 image: Option<Arc<image::Image>>,
48}
49impl Disc {
50 fn inspect(
51 reader: &impl AudioSectorReader,
52 toc: &Toc,
53 path: String,
54 token: String,
55 gate: Arc<Mutex<()>>,
56 ) -> Result<Self, String> {
57 if toc.tracks.is_empty()
58 || toc.tracks.len() > 99
59 || toc.leadout_lba > 540_000
60 || toc.first_track == 0
61 || toc.last_track > 99
62 || toc.first_track > toc.last_track
63 || toc
64 .tracks
65 .first()
66 .is_none_or(|t| t.number != toc.first_track)
67 || toc.tracks.last().is_none_or(|t| t.number != toc.last_track)
68 || toc.tracks.iter().any(|t| t.start_lba >= toc.leadout_lba)
69 || toc
70 .tracks
71 .windows(2)
72 .any(|w| w[0].number >= w[1].number || w[0].start_lba >= w[1].start_lba)
73 {
74 return Err("Table des pistes du CD invalide.".into());
75 }
76 let mut tracks = Vec::new();
77 for track in toc.tracks.iter().filter(|t| t.is_audio) {
78 let stream = cd_da_reader::open_track_stream(reader, toc, track.number)
80 .map_err(|_| "Limites des pistes du CD invalides.")?;
81 let frames = u64::from(stream.total_sectors()) * FRAMES_PER_SECTOR;
82 if frames == 0 {
83 return Err("Une piste du CD est vide.".into());
84 }
85 tracks.push(Track {
86 number: track.number,
87 frames,
88 start: track.start_lba,
89 });
90 }
91 if tracks.is_empty() {
92 return Err("Aucun CD audio dans ce lecteur.".into());
93 }
94 Ok(Self {
95 token,
96 tracks,
97 path,
98 layout: toc
99 .tracks
100 .iter()
101 .map(|t| (t.number, t.start_lba, t.is_audio))
102 .collect(),
103 leadout: toc.leadout_lba,
104 gate,
105 image: None,
106 })
107 }
108 fn matches(&self, toc: &Toc) -> bool {
109 self.leadout == toc.leadout_lba
110 && self.layout.iter().copied().eq(toc
111 .tracks
112 .iter()
113 .map(|t| (t.number, t.start_lba, t.is_audio)))
114 }
115 pub(crate) fn same_image(&self, other: &Self) -> bool {
116 self.tracks == other.tracks
117 && self
118 .image
119 .as_ref()
120 .zip(other.image.as_ref())
121 .is_some_and(|(a, b)| a.same(b))
122 }
123 pub fn title(&self) -> Option<&str> {
125 self.image.as_ref()?.title.as_deref()
126 }
127 pub fn performer(&self) -> Option<&str> {
129 self.image.as_ref()?.performer.as_deref()
130 }
131 pub fn track_names(&self, number: u8) -> (Option<&str>, Option<&str>) {
133 self.image
134 .as_ref()
135 .and_then(|image| image.names.get(&number))
136 .map(|(title, performer)| (title.as_deref(), performer.as_deref()))
137 .unwrap_or_default()
138 }
139 pub fn reference(&self, number: u8) -> String {
141 format!("track:cd/{}/{number}", self.token)
142 }
143 pub fn track(&self, reference: &str) -> Result<&Track, String> {
145 self.tracks
146 .iter()
147 .find(|t| self.reference(t.number) == reference)
148 .ok_or_else(|| "La sélection du CD a changé.".into())
149 }
150 pub fn with_stream<T>(
153 &self,
154 number: u8,
155 current: impl Fn() -> bool,
156 work: impl FnOnce(&mut TrackReader<'_>) -> Result<T, String>,
157 ) -> Result<T, String> {
158 let _guard = access::lock(&self.gate, current)?;
159 let track = self
160 .tracks
161 .iter()
162 .find(|t| t.number == number)
163 .ok_or("Piste absente du CD.")?;
164 if let Some(image) = &self.image {
165 let reader = image.reader();
166 return work(&mut TrackReader::new(&reader, track));
167 }
168 let reader = CdReader::open_path(&self.path)
169 .map_err(|_| "Le lecteur CD est absent, occupé ou inaccessible.")?;
170 let toc = reader
171 .read_toc()
172 .map_err(|_| "Le CD a été retiré ou ne peut pas être lu.")?;
173 if !self.matches(&toc) {
174 return Err("Le CD a changé. Actualisez la liste des lecteurs.".into());
175 }
176 let track = self
177 .tracks
178 .iter()
179 .find(|t| t.number == number)
180 .ok_or("Piste absente du CD.")?;
181 work(&mut TrackReader::new(&stream::NativeReader(&reader), track))
182 }
183}
184
185#[cfg(test)]
186#[path = "disc_tests.rs"]
187mod tests;