use crate::sector::SectorMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrackMode {
Mode1_2048,
Mode1_2352,
Mode2_2336,
Mode2_2352,
Audio,
Other(String),
}
impl TrackMode {
#[must_use]
pub fn sector_mode(&self) -> Option<SectorMode> {
match self {
Self::Mode1_2048 => Some(SectorMode::Iso2048),
Self::Mode1_2352 => Some(SectorMode::Raw2352),
Self::Mode2_2336 => Some(SectorMode::Mode2_2336),
Self::Mode2_2352 => Some(SectorMode::Raw2352Mode2),
Self::Audio | Self::Other(_) => None,
}
}
#[must_use]
pub fn is_data(&self) -> bool {
self.sector_mode().is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Msf {
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
}
impl Msf {
#[must_use]
pub fn to_lba(self) -> u32 {
(u32::from(self.minutes) * 60 + u32::from(self.seconds)) * 75 + u32::from(self.frames)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CueTrack {
pub number: u8,
pub mode: TrackMode,
pub indices: Vec<(u8, Msf)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CueFile {
pub name: String,
pub format: String,
pub tracks: Vec<CueTrack>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CueSheet {
pub files: Vec<CueFile>,
}
impl CueSheet {
#[must_use]
pub fn data_track(&self) -> Option<(&str, &CueTrack)> {
for f in &self.files {
for t in &f.tracks {
if t.mode.is_data() {
return Some((f.name.as_str(), t));
}
}
}
None
}
}
pub fn parse(text: &str) -> CueSheet {
let mut sheet = CueSheet::default();
for line in text.lines() {
let line = line.trim();
let mut tok = line.split_whitespace();
match tok.next().map(str::to_ascii_uppercase).as_deref() {
Some("FILE") => {
let (name, format) = parse_file_line(line);
sheet.files.push(CueFile { name, format, tracks: Vec::new() });
}
Some("TRACK") => {
if let Some(file) = sheet.files.last_mut() {
let number = tok.next().and_then(|n| n.parse().ok()).unwrap_or(0);
let mode =
tok.next().map(parse_mode).unwrap_or(TrackMode::Other(String::new()));
file.tracks.push(CueTrack { number, mode, indices: Vec::new() });
}
}
Some("INDEX") => {
if let Some(track) = sheet.files.last_mut().and_then(|f| f.tracks.last_mut()) {
if let (Some(n), Some(ts)) = (tok.next(), tok.next()) {
if let (Ok(num), Some(msf)) = (n.parse::<u8>(), parse_msf(ts)) {
track.indices.push((num, msf));
}
}
}
}
_ => {} }
}
sheet
}
fn parse_file_line(line: &str) -> (String, String) {
let rest = line.trim_start_matches(|c: char| c.is_ascii_alphabetic()).trim();
if let Some(close) = rest.strip_prefix('"').and_then(|r| r.find('"').map(|i| (r, i))) {
let (r, i) = close;
let name = r[..i].to_string();
let format = r[i + 1..].trim().to_string();
(name, format)
} else {
let mut parts = rest.split_whitespace();
let name = parts.next().unwrap_or("").to_string();
let format = parts.next().unwrap_or("").to_string();
(name, format)
}
}
fn parse_mode(token: &str) -> TrackMode {
match token.to_ascii_uppercase().as_str() {
"MODE1/2048" => TrackMode::Mode1_2048,
"MODE1/2352" => TrackMode::Mode1_2352,
"MODE2/2336" => TrackMode::Mode2_2336,
"MODE2/2352" => TrackMode::Mode2_2352,
"AUDIO" => TrackMode::Audio,
other => TrackMode::Other(other.to_string()),
}
}
fn parse_msf(token: &str) -> Option<Msf> {
let mut parts = token.split(':');
let m = parts.next()?.parse().ok()?;
let s = parts.next()?.parse().ok()?;
let f = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some(Msf { minutes: m, seconds: s, frames: f })
}