use anyhow::{Context, Result};
use base64::Engine;
use std::path::Path;
use std::process::Command;
pub fn fingerprint_file(path: &Path) -> Result<Vec<u8>> {
if let Ok(fp) = fingerprint_with_chromaprint(path) {
return Ok(fp);
}
if crate::media_dedup::is_ffmpeg_available() {
fingerprint_with_ffmpeg(path)
} else {
Err(anyhow::anyhow!(
"Neither chromaprint nor ffmpeg are available for audio fingerprinting"
))
}
}
fn fingerprint_with_chromaprint(path: &Path) -> Result<Vec<u8>> {
let output = Command::new("fpcalc")
.arg("-raw")
.arg("-json")
.arg(path)
.output()
.context("Failed to execute fpcalc. Is chromaprint installed?")?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"fpcalc failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let output_str = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&output_str).context("Failed to parse fpcalc JSON output")?;
if let Some(fingerprint_str) = json["fingerprint"].as_str() {
let fingerprint = hex::decode(fingerprint_str)
.or_else(|_| base64::engine::general_purpose::STANDARD.decode(fingerprint_str))
.context("Failed to decode fingerprint")?;
Ok(fingerprint)
} else {
Err(anyhow::anyhow!("No fingerprint found in fpcalc output"))
}
}
fn fingerprint_with_ffmpeg(path: &Path) -> Result<Vec<u8>> {
let output = Command::new("ffmpeg")
.args([
"-i",
path.to_str().unwrap(),
"-filter:a",
"ebur128=metadata=1",
"-f",
"null",
"-",
])
.output()
.context("Failed to execute ffmpeg")?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"ffmpeg failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let stderr = String::from_utf8_lossy(&output.stderr);
let mut fingerprint = Vec::new();
if let Some(pos) = stderr.find("I:") {
if let Some(end) = stderr[pos..].find(" ") {
let loudness_str = &stderr[pos + 2..pos + end];
if let Ok(loudness) = loudness_str.parse::<f32>() {
fingerprint.extend_from_slice(&loudness.to_le_bytes());
}
}
}
if let Some(pos) = stderr.find("LRA:") {
if let Some(end) = stderr[pos..].find(" ") {
let lra_str = &stderr[pos + 4..pos + end];
if let Ok(lra) = lra_str.parse::<f32>() {
fingerprint.extend_from_slice(&lra.to_le_bytes());
}
}
}
if fingerprint.is_empty() {
return Err(anyhow::anyhow!(
"Could not extract audio fingerprint from ffmpeg output"
));
}
Ok(fingerprint)
}
pub fn compare_fingerprints(fp1: &[u8], fp2: &[u8]) -> f64 {
if fp1.len() != fp2.len() || fp1.is_empty() {
return 0.0;
}
let mut matches = 0;
for (b1, b2) in fp1.iter().zip(fp2.iter()) {
let xor = b1 ^ b2;
matches += 8 - xor.count_ones();
}
(matches as f64) / ((fp1.len() * 8) as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compare_fingerprints() {
let fp1 = vec![0, 1, 2, 3, 4];
let fp2 = vec![0, 1, 2, 3, 4];
assert_eq!(compare_fingerprints(&fp1, &fp2), 1.0);
let fp3 = vec![255, 255, 255, 255, 255];
assert!(compare_fingerprints(&fp1, &fp3) < 0.5);
let empty = vec![];
assert_eq!(compare_fingerprints(&fp1, &empty), 0.0);
}
}