aboutsummaryrefslogtreecommitdiff
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
parent960007b06e2b47d41f88365c26f043f61c817f08 (diff)
downloadjellything-388ae00ce1d510143789e85732831280ec803da7.tar
jellything-388ae00ce1d510143789e85732831280ec803da7.tar.bz2
jellything-388ae00ce1d510143789e85732831280ec803da7.tar.zst
more work on musicbrainz
-rw-r--r--import/src/lib.rs49
-rw-r--r--import/src/musicbrainz.rs128
-rw-r--r--locale/en.ini4
-rw-r--r--server/src/routes/ui/node.rs4
4 files changed, 172 insertions, 13 deletions
diff --git a/import/src/lib.rs b/import/src/lib.rs
index b45361a..f033171 100644
--- a/import/src/lib.rs
+++ b/import/src/lib.rs
@@ -18,6 +18,7 @@ use jellyclient::{
};
use jellyremuxer::metadata::checked_matroska_metadata;
use log::info;
+use musicbrainz::MusicBrainz;
use rayon::iter::{ParallelBridge, ParallelIterator};
use regex::Regex;
use std::{
@@ -58,6 +59,7 @@ struct Apis {
trakt: Option<Trakt>,
tmdb: Option<Tmdb>,
acoustid: Option<AcoustID>,
+ musicbrainz: MusicBrainz,
}
pub fn is_importing() -> bool {
@@ -84,6 +86,7 @@ fn import(db: &Database, incremental: bool) -> Result<()> {
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)),
+ musicbrainz: MusicBrainz::new(),
};
let rthandle = Handle::current();
@@ -367,11 +370,15 @@ fn import_media_file(
for (key, value) in &tags {
match key.as_str() {
- "MUSICBRAINZ_TRACKID" => eids.insert("musicbrainz.track".to_string(), value.to_owned()),
+ "MUSICBRAINZ_TRACKID" => {
+ eids.insert("musicbrainz.recording".to_string(), value.to_owned())
+ }
"MUSICBRAINZ_ARTISTID" => {
eids.insert("musicbrainz.artist".to_string(), value.to_owned())
}
- "MUSICBRAINZ_ALBUMID" => eids.insert("musicbrainz.album".to_string(), value.to_owned()),
+ "MUSICBRAINZ_ALBUMID" => {
+ eids.insert("musicbrainz.release".to_string(), value.to_owned())
+ }
"MUSICBRAINZ_ALBUMARTISTID" => {
eids.insert("musicbrainz.albumartist".to_string(), value.to_owned())
}
@@ -393,10 +400,12 @@ fn import_media_file(
.get_atid_mbid(&fp),
)? {
eids.insert("acoustid.track".to_string(), atid);
- eids.insert("musicbrainz.track".to_string(), mbid);
+ eids.insert("musicbrainz.recording".to_string(), mbid);
};
}
+ let mbrec = eids.get("musicbrainz.recording").cloned();
+
db.update_node_init(node, |node| {
node.slug = slug;
node.title = info.title.or(node.title.clone());
@@ -599,6 +608,9 @@ fn import_media_file(
for tok in filename_toks {
apply_node_flag(db, rthandle, apis, node, tok)?;
}
+ if let Some(mbid) = mbrec {
+ apply_musicbrainz_recording(db, rthandle, apis, node, mbid)?;
+ }
Ok(())
}
@@ -652,6 +664,37 @@ fn apply_node_flag(
Ok(())
})?;
}
+ if let Some(mbid) = flag.strip_prefix("mbrec-").or(flag.strip_prefix("mbrec=")) {
+ apply_musicbrainz_recording(db, rthandle, apis, node, mbid.to_string())?
+ }
+ Ok(())
+}
+
+fn apply_musicbrainz_recording(
+ db: &Database,
+ rthandle: &Handle,
+ apis: &Apis,
+ node: NodeID,
+ mbid: String,
+) -> Result<()> {
+ let rec = rthandle.block_on(apis.musicbrainz.lookup_recording(mbid))?;
+
+ db.update_node_init(node, |node| {
+ node.title = Some(rec.title.clone());
+ node.external_ids
+ .insert("musicbrainz.recording".to_string(), rec.id.to_string());
+ if let Some(a) = rec.artist_credit.get(0) {
+ node.subtitle = Some(a.artist.name.clone());
+ node.external_ids
+ .insert("musicbrainz.artist".to_string(), a.artist.id.to_string());
+ }
+
+ for isrc in &rec.isrcs {
+ node.external_ids
+ .insert("isrc".to_string(), isrc.to_string());
+ }
+ Ok(())
+ })?;
Ok(())
}
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
+ }
}
diff --git a/locale/en.ini b/locale/en.ini
index 8705ae1..944a29d 100644
--- a/locale/en.ini
+++ b/locale/en.ini
@@ -159,11 +159,11 @@ settings.player_preference.changed=Media Player preference changed.
eid.isrc=ISRC
eid.barcode=Barcode
-eid.musicbrainz.album=MusicBrainz Album ID
+eid.musicbrainz.release=MusicBrainz Release ID
eid.musicbrainz.albumartist=MusicBrainz Album Artist ID
eid.musicbrainz.artist=MusicBrainz Artist ID
eid.musicbrainz.releasegroup=MusicBrainz Release Group ID
-eid.musicbrainz.track=MusicBrainz Track ID
+eid.musicbrainz.recording=MusicBrainz Recording ID
eid.acoustid.track=AcoustID Track ID
eid.youtube.channelname=YouTube Channel Handle
eid.youtube.channelname=YouTube Channel ID
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index 6d2151f..d968f0a 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -548,11 +548,11 @@ fn external_id_url(key: &str, value: &str) -> Option<String> {
"youtube.video" => format!("https://youtube.com/watch?v={value}"),
"youtube.channel" => format!("https://youtube.com/channel/{value}"),
"youtube.channelname" => format!("https://youtube.com/channel/@{value}"),
- "musicbrainz.album" => format!("https://musicbrainz.org/release/{value}"),
+ "musicbrainz.release" => format!("https://musicbrainz.org/release/{value}"),
"musicbrainz.albumartist" => format!("https://musicbrainz.org/artist/{value}"),
"musicbrainz.artist" => format!("https://musicbrainz.org/artist/{value}"),
"musicbrainz.releasegroup" => format!("https://musicbrainz.org/release-group/{value}"),
- "musicbrainz.track" => format!("https://musicbrainz.org/recording/{value}"),
+ "musicbrainz.recording" => format!("https://musicbrainz.org/recording/{value}"),
_ => return None,
})
}