diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-23 19:04:33 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-23 19:04:33 +0200 |
commit | 388ae00ce1d510143789e85732831280ec803da7 (patch) | |
tree | 470092c081ca2abf71d884461f6d8c0cb2269748 | |
parent | 960007b06e2b47d41f88365c26f043f61c817f08 (diff) | |
download | jellything-388ae00ce1d510143789e85732831280ec803da7.tar jellything-388ae00ce1d510143789e85732831280ec803da7.tar.bz2 jellything-388ae00ce1d510143789e85732831280ec803da7.tar.zst |
more work on musicbrainz
-rw-r--r-- | import/src/lib.rs | 49 | ||||
-rw-r--r-- | import/src/musicbrainz.rs | 128 | ||||
-rw-r--r-- | locale/en.ini | 4 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 4 |
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, }) } |