Skip to main content

phemios_cd/
discovery.rs

1use super::{Disc, eject};
2use cd_da_reader::CdReader;
3use std::{
4    collections::BTreeMap,
5    sync::{Arc, Mutex},
6};
7/// One discovered optical drive or explicitly selected local image. Device paths remain native and private.
8#[derive(Clone)]
9pub struct Drive {
10    /// Process-local selector accepted by ejection.
11    pub id: String,
12    /// User-visible device number, not an arbitrary filesystem path.
13    pub name: String,
14    /// Confirmed audio-disc selection, if readable.
15    pub disc: Option<Disc>,
16    /// Read failure kept distinct from an empty drive.
17    pub error: Option<String>,
18    /// An explicitly selected CUE/BIN image, closed without a physical eject command.
19    pub image: bool,
20    path: String,
21}
22/// Bounded source registry; optical discovery is confined to the scan operation.
23#[derive(Default)]
24pub struct Library {
25    drives: Vec<Drive>,
26    next: u64,
27    gate: Arc<Mutex<()>>,
28}
29impl Library {
30    fn token(&mut self) -> Result<String, String> {
31        self.next = self
32            .next
33            .checked_add(1)
34            .ok_or("Trop de changements de CD ; redémarrez l’application.")?;
35        Ok(format!("{:016x}", self.next))
36    }
37    /// Last successful explicit discovery, without disk/network I/O.
38    pub fn snapshot(&self) -> &[Drive] {
39        &self.drives
40    }
41    /// Enumerate and inspect at most sixteen optical drives. Existing tokens survive an unchanged TOC.
42    pub fn scan(&mut self) -> Result<&[Drive], String> {
43        let gate = self.gate.clone();
44        let _guard = gate.try_lock().map_err(|_| "Le lecteur CD est occupé.")?;
45        let drives =
46            CdReader::list_drives().map_err(|_| "Détection des lecteurs CD impossible.")?;
47        if drives.len() > 16 {
48            return Err("Trop de lecteurs CD détectés.".into());
49        }
50        let previous: BTreeMap<_, _> = self
51            .drives
52            .iter()
53            .map(|d| (d.path.clone(), d.clone()))
54            .collect();
55        let mut next = Vec::new();
56        for drive in drives {
57            let old = previous.get(&drive.path);
58            let id = match old {
59                Some(d) => d.id.clone(),
60                None => self.token()?,
61            };
62            let probe = CdReader::open(&drive).and_then(|r| r.read_toc().map(|toc| (r, toc)));
63            let mut error = None;
64            let disc = match probe {
65                Ok((reader, toc)) => match Disc::inspect(
66                    &reader,
67                    &toc,
68                    drive.path.clone(),
69                    self.token()?,
70                    gate.clone(),
71                ) {
72                    Ok(disc) => Some(
73                        old.and_then(|d| d.disc.as_ref())
74                            .filter(|d| d.matches(&toc))
75                            .cloned()
76                            .unwrap_or(disc),
77                    ),
78                    Err(reason) => {
79                        error = Some(reason);
80                        None
81                    }
82                },
83                Err(reason) => {
84                    error = Some(format!("CD absent ou inaccessible : {reason}"));
85                    None
86                }
87            };
88            next.push(Drive {
89                id,
90                name: format!("Lecteur CD {}", next.len() + 1),
91                path: drive.path,
92                disc,
93                error,
94                image: false,
95            });
96        }
97        next.extend(self.drives.iter().filter(|d| d.image).cloned());
98        if next.len() > 16 {
99            return Err("Trop de sources CD ouvertes.".into());
100        }
101        self.drives = next;
102        Ok(&self.drives)
103    }
104    /// Open a selected audio CUE/BIN image transactionally, retaining at most one image.
105    pub fn open_image(&mut self, path: &std::path::Path) -> Result<String, String> {
106        if self.drives.iter().filter(|d| !d.image).count() >= 16 {
107            return Err("Trop de sources CD ouvertes.".into());
108        }
109        let id = self.token()?;
110        let disc = super::image::Image::open(path, self.token()?)?;
111        if let Some(old) = self
112            .drives
113            .iter()
114            .find(|d| d.image && d.disc.as_ref().is_some_and(|old| old.same_image(&disc)))
115        {
116            return Ok(old.id.clone());
117        }
118        let name = path
119            .file_name()
120            .and_then(|n| n.to_str())
121            .unwrap_or("CUE/BIN")
122            .chars()
123            .take(90)
124            .collect::<String>();
125        let drive = Drive {
126            id: id.clone(),
127            name,
128            path: disc.path.clone(),
129            disc: Some(disc),
130            error: None,
131            image: true,
132        };
133        self.drives.retain(|d| !d.image);
134        self.drives.push(drive);
135        Ok(id)
136    }
137    /// Reload only the selected image, without enumerating or touching physical readers.
138    pub fn refresh_image(&mut self, id: &str) -> Result<bool, String> {
139        let Some(index) = self.drives.iter().position(|d| d.id == id && d.image) else {
140            return Ok(false);
141        };
142        let path = self.drives[index]
143            .disc
144            .as_ref()
145            .and_then(|disc| disc.image.as_ref())
146            .ok_or("Image CD indisponible.")?
147            .path
148            .clone();
149        let mut disc = super::image::Image::open(&path, self.token()?)?;
150        if let Some(old) = self.drives[index]
151            .disc
152            .as_ref()
153            .filter(|old| old.same_image(&disc))
154        {
155            disc.token = old.token.clone();
156        }
157        self.drives[index].disc = Some(disc);
158        Ok(true)
159    }
160    /// Admit a previously discovered disc token; the webview never supplies a device path.
161    pub fn disc(&self, token: &str) -> Result<Disc, String> {
162        self.drives
163            .iter()
164            .filter_map(|d| d.disc.as_ref())
165            .find(|d| d.token == token)
166            .cloned()
167            .ok_or_else(|| "Le CD a changé. Actualisez la liste des lecteurs.".into())
168    }
169    /// Explicitly eject one previously discovered drive; no source audio is written.
170    pub fn eject(&mut self, id: &str) -> Result<(), String> {
171        if let Some(index) = self.drives.iter().position(|d| d.id == id && d.image) {
172            self.drives.remove(index);
173            return Ok(());
174        }
175        let gate = self.gate.clone();
176        let _guard = super::access::lock(&gate, || true)?;
177        let drive = self
178            .drives
179            .iter_mut()
180            .find(|d| d.id == id)
181            .ok_or("Lecteur CD inconnu.")?;
182        eject::run(&drive.path)?;
183        drive.disc = None;
184        drive.error = None;
185        Ok(())
186    }
187}