cd_da_reader/
discovery.rs1use crate::{CdReader, CdReaderError};
2
3#[derive(Debug, Clone)]
6pub struct DriveInfo {
7 pub path: String,
10 pub display_name: Option<String>,
12 pub has_audio_cd: bool,
14}
15
16impl CdReader {
17 #[cfg(target_os = "macos")]
24 pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
25 Err(CdReaderError::Io(std::io::Error::other(
26 "list_drives is not reliable on macOS due to remount/re-enumeration; use open_default instead or open a disk directly using CDReader::open",
27 )))
28 }
29
30 #[cfg(not(target_os = "macos"))]
35 pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
36 let mut paths = {
37 #[cfg(target_os = "windows")]
38 {
39 crate::windows::list_drive_paths().map_err(CdReaderError::Io)?
40 }
41
42 #[cfg(target_os = "linux")]
43 {
44 crate::linux::list_drive_paths().map_err(CdReaderError::Io)?
45 }
46
47 #[cfg(not(any(target_os = "windows", target_os = "linux")))]
48 {
49 compile_error!("Unsupported platform")
50 }
51 };
52
53 paths.sort();
54 paths.dedup();
55
56 let mut drives = Vec::with_capacity(paths.len());
57 for path in paths {
58 let has_audio_cd = match Self::open(&path) {
59 Ok(reader) => match reader.read_toc() {
60 Ok(toc) => toc.tracks.iter().any(|track| track.is_audio),
61 Err(_) => false,
62 },
63 Err(_) => false,
64 };
65
66 drives.push(DriveInfo {
67 display_name: Some(path.clone()),
68 path,
69 has_audio_cd,
70 });
71 }
72
73 Ok(drives)
74 }
75
76 #[cfg(target_os = "macos")]
82 pub fn open_default() -> Result<Self, CdReaderError> {
83 let mut paths = crate::macos::list_drive_paths().map_err(CdReaderError::Io)?;
84 paths.sort();
85 paths.dedup();
86
87 for path in paths {
88 let Ok(reader) = Self::open(&path) else {
89 continue;
90 };
91 let Ok(toc) = reader.read_toc() else {
92 continue;
93 };
94 if toc.tracks.iter().any(|track| track.is_audio) {
95 return Ok(reader);
96 }
97 }
98
99 Err(CdReaderError::Io(std::io::Error::new(
100 std::io::ErrorKind::NotFound,
101 "no usable audio CD drive found",
102 )))
103 }
104
105 #[cfg(not(target_os = "macos"))]
110 pub fn open_default() -> Result<Self, CdReaderError> {
111 let drives = Self::list_drives()?;
112 let chosen = pick_default_drive_path(&drives).ok_or_else(|| {
113 CdReaderError::Io(std::io::Error::new(
114 std::io::ErrorKind::NotFound,
115 "no usable audio CD drive found",
116 ))
117 })?;
118
119 Self::open(chosen).map_err(CdReaderError::Io)
120 }
121}
122
123#[cfg(any(test, not(target_os = "macos")))]
124fn pick_default_drive_path(drives: &[DriveInfo]) -> Option<&str> {
125 drives
126 .iter()
127 .find(|drive| drive.has_audio_cd)
128 .map(|drive| drive.path.as_str())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{DriveInfo, pick_default_drive_path};
134
135 #[test]
136 fn chooses_first_audio_drive() {
137 let drives = vec![
138 DriveInfo {
139 path: "disk10".to_string(),
140 display_name: None,
141 has_audio_cd: false,
142 },
143 DriveInfo {
144 path: "disk11".to_string(),
145 display_name: None,
146 has_audio_cd: true,
147 },
148 DriveInfo {
149 path: "disk12".to_string(),
150 display_name: None,
151 has_audio_cd: true,
152 },
153 ];
154
155 assert_eq!(pick_default_drive_path(&drives), Some("disk11"));
156 }
157
158 #[test]
159 fn returns_none_when_no_audio_drive() {
160 let drives = vec![DriveInfo {
161 path: "/dev/sr0".to_string(),
162 display_name: None,
163 has_audio_cd: false,
164 }];
165
166 assert_eq!(pick_default_drive_path(&drives), None);
167 }
168}