Skip to main content

cd_da_reader/
discovery.rs

1use crate::{CdReader, CdReaderError};
2
3/// Information about an optical drive discovered by [`CdReader::list_drives`].
4///
5/// Audio-CD detection is best-effort. If a drive cannot be opened or its TOC
6/// cannot be read, it is still returned with [`DriveInfo::has_audio_cd`] set to
7/// `false`. If you already know the platform-specific device path, you can
8/// bypass discovery with [`CdReader::open_path`].
9#[derive(Debug, Clone)]
10pub struct DriveInfo {
11    /// Path to the drive, which can be something like 'disk6' on macOS,
12    /// '\\.\E:' on Windows, and '/dev/sr0' on Linux
13    pub path: String,
14    /// Whether the current disc appears to contain at least one audio track.
15    pub has_audio_cd: bool,
16}
17
18impl CdReader {
19    /// Enumerate candidate optical drives and probe whether they currently have an audio CD.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`CdReaderError::Io`] if platform drive enumeration fails.
24    /// Errors while probing an individual drive are represented by
25    /// [`DriveInfo::has_audio_cd`] being `false` instead.
26    pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
27        let mut paths = crate::platform::list_drive_paths()?;
28        paths.sort();
29        paths.dedup();
30
31        let mut drives = Vec::with_capacity(paths.len());
32        for path in paths {
33            let has_audio_cd = match Self::open_path(&path) {
34                Ok(reader) => match reader.read_toc() {
35                    Ok(toc) => toc.tracks.iter().any(|track| track.is_audio),
36                    Err(_) => false,
37                },
38                Err(_) => false,
39            };
40
41            drives.push(DriveInfo { path, has_audio_cd });
42        }
43
44        Ok(drives)
45    }
46
47    /// Open the first discovered drive that currently has an audio CD.
48    ///
49    /// # Errors
50    ///
51    /// - Returns [`CdReaderError::NoUsableDrive`] if no discovered drive has a
52    ///   readable audio CD.
53    /// - Returns [`CdReaderError::Io`] if drive enumeration or opening the
54    ///   selected drive fails.
55    pub fn open_default() -> Result<Self, CdReaderError> {
56        let drives = Self::list_drives()?;
57        let chosen = pick_default_drive(&drives).ok_or(CdReaderError::NoUsableDrive)?;
58
59        Self::open(chosen)
60    }
61}
62
63fn pick_default_drive(drives: &[DriveInfo]) -> Option<&DriveInfo> {
64    drives.iter().find(|drive| drive.has_audio_cd)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::{DriveInfo, pick_default_drive};
70
71    #[test]
72    fn chooses_first_audio_drive() {
73        let drives = vec![
74            DriveInfo {
75                path: "disk10".to_string(),
76                has_audio_cd: false,
77            },
78            DriveInfo {
79                path: "disk11".to_string(),
80                has_audio_cd: true,
81            },
82            DriveInfo {
83                path: "disk12".to_string(),
84                has_audio_cd: true,
85            },
86        ];
87
88        assert_eq!(
89            pick_default_drive(&drives).map(|drive| drive.path.as_str()),
90            Some("disk11")
91        );
92    }
93
94    #[test]
95    fn returns_none_when_no_audio_drive() {
96        let drives = vec![DriveInfo {
97            path: "/dev/sr0".to_string(),
98            has_audio_cd: false,
99        }];
100
101        assert!(pick_default_drive(&drives).is_none());
102    }
103}