use anyhow::Result;
use std::path::Path;
const FP_SECONDS: u64 = 30;
use symphonia::core::codecs::{CodecParameters, audio::AudioDecoderOptions};
use symphonia::core::formats::probe::Hint;
use symphonia::core::formats::{FormatOptions, TrackType};
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
const AUDIO_EXTS: &[&str] = &[
"mp3", "flac", "ogg", "oga", "m4a", "mp4", "aac", "alac", "wav", "wave", "aiff", "aif",
];
pub fn is_audio(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.map(|e| AUDIO_EXTS.contains(&e.to_ascii_lowercase().as_str()))
.unwrap_or(false)
}
pub fn fingerprint(path: &Path) -> Result<Option<String>> {
if !is_audio(path) {
return Ok(None);
}
Ok(decode_hash(path).unwrap_or(None))
}
fn decode_hash(path: &Path) -> Result<Option<String>> {
let file = std::fs::File::open(path)?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let mut format = match symphonia::default::get_probe().probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
) {
Ok(f) => f,
Err(_) => return Ok(None),
};
let track = match format.default_track(TrackType::Audio) {
Some(t) => t,
None => return Ok(None),
};
let track_id = track.id;
let params = match &track.codec_params {
Some(CodecParameters::Audio(p)) => p,
_ => return Ok(None),
};
let mut decoder = match symphonia::default::get_codecs()
.make_audio_decoder(params, &AudioDecoderOptions::default())
{
Ok(d) => d,
Err(_) => return Ok(None),
};
let mut hasher = blake3::Hasher::new();
let mut header_done = false;
let mut target: u64 = u64::MAX;
let mut hashed_samples: u64 = 0;
let mut interleaved: Vec<i16> = Vec::new();
let mut bytes: Vec<u8> = Vec::new();
while let Ok(Some(packet)) = format.next_packet() {
if packet.track_id != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(symphonia::core::errors::Error::DecodeError(_)) => continue,
Err(_) => break,
};
if !header_done {
let spec = decoded.spec();
let rate = spec.rate() as u64;
let channels = spec.channels().count() as u64;
hasher.update(&(rate as u32).to_le_bytes());
hasher.update(&(channels as u32).to_le_bytes());
target = rate.saturating_mul(channels).saturating_mul(FP_SECONDS);
header_done = true;
}
let n = decoded.samples_interleaved();
if interleaved.len() < n {
interleaved.resize(n, 0);
}
decoded.copy_to_slice_interleaved::<i16, _>(&mut interleaved[..n]);
bytes.clear();
bytes.reserve(n * 2);
for s in &interleaved[..n] {
bytes.extend_from_slice(&s.to_le_bytes());
}
hasher.update(&bytes);
hashed_samples += n as u64;
if hashed_samples >= target {
break;
}
}
if !header_done {
return Ok(None);
}
Ok(Some(hasher.finalize().to_hex().to_string()))
}