diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-23 12:00:09 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-23 12:00:09 +0200 |
commit | 81b7026e10cb4aa131e61920449cd52a54897952 (patch) | |
tree | 56fe08bcd4183e8ec1e933b339c9cbd2a2062b62 | |
parent | d1ef44b1289ad0de08c757b3216eb226da7450d9 (diff) | |
download | jellything-81b7026e10cb4aa131e61920449cd52a54897952.tar jellything-81b7026e10cb4aa131e61920449cd52a54897952.tar.bz2 jellything-81b7026e10cb4aa131e61920449cd52a54897952.tar.zst |
more work on acoustid and import existing musicbrainz tags
-rw-r--r-- | common/src/config.rs | 1 | ||||
-rw-r--r-- | import/src/acoustid.rs | 101 | ||||
-rw-r--r-- | import/src/lib.rs | 31 |
3 files changed, 125 insertions, 8 deletions
diff --git a/common/src/config.rs b/common/src/config.rs index 4ec43eb..e2f4e62 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -73,6 +73,7 @@ pub struct FederationAccount { #[derive(Serialize, Deserialize, Debug, Default)] pub struct ApiSecrets { + pub acoustid: Option<String>, pub tmdb: Option<String>, pub tvdb: Option<String>, pub imdb: Option<String>, diff --git a/import/src/acoustid.rs b/import/src/acoustid.rs index b5a466a..8e8a603 100644 --- a/import/src/acoustid.rs +++ b/import/src/acoustid.rs @@ -6,16 +6,105 @@ 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}; -use tokio::{io::AsyncReadExt, process::Command}; +use std::{path::Path, process::Stdio, sync::Arc, time::Duration}; +use tokio::{ + io::AsyncReadExt, + process::Command, + sync::Semaphore, + time::{sleep_until, Instant}, +}; -#[derive(Debug, Encode, Decode, Deserialize)] +pub(crate) struct AcoustID { + client: Client, + key: String, + rate_limit: Arc<Semaphore>, +} + +#[derive(Debug, Hash, Clone, Encode, Decode, Deserialize)] pub(crate) struct Fingerprint { - duration: f32, + duration: u32, 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<AcoustIDLookupResultRecording>, +} +#[derive(Deserialize, Encode, Decode)] +pub(crate) struct AcoustIDLookupResponse { + status: String, + results: Vec<AcoustIDLookupResult>, +} + +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"), + )])) + .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<Option<(String, String)>> { + let res = self.lookup(fp).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<Arc<AcoustIDLookupResponse>> { + 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; + let client = &self.key; + let meta = "recordingids"; + let body = format!("format=json&client={client}&duration={duration}&fingerprint={fingerprint}&meta={meta}"); + + let resp = self + .client + .post(format!("https://api.acoustid.org/v2/lookup")) + .body(body) + .send() + .await?.error_for_status()?.json::<AcoustIDLookupResponse>().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<Arc<Fingerprint>> { async_cache_memory("fpcalc", path, || async move { @@ -33,7 +122,3 @@ pub(crate) async fn acoustid_fingerprint(path: &Path) -> Result<Arc<Fingerprint> }) .await } - -// pub(crate) async fn acoustid_mbid(fingerprint: Fingerprint) -> Result<Arc<Option<String>>> { -// async_cache_memory(&["api-acoustid", fingerprint], generate) -// } diff --git a/import/src/lib.rs b/import/src/lib.rs index 8b7dc8f..4ee4a6e 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -3,6 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +#![feature(duration_constants)] +use acoustid::AcoustID; use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; use jellybase::{ @@ -48,6 +50,7 @@ static RE_EPISODE_FILENAME: LazyLock<Regex> = struct Apis { trakt: Option<Trakt>, tmdb: Option<Tmdb>, + acoustid: Option<AcoustID>, } pub fn is_importing() -> bool { @@ -73,6 +76,7 @@ fn import(db: &Database, incremental: bool) -> Result<()> { let apis = Apis { trakt: SECRETS.api.trakt.as_ref().map(|key| Trakt::new(key)), tmdb: SECRETS.api.tmdb.as_ref().map(|key| Tmdb::new(key)), + acoustid: SECRETS.api.acoustid.as_ref().map(|key| AcoustID::new(key)), }; let rthandle = Handle::current(); @@ -365,6 +369,33 @@ fn import_media_file( } } + for (key, value) in &tags { + match key.as_str() { + "MUSICBRAINZ_TRACKID" => node + .external_ids + .insert("musicbrainz:track".to_string(), value.to_owned()), + "MUSICBRAINZ_ARTISTID" => node + .external_ids + .insert("musicbrainz:artist".to_string(), value.to_owned()), + "MUSICBRAINZ_ALBUMID" => node + .external_ids + .insert("musicbrainz:album".to_string(), value.to_owned()), + "MUSICBRAINZ_ALBUMARTISTID" => node + .external_ids + .insert("musicbrainz:albumarists".to_string(), value.to_owned()), + "MUSICBRAINZ_RELEASEGROUPID" => node + .external_ids + .insert("musicbrainz:releasegroup".to_string(), value.to_owned()), + "ISRC" => node + .external_ids + .insert("isrc".to_string(), value.to_owned()), + "BARCODE" => node + .external_ids + .insert("barcode".to_string(), value.to_owned()), + _ => None, + }; + } + let tracks = tracks .entries .into_iter() |