/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use crate::USER_AGENT; use anyhow::Result; use bincode::{Decode, Encode}; use jellybase::cache::async_cache_memory; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; use serde::Deserialize; use std::{path::Path, process::Stdio, sync::Arc, time::Duration}; use tokio::{ io::AsyncReadExt, process::Command, sync::Semaphore, time::{sleep_until, Instant}, }; pub(crate) struct AcoustID { client: Client, key: String, rate_limit: Arc, } #[derive(Debug, Hash, Clone, Encode, Decode)] pub(crate) struct Fingerprint { duration: u32, fingerprint: String, } #[derive(Debug, Deserialize)] pub(crate) struct FpCalcOutput { duration: f32, fingerprint: String, } #[derive(Deserialize, Encode, Decode)] pub(crate) struct AcoustIDLookupResultRecording { id: String, } #[derive(Deserialize, Encode, Decode)] pub(crate) struct AcoustIDLookupResult { id: String, score: f32, recordings: Vec, } #[derive(Deserialize, Encode, Decode)] pub(crate) struct AcoustIDLookupResponse { status: String, results: Vec, } impl AcoustID { pub fn new(api_key: &str) -> Self { let client = ClientBuilder::new() .default_headers(HeaderMap::from_iter([ ( HeaderName::from_static("accept"), HeaderValue::from_static("application/json"), ), ( HeaderName::from_static("user-agent"), HeaderValue::from_static(USER_AGENT), ), ])) .build() .unwrap(); Self { client, // send at most 3 req/s according to acoustid docs, each lock is therefore held for 1s // this implementation also never sends more than 3 requests in-flight. rate_limit: Arc::new(Semaphore::new(3)), key: api_key.to_owned(), } } pub async fn get_atid_mbid(&self, fp: &Fingerprint) -> Result> { let res = self.lookup(fp.to_owned()).await?; for r in &res.results { if let Some(k) = r.recordings.get(0) { return Ok(Some((r.id.clone(), k.id.clone()))); } } Ok(None) } pub async fn lookup(&self, fp: Fingerprint) -> Result> { async_cache_memory("api-acoustid", fp.clone(), || async move { let _permit = self.rate_limit.clone().acquire_owned().await?; let permit_drop_ts = Instant::now() + Duration::SECOND; let duration = fp.duration; let fingerprint = fp.fingerprint.replace("=", "%3D"); let client = &self.key; let body = format!("format=json&meta=recordingids&client={client}&duration={duration}&fingerprint={fingerprint}"); let resp = self .client .post(format!("https://api.acoustid.org/v2/lookup")) // .post(format!("http://127.0.0.1:1234/v2/lookup")) .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() .await?.error_for_status()?.json::().await?; tokio::task::spawn(async move { sleep_until(permit_drop_ts).await; drop(_permit); }); Ok(resp) }) .await } } #[allow(unused)] pub(crate) async fn acoustid_fingerprint(path: &Path) -> Result> { async_cache_memory("fpcalc", path, || async move { let child = Command::new("fpcalc") .arg("-json") .arg(path) .stdout(Stdio::piped()) .spawn()?; let mut buf = Vec::new(); child.stdout.unwrap().read_to_end(&mut buf).await?; let out: FpCalcOutput = serde_json::from_slice(&buf)?; let out = Fingerprint { duration: out.duration as u32, fingerprint: out.fingerprint, }; Ok(out) }) .await }