#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Toc {
pub first_track: u8,
pub track_frames: Vec<u32>,
pub leadout_frame: u32,
}
impl Toc {
#[must_use]
pub fn from_cue(sheet: &crate::cue::CueSheet, total_frames: u32) -> Option<Self> {
const LEAD_IN: u32 = 150;
let mut numbers = Vec::new();
let mut frames = Vec::new();
for file in &sheet.files {
for track in &file.tracks {
let idx = track
.indices
.iter()
.find(|(n, _)| *n == 1)
.or_else(|| track.indices.first())?;
numbers.push(track.number);
frames.push(idx.1.to_lba() + LEAD_IN);
}
}
if frames.is_empty() {
return None;
}
Some(Self {
first_track: numbers[0],
track_frames: frames,
leadout_frame: total_frames + LEAD_IN,
})
}
#[must_use]
pub fn track_count(&self) -> usize {
self.track_frames.len()
}
#[must_use]
pub fn last_track(&self) -> u8 {
self.first_track + self.track_frames.len().saturating_sub(1) as u8
}
#[must_use]
pub fn track_length_frames(&self, i: usize) -> Option<u32> {
let start = *self.track_frames.get(i)?;
let next = self.track_frames.get(i + 1).copied().unwrap_or(self.leadout_frame);
Some(next.saturating_sub(start))
}
#[must_use]
pub fn freedb_id(&self) -> u32 {
let digit_sum = |mut n: u32| -> u32 {
let mut s = 0;
while n > 0 {
s += n % 10;
n /= 10;
}
s
};
let n: u32 = self.track_frames.iter().map(|&f| digit_sum(f / 75)).sum();
let first = self.track_frames.first().copied().unwrap_or(0);
let total = (self.leadout_frame / 75).saturating_sub(first / 75);
((n % 255) << 24) | (total << 8) | (self.track_count() as u32 & 0xff)
}
#[must_use]
pub fn freedb_id_hex(&self) -> String {
format!("{:08x}", self.freedb_id())
}
#[must_use]
pub fn musicbrainz_id(&self) -> String {
use sha1::{Digest, Sha1};
let mut fo = [0u32; 100];
fo[0] = self.leadout_frame;
let first = self.first_track as usize;
for (k, &f) in self.track_frames.iter().enumerate() {
let idx = first + k;
if idx < fo.len() {
fo[idx] = f;
}
}
let mut s = format!("{:02X}{:02X}", self.first_track, self.last_track());
for v in fo {
s.push_str(&format!("{v:08X}"));
}
let digest = Sha1::digest(s.as_bytes());
base64_musicbrainz(&digest)
}
}
fn base64_musicbrainz(data: &[u8]) -> String {
const STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(STD[(n >> 18 & 63) as usize] as char);
out.push(STD[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 { STD[(n >> 6 & 63) as usize] as char } else { '=' });
out.push(if chunk.len() > 2 { STD[(n & 63) as usize] as char } else { '=' });
}
out.replace('+', ".").replace('/', "_").replace('=', "-")
}