From 2c6f753797b8e223c5982c4790f1be8b08fe63d6 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Fri, 25 Apr 2025 21:34:59 +0200 Subject: images from wikidata works --- import/src/lib.rs | 72 +++++++++++++++++++++++-------- import/src/musicbrainz.rs | 107 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 24 deletions(-) diff --git a/import/src/lib.rs b/import/src/lib.rs index 0f89ab0..fa74b9c 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -700,26 +700,60 @@ fn apply_musicbrainz_recording( for rel in &rec.relations { use musicbrainz::reltypes::*; - match rel.type_id.as_str() { - INSTRUMENT => { - node.people - .entry(PeopleGroup::Instrument) - .or_default() - .push(Appearance { - jobs: rel.attributes.clone(), - characters: vec![], - person: Person { - name: rel - .artist - .as_ref() - .map(|a| a.name.clone()) - .unwrap_or_default(), - headshot: None, - ids: ObjectIds::default(), - }, - }); + let a = match rel.type_id.as_str() { + INSTRUMENT => Some(("", PeopleGroup::Instrument)), + VOCAL => Some(("", PeopleGroup::Vocal)), + PRODUCER => Some(("", PeopleGroup::Producer)), + MIX => Some(("mix ", PeopleGroup::Engineer)), + PHONOGRAPHIC_COPYRIGHT => Some(("phonographic copyright ", PeopleGroup::Engineer)), + PROGRAMMING => Some(("programming ", PeopleGroup::Engineer)), + _ => None, + }; + + if let Some((note, group)) = a { + let artist = rel.artist.as_ref().unwrap(); + + let artist = + rthandle.block_on(apis.musicbrainz.lookup_artist(artist.id.clone()))?; + + let mut image = None; + + for rel in &artist.relations { + match rel.type_id.as_str() { + WIKIDATA => { + let url = rel.url.as_ref().unwrap().resource.clone(); + if let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { + if let Some(filename) = rthandle + .block_on(apis.wikidata.query_image_path(id.to_owned()))? + { + let path = rthandle.block_on( + apis.wikimedia_commons.image_by_filename(filename), + )?; + image = Some(AssetInner::Cache(path).ser()); + } + } + } + _ => (), + } } - _ => (), + let mut jobs = vec![]; + if !note.is_empty() { + jobs.push(note.to_string()); + } + jobs.extend(rel.attributes.clone()); + node.people.entry(group).or_default().push(Appearance { + jobs, + characters: vec![], + person: Person { + name: if rel.target_credit.is_empty() { + artist.name.clone() + } else { + rel.target_credit.clone() + }, + headshot: image, + ids: ObjectIds::default(), + }, + }); } } diff --git a/import/src/musicbrainz.rs b/import/src/musicbrainz.rs index 0fe839b..704807b 100644 --- a/import/src/musicbrainz.rs +++ b/import/src/musicbrainz.rs @@ -53,6 +53,24 @@ pub struct MbRecordingRel { pub artist_credit: Vec, } +#[derive(Debug, Deserialize, Encode, Decode)] +#[serde(rename_all = "kebab-case")] +pub struct MbArtistRel { + pub id: String, + pub isnis: Vec, + pub ipis: Vec, + pub name: String, + pub disambiguation: String, + pub country: Option, + pub sort_name: String, + pub gender_id: Option, + pub area: Option, + pub begin_area: Option, + pub end_area: Option, + pub life_span: MbTimespan, + pub relations: Vec, +} + #[derive(Debug, Deserialize, Encode, Decode)] #[serde(rename_all = "kebab-case")] pub struct MbArtistCredit { @@ -81,6 +99,7 @@ pub struct MbRelation { pub url: Option, pub recording: Option, pub series: Option, + pub event: Option, } #[derive(Debug, Deserialize, Encode, Decode)] @@ -98,10 +117,12 @@ pub struct MbSeries { pub struct MbRecording { pub id: String, pub title: String, + #[serde(default)] pub isrcs: Vec, pub video: bool, pub disambiguation: String, - pub length: u32, + pub length: Option, + #[serde(default)] pub artist_credit: Vec, } @@ -109,8 +130,8 @@ pub struct MbRecording { #[serde(rename_all = "kebab-case")] pub struct MbWork { pub id: String, - pub r#type: String, - pub type_id: String, + pub r#type: Option, + pub type_id: Option, pub languages: Vec, pub iswcs: Vec, pub language: Option, @@ -119,6 +140,19 @@ pub struct MbWork { pub disambiguation: String, } +#[derive(Debug, Deserialize, Encode, Decode)] +#[serde(rename_all = "kebab-case")] +pub struct MbEvent { + pub id: String, + pub r#type: String, + pub type_id: String, + pub name: String, + pub time: String, + pub cancelled: bool, + pub setlist: String, + pub life_span: MbTimespan, +} + #[derive(Debug, Deserialize, Encode, Decode)] #[serde(rename_all = "kebab-case")] pub struct MbArtist { @@ -131,11 +165,30 @@ pub struct MbArtist { pub sort_name: String, } +#[derive(Debug, Deserialize, Encode, Decode)] +#[serde(rename_all = "kebab-case")] +pub struct MbTimespan { + pub begin: Option, + pub end: Option, + pub ended: bool, +} + +#[derive(Debug, Deserialize, Encode, Decode)] +#[serde(rename_all = "kebab-case")] +pub struct MbArea { + pub name: String, + pub sort_name: String, + #[serde(default)] + pub iso_3166_1_codes: Vec, + pub id: String, + pub disambiguation: String, +} + #[derive(Debug, Deserialize, Encode, Decode)] #[serde(rename_all = "kebab-case")] pub struct MbUrl { - id: String, - resource: String, + pub id: String, + pub resource: String, } impl MusicBrainz { @@ -206,4 +259,48 @@ impl MusicBrainz { }) .await } + + pub async fn lookup_artist(&self, id: String) -> Result> { + async_cache_memory("api-musicbrainz-artist", id.clone(), || async move { + let _permit = self.rate_limit.clone().acquire_owned().await?; + let permit_drop_ts = Instant::now() + Duration::from_secs(10); + info!("artist lookup: {id}"); + + let inc = [ + "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/artist/{id}?inc={inc}" + )) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + tokio::task::spawn(async move { + sleep_until(permit_drop_ts).await; + drop(_permit); + }); + + Ok(resp) + }) + .await + } } -- cgit v1.2.3-70-g09d2