aboutsummaryrefslogtreecommitdiff
path: root/import/src/musicbrainz.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-23 19:04:33 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-23 19:04:33 +0200
commit388ae00ce1d510143789e85732831280ec803da7 (patch)
tree470092c081ca2abf71d884461f6d8c0cb2269748 /import/src/musicbrainz.rs
parent960007b06e2b47d41f88365c26f043f61c817f08 (diff)
downloadjellything-388ae00ce1d510143789e85732831280ec803da7.tar
jellything-388ae00ce1d510143789e85732831280ec803da7.tar.bz2
jellything-388ae00ce1d510143789e85732831280ec803da7.tar.zst
more work on musicbrainz
Diffstat (limited to 'import/src/musicbrainz.rs')
-rw-r--r--import/src/musicbrainz.rs128
1 files changed, 122 insertions, 6 deletions
diff --git a/import/src/musicbrainz.rs b/import/src/musicbrainz.rs
index 059b8f5..7a58cec 100644
--- a/import/src/musicbrainz.rs
+++ b/import/src/musicbrainz.rs
@@ -4,22 +4,95 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
+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 std::sync::Arc;
-use tokio::sync::Semaphore;
-use crate::USER_AGENT;
+use serde::Deserialize;
+use std::{collections::BTreeMap, sync::Arc, time::Duration};
+use tokio::{
+ sync::Semaphore,
+ time::{sleep_until, Instant},
+};
pub struct MusicBrainz {
client: Client,
- key: String,
rate_limit: Arc<Semaphore>,
}
+#[derive(Debug, Deserialize, Encode, Decode)]
+#[serde(rename_all = "kebab-case")]
+pub struct MbRecording {
+ pub id: String,
+ pub first_release_date: String,
+ pub title: String,
+ pub isrcs: Vec<String>,
+ pub video: bool,
+ pub disambiguation: String,
+ pub length: u32,
+ pub relations: Vec<MbRelation>,
+}
+
+#[derive(Debug, Deserialize, Encode, Decode)]
+#[serde(rename_all = "kebab-case")]
+pub struct MbRelation {
+ direction: String,
+ r#type: String,
+ type_id: String,
+ begin: Option<String>,
+ end: Option<String>,
+ ended: bool,
+ target_type: String,
+ target_credit: String,
+ source_credit: String,
+ attributes: Vec<String>,
+ attribute_ids: BTreeMap<String, String>,
+ attribute_values: BTreeMap<String, String>,
+
+ work: Option<MbWork>,
+ artist: Option<MbArtist>,
+ url: Option<MbUrl>,
+}
+
+#[derive(Debug, Deserialize, Encode, Decode)]
+#[serde(rename_all = "kebab-case")]
+pub struct MbWork {
+ id: String,
+ r#type: String,
+ type_id: String,
+ languages: Vec<String>,
+ iswcs: Vec<String>,
+ language: Option<String>,
+ title: String,
+ attributes: Vec<String>,
+ disambiguation: String,
+}
+
+#[derive(Debug, Deserialize, Encode, Decode)]
+#[serde(rename_all = "kebab-case")]
+pub struct MbArtist {
+ id: String,
+ r#type: String,
+ type_id: String,
+ name: String,
+ disambiguation: String,
+ country: String,
+ sort_name: String,
+}
+
+#[derive(Debug, Deserialize, Encode, Decode)]
+#[serde(rename_all = "kebab-case")]
+pub struct MbUrl {
+ id: String,
+ resource: String,
+}
+
impl MusicBrainz {
- pub fn new(api_key: &str) -> Self {
+ pub fn new() -> Self {
let client = ClientBuilder::new()
.default_headers(HeaderMap::from_iter([
(
@@ -38,7 +111,50 @@ impl MusicBrainz {
// send at most 1 req/s according to musicbrainz docs, each lock is held for 10s
// this implementation also never sends more than 10 requests in-flight.
rate_limit: Arc::new(Semaphore::new(10)),
- key: api_key.to_owned(),
}
}
+
+ pub async fn lookup_recording(&self, id: String) -> Result<Arc<MbRecording>> {
+ async_cache_memory("api-musicbrainz-recording", id.clone(), || async move {
+ let _permit = self.rate_limit.clone().acquire_owned().await?;
+ let permit_drop_ts = Instant::now() + Duration::from_secs(10);
+
+ let inc = [
+ "isrcs",
+ "area-rels",
+ "artist-rels",
+ "event-rels",
+ "genre-rels",
+ "instrument-rels",
+ "label-rels",
+ "place-rels",
+ "recording-rels",
+ "release-rels",
+ "release-group-rels",
+ "series-rels",
+ "url-rels",
+ "work-rels",
+ ]
+ .join("+");
+
+ let resp = self
+ .client
+ .get(format!(
+ "https://musicbrainz.org/ws/2/recording/{id}?inc={inc}"
+ ))
+ .send()
+ .await?
+ .error_for_status()?
+ .json::<MbRecording>()
+ .await?;
+
+ tokio::task::spawn(async move {
+ sleep_until(permit_drop_ts).await;
+ drop(_permit);
+ });
+
+ Ok(resp)
+ })
+ .await
+ }
}