/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use crate::USER_AGENT; use anyhow::{bail, Result}; use jellybase::cache::async_cache_memory; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; use serde::Deserialize; use serde_json::Value; use std::collections::BTreeMap; pub struct Wikidata { client: Client, } #[derive(Debug, Deserialize, Clone)] pub struct WikidataResponse { entities: BTreeMap, } #[derive(Debug, Deserialize, Clone)] pub struct WikidataEntity { pub pageid: u64, pub ns: u64, pub title: String, pub lastrevid: u64, pub modified: String, pub r#type: String, pub id: String, pub claims: BTreeMap>, } #[derive(Debug, Deserialize, Clone)] pub struct WikidataClaim { pub r#type: String, pub id: String, pub rank: String, pub mainsnak: WikidataSnak, } #[derive(Debug, Deserialize, Clone)] pub struct WikidataSnak { pub snaktype: String, pub property: String, pub hash: String, pub datavalue: WikidataValue, pub datatype: String, } #[derive(Debug, Deserialize, Clone)] pub struct WikidataValue { pub value: Value, pub r#type: String, } pub mod properties { pub static IMAGE: &str = "P18"; } impl Wikidata { pub fn new() -> Self { let client = ClientBuilder::new() .default_headers(HeaderMap::from_iter([ ( HeaderName::from_static("accept"), HeaderValue::from_static("application/json"), ), ( HeaderName::from_static("user-agent"), HeaderValue::from_static(USER_AGENT), ), ])) .build() .unwrap(); Self { client } } pub async fn query_image_path(&self, id: String) -> Result> { let response = self.query(id.clone()).await?; if let Some(entity) = response.entities.get(&id) { if let Some(images) = entity.claims.get(properties::IMAGE) { for image in images { if image.mainsnak.datatype != "commonsMedia" { bail!("image is of type {:?}", image.mainsnak.datatype); } if let Value::String(filename) = &image.mainsnak.datavalue.value { return Ok(Some(filename.to_owned())); } } } } Ok(None) } pub async fn query(&self, id: String) -> Result { let json = async_cache_memory("api-wikidata", id.clone(), || async move { info!("entity query: {id}"); let resp = self .client .get(format!("https://www.wikidata.org/entity/{id}")) .send() .await? .error_for_status()? .text() .await?; Ok(resp) }) .await?; Ok(serde_json::from_str(&json)?) } }