diff options
Diffstat (limited to 'import/src')
| -rw-r--r-- | import/src/acoustid.rs | 75 | ||||
| -rw-r--r-- | import/src/lib.rs | 138 | ||||
| -rw-r--r-- | import/src/musicbrainz.rs | 172 | ||||
| -rw-r--r-- | import/src/tmdb.rs | 163 | ||||
| -rw-r--r-- | import/src/trakt.rs | 116 | ||||
| -rw-r--r-- | import/src/vgmdb.rs | 85 | ||||
| -rw-r--r-- | import/src/wikidata.rs | 53 | ||||
| -rw-r--r-- | import/src/wikimedia_commons.rs | 42 |
8 files changed, 446 insertions, 398 deletions
diff --git a/import/src/acoustid.rs b/import/src/acoustid.rs index c35708d..809d964 100644 --- a/import/src/acoustid.rs +++ b/import/src/acoustid.rs @@ -5,17 +5,22 @@ */ use crate::USER_AGENT; use anyhow::{Context, Result}; -use jellycache::async_cache_memory; +use jellycache::{cache_memory, CacheKey}; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; use serde::{Deserialize, Serialize}; -use std::{path::Path, process::Stdio, sync::Arc, time::Duration}; +use std::{ + io::Read, + path::Path, + process::{Command, Stdio}, + sync::Arc, + time::Duration, +}; use tokio::{ - io::AsyncReadExt, - process::Command, + runtime::Handle, sync::Semaphore, time::{sleep_until, Instant}, }; @@ -79,8 +84,8 @@ impl AcoustID { } } - pub async fn get_atid_mbid(&self, fp: &Fingerprint) -> Result<Option<(String, String)>> { - let res = self.lookup(fp.to_owned()).await?; + pub fn get_atid_mbid(&self, fp: &Fingerprint, rt: &Handle) -> Result<Option<(String, String)>> { + let res = self.lookup(fp.to_owned(), rt)?; for r in &res.results { if let Some(k) = r.recordings.first() { return Ok(Some((r.id.clone(), k.id.clone()))); @@ -89,8 +94,8 @@ impl AcoustID { Ok(None) } - pub async fn lookup(&self, fp: Fingerprint) -> Result<Arc<AcoustIDLookupResponse>> { - async_cache_memory("api-acoustid", fp.clone(), || async move { + pub fn lookup(&self, fp: Fingerprint, rt: &Handle) -> Result<Arc<AcoustIDLookupResponse>> { + cache_memory(CacheKey::new_json(("acoustid-lookup", &fp)) , move || rt.block_on(async { let _permit = self.rate_limit.clone().acquire_owned().await?; let permit_drop_ts = Instant::now() + Duration::SECOND; info!("acoustid lookup"); @@ -114,36 +119,36 @@ impl AcoustID { }); Ok(resp) - }) - .await.context("acoustid lookup") + })) + .context("acoustid lookup") } } -#[allow(unused)] -pub(crate) async fn acoustid_fingerprint(path: &Path) -> Result<Arc<Fingerprint>> { - async_cache_memory("fpcalc", path, || async move { - let child = Command::new("fpcalc") - .arg("-json") - .arg(path) - .stdout(Stdio::piped()) - .spawn() - .context("fpcalc")?; - - let mut buf = Vec::new(); - child - .stdout - .unwrap() - .read_to_end(&mut buf) - .await - .context("read fpcalc output")?; +pub(crate) fn acoustid_fingerprint(path: &Path) -> Result<Arc<Fingerprint>> { + cache_memory( + CacheKey::new_json(("acoustid-fingerprint", path)), + move || { + let child = Command::new("fpcalc") + .arg("-json") + .arg(path) + .stdout(Stdio::piped()) + .spawn() + .context("fpcalc")?; - let out: FpCalcOutput = serde_json::from_slice(&buf).context("parsing fpcalc output")?; - let out = Fingerprint { - duration: out.duration as u32, - fingerprint: out.fingerprint, - }; + let mut buf = Vec::new(); + child + .stdout + .unwrap() + .read_to_end(&mut buf) + .context("read fpcalc output")?; - Ok(out) - }) - .await + let out: FpCalcOutput = + serde_json::from_slice(&buf).context("parsing fpcalc output")?; + let out = Fingerprint { + duration: out.duration as u32, + fingerprint: out.fingerprint, + }; + Ok(out) + }, + ) } diff --git a/import/src/lib.rs b/import/src/lib.rs index d12913f..3cbabdc 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -20,11 +20,10 @@ use crate::{tmdb::TmdbKind, trakt::TraktKind}; use acoustid::{acoustid_fingerprint, AcoustID}; use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; -use jellycache::{cache_file, cache_memory}; +use jellycache::{cache, cache_memory, cache_store, CacheKey}; use jellycommon::{ - Appearance, Asset, Chapter, CreditCategory, IdentifierType, LocalTrack, MediaInfo, Node, - NodeID, NodeKind, PictureSlot, RatingType, SourceTrack, SourceTrackKind, TrackSource, - Visibility, + Appearance, Chapter, CreditCategory, IdentifierType, MediaInfo, Node, NodeID, NodeKind, + Picture, PictureSlot, RatingType, SourceTrack, SourceTrackKind, TrackSource, Visibility, }; use jellyimport_fallback_generator::generate_fallback; use jellyremuxer::{ @@ -39,7 +38,7 @@ use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, fs::{read_to_string, File}, - io::{self, BufReader, Write}, + io::{BufReader, Read}, path::{Path, PathBuf}, sync::{Arc, LazyLock, Mutex}, time::UNIX_EPOCH, @@ -262,23 +261,27 @@ fn import_file( match filename.as_ref() { "poster.jpeg" | "poster.webp" | "poster.png" => { info!("import poster at {path:?}"); - let path = cache_file("picture-file", path, |mut f| { - io::copy(&mut File::open(path)?, &mut f)?; - Ok(()) + let key = CacheKey::new_image(path); + cache(key, || { + let mut data = Vec::new(); + File::open(path)?.read_to_end(&mut data)?; + Ok(data) })?; db.update_node_init(parent, |node| { - node.pictures.insert(PictureSlot::Poster, Asset(path.0)); + node.pictures.insert(PictureSlot::Cover, Picture(key.0)); Ok(()) })?; } "backdrop.jpeg" | "backdrop.webp" | "backdrop.png" => { info!("import backdrop at {path:?}"); - let path = cache_file("picture-file", path, |mut f| { - io::copy(&mut File::open(path)?, &mut f)?; - Ok(()) + let key = CacheKey::new_image(path); + cache(key, || { + let mut data = Vec::new(); + File::open(path)?.read_to_end(&mut data)?; + Ok(data) })?; db.update_node_init(parent, |node| { - node.pictures.insert(PictureSlot::Backdrop, Asset(path.0)); + node.pictures.insert(PictureSlot::Backdrop, Picture(key.0)); Ok(()) })?; } @@ -353,7 +356,7 @@ fn import_file( } pub fn read_media_metadata(path: &Path) -> Result<Arc<matroska::Segment>> { - cache_memory("mkmeta-v4", path, move || { + cache_memory(CacheKey::new_json(path), move || { let media = File::open(path)?; let mut media = create_demuxer_autodetect(Box::new(media))?.ok_or(anyhow!("media format unknown"))?; @@ -400,9 +403,8 @@ fn import_media_file( .flat_map(|a| &a.files) .find(|a| a.name.starts_with("cover") && a.media_type.starts_with("image/")) .map(|att| { - cache_file("att-cover-v2", path, move |mut file| { - file.write_all(&att.data)?; - Ok(()) + cache_store(CacheKey::new_image(("cover", path)), || { + Ok(att.data.clone()) }) }) .transpose()?; @@ -488,13 +490,13 @@ fn import_media_file( } if iflags.use_acoustid { - let fp = rthandle.block_on(acoustid_fingerprint(path))?; - if let Some((atid, mbid)) = rthandle.block_on( - apis.acoustid - .as_ref() - .ok_or(anyhow!("need acoustid"))? - .get_atid_mbid(&fp), - )? { + let fp = acoustid_fingerprint(path)?; + if let Some((atid, mbid)) = apis + .acoustid + .as_ref() + .ok_or(anyhow!("need acoustid"))? + .get_atid_mbid(&fp, rthandle)? + { eids.insert(IdentifierType::AcoustIdTrack, atid); eids.insert(IdentifierType::MusicbrainzRecording, mbid); }; @@ -517,7 +519,7 @@ fn import_media_file( node.identifiers.extend(eids); if let Some(cover) = cover { - node.pictures.insert(PictureSlot::Cover, Asset(cover.0)); + node.pictures.insert(PictureSlot::Cover, Picture(cover.0)); } if let Some(ct) = tags.get("CONTENT_TYPE") { @@ -664,18 +666,17 @@ fn import_media_file( } if let Some(trakt_id) = trakt_id { let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?; - let seasons = rthandle.block_on(trakt.show_seasons(trakt_id))?; + let seasons = trakt.show_seasons(trakt_id, rthandle)?; if seasons.iter().any(|x| x.number == season) { - let episodes = rthandle.block_on(trakt.show_season_episodes(trakt_id, season))?; + let episodes = trakt.show_season_episodes(trakt_id, season, rthandle)?; let mut poster = None; if let Some(tmdb) = &apis.tmdb { - let trakt_details = - rthandle.block_on(trakt.lookup(TraktKind::Show, trakt_id))?; + let trakt_details = trakt.lookup(TraktKind::Show, trakt_id, rthandle)?; if let Some(tmdb_id) = trakt_details.ids.tmdb { let tmdb_details = - rthandle.block_on(tmdb.episode_details(tmdb_id, season, episode))?; + tmdb.episode_details(tmdb_id, season, episode, rthandle)?; if let Some(still) = &tmdb_details.still_path { - poster = Some(Asset(rthandle.block_on(tmdb.image(still))?.0)) + poster = Some(Picture(tmdb.image(still, rthandle)?.0)) } } } @@ -685,7 +686,7 @@ fn import_media_file( node.index = Some(episode.number); node.title = Some(episode.title.clone()); if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Poster, poster); + node.pictures.insert(PictureSlot::Cover, poster); } node.description = episode.overview.clone().or(node.description.clone()); node.ratings.insert(RatingType::Trakt, episode.rating); @@ -768,7 +769,7 @@ fn apply_musicbrainz_recording( node: NodeID, mbid: String, ) -> Result<()> { - let rec = rthandle.block_on(apis.musicbrainz.lookup_recording(mbid))?; + let rec = apis.musicbrainz.lookup_recording(mbid, rthandle)?; db.update_node_init(node, |node| { node.title = Some(rec.title.clone()); @@ -800,8 +801,9 @@ fn apply_musicbrainz_recording( 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 artist = apis + .musicbrainz + .lookup_artist(artist.id.clone(), rthandle)?; let mut image_1 = None; let mut image_2 = None; @@ -811,13 +813,13 @@ fn apply_musicbrainz_recording( 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()))? + if let Some(filename) = + apis.wikidata.query_image_path(id.to_owned(), rthandle)? { - let path = rthandle.block_on( - apis.wikimedia_commons.image_by_filename(filename), - )?; - image_1 = Some(Asset(path.0)); + let path = apis + .wikimedia_commons + .image_by_filename(filename, rthandle)?; + image_1 = Some(Picture(path.0)); } } } @@ -825,10 +827,8 @@ fn apply_musicbrainz_recording( let url = rel.url.as_ref().unwrap().resource.clone(); if let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { let id = id.parse::<u64>().context("parse vgmdb id")?; - if let Some(path) = - rthandle.block_on(apis.vgmdb.get_artist_image(id))? - { - image_2 = Some(Asset(path.0)); + if let Some(path) = apis.vgmdb.get_artist_image(id, rthandle)? { + image_2 = Some(Picture(path.0)); } } } @@ -843,10 +843,9 @@ fn apply_musicbrainz_recording( let headshot = match image_1.or(image_2) { Some(x) => x, - None => Asset( - cache_file("person-headshot-fallback", &artist.sort_name, |mut file| { - generate_fallback(&artist.sort_name, &mut file)?; - Ok(()) + None => Picture( + cache_store(CacheKey::new_image(("fallback", &artist.sort_name)), || { + generate_fallback(&artist.sort_name) })? .0, ), @@ -879,12 +878,8 @@ fn apply_trakt_tmdb( ) -> Result<()> { let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?; if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) { - let data = rthandle - .block_on(trakt.lookup(trakt_kind, trakt_id)) - .context("trakt lookup")?; - let people = rthandle - .block_on(trakt.people(trakt_kind, trakt_id)) - .context("trakt people lookup")?; + let data = trakt.lookup(trakt_kind, trakt_id, rthandle)?; + let people = trakt.people(trakt_kind, trakt_id, rthandle)?; let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new(); for p in people.cast.iter() { @@ -903,29 +898,24 @@ fn apply_trakt_tmdb( let mut backdrop = None; let mut poster = None; if let Some(tmdb_id) = data.ids.tmdb { - let data = rthandle - .block_on(tmdb.details( - match trakt_kind { - TraktKind::Movie => TmdbKind::Movie, - TraktKind::Show => TmdbKind::Tv, - _ => TmdbKind::Movie, - }, - tmdb_id, - )) - .context("tmdb details")?; + let data = tmdb.details( + match trakt_kind { + TraktKind::Movie => TmdbKind::Movie, + TraktKind::Show => TmdbKind::Tv, + _ => TmdbKind::Movie, + }, + tmdb_id, + rthandle, + )?; tmdb_data = Some(data.clone()); if let Some(path) = &data.backdrop_path { - let im = rthandle - .block_on(tmdb.image(path)) - .context("tmdb backdrop image")?; - backdrop = Some(Asset(im.0)); + let im = tmdb.image(path, rthandle).context("tmdb backdrop image")?; + backdrop = Some(Picture(im.0)); } if let Some(path) = &data.poster_path { - let im = rthandle - .block_on(tmdb.image(path)) - .context("tmdb poster image")?; - poster = Some(Asset(im.0)); + let im = tmdb.image(path, rthandle).context("tmdb poster image")?; + poster = Some(Picture(im.0)); } // for p in people_map.values_mut().flatten() { @@ -960,7 +950,7 @@ fn apply_trakt_tmdb( node.ratings.insert(RatingType::Trakt, *rating); } if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Poster, poster); + node.pictures.insert(PictureSlot::Cover, poster); } if let Some(backdrop) = backdrop { node.pictures.insert(PictureSlot::Backdrop, backdrop); diff --git a/import/src/musicbrainz.rs b/import/src/musicbrainz.rs index 934a9e8..92df703 100644 --- a/import/src/musicbrainz.rs +++ b/import/src/musicbrainz.rs @@ -6,7 +6,7 @@ use crate::USER_AGENT; use anyhow::{Context, Result}; -use jellycache::async_cache_memory; +use jellycache::{cache_memory, CacheContentType, CacheKey}; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, @@ -15,6 +15,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, sync::Arc, time::Duration}; use tokio::{ + runtime::Handle, sync::Semaphore, time::{sleep_until, Instant}, }; @@ -221,95 +222,108 @@ impl MusicBrainz { } } - pub async fn lookup_recording(&self, id: String) -> Result<Arc<MbRecordingRel>> { - 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(Self::MAX_PAR_REQ as u64); - info!("recording lookup: {id}"); + pub fn lookup_recording(&self, id: String, rt: &Handle) -> Result<Arc<MbRecordingRel>> { + cache_memory( + CacheKey::new( + CacheContentType::Json, + ("musicbrainz-recording-lookup", &id), + ), + move || { + rt.block_on(async { + let _permit = self.rate_limit.clone().acquire_owned().await?; + let permit_drop_ts = + Instant::now() + Duration::from_secs(Self::MAX_PAR_REQ as u64); + info!("recording lookup: {id}"); - let inc = [ - "isrcs", - "artists", - "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 inc = [ + "isrcs", + "artists", + "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::<MbRecordingRel>() - .await?; + let resp = self + .client + .get(format!( + "https://musicbrainz.org/ws/2/recording/{id}?inc={inc}" + )) + .send() + .await? + .error_for_status()? + .json::<MbRecordingRel>() + .await?; - tokio::task::spawn(async move { - sleep_until(permit_drop_ts).await; - drop(_permit); - }); + tokio::task::spawn(async move { + sleep_until(permit_drop_ts).await; + drop(_permit); + }); - Ok(resp) - }) - .await + Ok(resp) + }) + }, + ) .context("musicbrainz recording lookup") } - pub async fn lookup_artist(&self, id: String) -> Result<Arc<MbArtistRel>> { - 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(Self::MAX_PAR_REQ as u64); - info!("artist lookup: {id}"); + pub fn lookup_artist(&self, id: String, rt: &Handle) -> Result<Arc<MbArtistRel>> { + cache_memory( + CacheKey::new(CacheContentType::Json, ("musicbrainz-artist-lookup", &id)), + move || { + rt.block_on(async { + let _permit = self.rate_limit.clone().acquire_owned().await?; + let permit_drop_ts = + Instant::now() + Duration::from_secs(Self::MAX_PAR_REQ as u64); + 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 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::<MbArtistRel>() - .await?; + let resp = self + .client + .get(format!( + "https://musicbrainz.org/ws/2/artist/{id}?inc={inc}" + )) + .send() + .await? + .error_for_status()? + .json::<MbArtistRel>() + .await?; - tokio::task::spawn(async move { - sleep_until(permit_drop_ts).await; - drop(_permit); - }); + tokio::task::spawn(async move { + sleep_until(permit_drop_ts).await; + drop(_permit); + }); - Ok(resp) - }) - .await + Ok(resp) + }) + }, + ) .context("musicbrainz artist lookup") } } diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs index 414058f..d4a0c25 100644 --- a/import/src/tmdb.rs +++ b/import/src/tmdb.rs @@ -4,8 +4,8 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::USER_AGENT; -use anyhow::{anyhow, bail, Context}; -use jellycache::{async_cache_file, async_cache_memory, CachePath}; +use anyhow::{anyhow, bail, Context, Result}; +use jellycache::{cache_memory, cache_store, CacheKey}; use jellycommon::chrono::{format::Parsed, Utc}; use log::info; use reqwest::{ @@ -14,7 +14,7 @@ use reqwest::{ }; use serde::{Deserialize, Serialize}; use std::{fmt::Display, sync::Arc}; -use tokio::io::AsyncWriteExt; +use tokio::runtime::Handle; pub struct Tmdb { client: Client, @@ -44,100 +44,109 @@ impl Tmdb { key: api_key.to_owned(), } } - pub async fn search(&self, kind: TmdbKind, query: &str) -> anyhow::Result<Arc<TmdbQuery>> { - async_cache_memory("api-tmdb-search", (kind, query), || async move { - info!("searching tmdb: {query:?}"); - Ok(self - .client - .get(format!( - "https://api.themoviedb.org/3/search/{kind}?query={}?api_key={}", - query.replace(" ", "+"), - self.key - )) - .send() - .await? - .error_for_status()? - .json::<TmdbQuery>() - .await?) - }) - .await + pub fn search(&self, kind: TmdbKind, query: &str, rt: &Handle) -> Result<Arc<TmdbQuery>> { + cache_memory( + CacheKey::new_json(("api-tmdb-search", kind, query)), + move || { + rt.block_on(async { + info!("searching tmdb: {query:?}"); + Ok(self + .client + .get(format!( + "https://api.themoviedb.org/3/search/{kind}?query={}?api_key={}", + query.replace(" ", "+"), + self.key + )) + .send() + .await? + .error_for_status()? + .json::<TmdbQuery>() + .await?) + }) + }, + ) .context("tmdb search") } - pub async fn details(&self, kind: TmdbKind, id: u64) -> anyhow::Result<Arc<TmdbDetails>> { - async_cache_memory("api-tmdb-details", (kind, id), || async move { - info!("fetching details: {id:?}"); - Ok(self - .client - .get(format!( - "https://api.themoviedb.org/3/{kind}/{id}?api_key={}", - self.key, - )) - .send() - .await? - .error_for_status()? - .json() - .await?) + pub fn details(&self, kind: TmdbKind, id: u64, rt: &Handle) -> Result<Arc<TmdbDetails>> { + cache_memory(CacheKey::new_json(("tmdb-details", kind, id)), move || { + rt.block_on(async { + info!("fetching details: {id:?}"); + Ok(self + .client + .get(format!( + "https://api.themoviedb.org/3/{kind}/{id}?api_key={}", + self.key, + )) + .send() + .await? + .error_for_status()? + .json() + .await?) + }) }) - .await .context("tmdb details") } - pub async fn person_image(&self, id: u64) -> anyhow::Result<Arc<TmdbPersonImage>> { - async_cache_memory("api-tmdb-search", id, || async move { - Ok(self - .client - .get(format!( - "https://api.themoviedb.org/3/person/{id}/images?api_key={}", - self.key, - )) - .send() - .await? - .error_for_status()? - .json() - .await?) + pub fn person_image(&self, id: u64, rt: &Handle) -> Result<Arc<TmdbPersonImage>> { + cache_memory(CacheKey::new_image(("tmdb-person-image", id)), move || { + rt.block_on(async { + Ok(self + .client + .get(format!( + "https://api.themoviedb.org/3/person/{id}/images?api_key={}", + self.key, + )) + .send() + .await? + .error_for_status()? + .json() + .await?) + }) }) - .await .context("tmdb person images") } - pub async fn image(&self, path: &str) -> anyhow::Result<CachePath> { - async_cache_file("api-tmdb-image", path, |mut file| async move { - info!("downloading image {path:?}"); - let mut res = self - .image_client - .get(format!("https://image.tmdb.org/t/p/original{path}")) - .send() - .await? - .error_for_status()?; - while let Some(chunk) = res.chunk().await? { - file.write_all(&chunk).await?; - } - Ok(()) + pub fn image(&self, path: &str, rt: &Handle) -> Result<CacheKey> { + cache_store(CacheKey::new_image(("tmdb-image", path)), move || { + rt.block_on(async { + info!("downloading image {path:?}"); + Ok(self + .image_client + .get(format!("https://image.tmdb.org/t/p/original{path}")) + .send() + .await? + .error_for_status()? + .bytes() + .await? + .to_vec()) + }) }) - .await .context("tmdb image download") } - pub async fn episode_details( + pub fn episode_details( &self, series_id: u64, season: usize, episode: usize, - ) -> anyhow::Result<Arc<TmdbEpisode>> { - async_cache_memory("api-tmdb-episode-details", (series_id,season,episode), || async move { - info!("tmdb episode details {series_id} S={season} E={episode}"); - Ok(self - .image_client - .get(format!("https://api.themoviedb.org/3/tv/{series_id}/season/{season}/episode/{episode}?api_key={}", self.key)) - .send() - .await? - .error_for_status()? - .json() - .await?) + rt: &Handle, + ) -> Result<Arc<TmdbEpisode>> { + cache_memory(CacheKey::new_json(("tmdb-episode-details", series_id, season, episode)), move || { + rt.block_on(async { + info!("tmdb episode details {series_id} S={season} E={episode}"); + Ok(self + .image_client + .get(format!("https://api.themoviedb.org/3/tv/{series_id}/season/{season}/episode/{episode}?api_key={}", self.key)) + .send() + .await? + .error_for_status()? + .json() + .await?) + }) }) - .await.context("tmdb episode details") + .context("tmdb episode details") } } -pub fn parse_release_date(d: &str) -> anyhow::Result<Option<i64>> { +pub fn parse_release_date(d: &str) -> Result<Option<i64>> { if d.is_empty() { return Ok(None); } else if d.len() < 10 { diff --git a/import/src/trakt.rs b/import/src/trakt.rs index 3f0ad47..1640ca5 100644 --- a/import/src/trakt.rs +++ b/import/src/trakt.rs @@ -5,7 +5,7 @@ */ use crate::USER_AGENT; use anyhow::Context; -use jellycache::async_cache_memory; +use jellycache::{cache_memory, CacheKey}; use jellycommon::{Appearance, CreditCategory, NodeID}; use log::info; use reqwest::{ @@ -14,6 +14,7 @@ use reqwest::{ }; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display, sync::Arc}; +use tokio::runtime::Handle; pub struct Trakt { client: Client, @@ -45,76 +46,97 @@ impl Trakt { Self { client } } - pub async fn search( + pub fn search( &self, kinds: &[TraktKind], query: &str, + rt: &Handle, ) -> anyhow::Result<Arc<Vec<TraktSearchResult>>> { - async_cache_memory("api-trakt-lookup", (kinds, query), || async move { - let url = format!( - "https://api.trakt.tv/search/{}?query={}&extended=full", - kinds - .iter() - .map(|t| t.singular()) - .collect::<Vec<_>>() - .join(","), - urlencoding::encode(query), - ); - let res = self.client.get(url).send().await?.error_for_status()?; - Ok(res.json().await?) + cache_memory(CacheKey::new_json(("trakt-lookup", query)), move || { + rt.block_on(async { + let url = format!( + "https://api.trakt.tv/search/{}?query={}&extended=full", + kinds + .iter() + .map(|t| t.singular()) + .collect::<Vec<_>>() + .join(","), + urlencoding::encode(query), + ); + let res = self.client.get(url).send().await?.error_for_status()?; + Ok(res.json().await?) + }) }) - .await .context("trakt search") } - pub async fn lookup(&self, kind: TraktKind, id: u64) -> anyhow::Result<Arc<TraktMediaObject>> { - async_cache_memory("api-trakt-lookup", (kind, id), || async move { - info!("trakt lookup {kind:?}:{id:?}"); - let url = format!("https://api.trakt.tv/{}/{id}?extended=full", kind.plural()); - let res = self.client.get(url).send().await?.error_for_status()?; - Ok(res.json().await?) + pub fn lookup( + &self, + kind: TraktKind, + id: u64, + rt: &Handle, + ) -> anyhow::Result<Arc<TraktMediaObject>> { + cache_memory(CacheKey::new_json(("trakt-lookup", kind, id)), move || { + rt.block_on(async { + info!("trakt lookup {kind:?}:{id:?}"); + let url = format!("https://api.trakt.tv/{}/{id}?extended=full", kind.plural()); + let res = self.client.get(url).send().await?.error_for_status()?; + Ok(res.json().await?) + }) }) - .await .context("trakt lookup") } - pub async fn people(&self, kind: TraktKind, id: u64) -> anyhow::Result<Arc<TraktPeople>> { - async_cache_memory("api-trakt-people", (kind, id), || async move { - info!("trakt people {kind:?}:{id:?}"); - let url = format!( - "https://api.trakt.tv/{}/{id}/people?extended=full", - kind.plural() - ); - let res = self.client.get(url).send().await?.error_for_status()?; - Ok(res.json().await?) + pub fn people( + &self, + kind: TraktKind, + id: u64, + rt: &Handle, + ) -> anyhow::Result<Arc<TraktPeople>> { + cache_memory(CacheKey::new_json(("trakt-people", kind, id)), move || { + rt.block_on(async { + info!("trakt people {kind:?}:{id:?}"); + let url = format!( + "https://api.trakt.tv/{}/{id}/people?extended=full", + kind.plural() + ); + let res = self.client.get(url).send().await?.error_for_status()?; + Ok(res.json().await?) + }) }) - .await .context("trakt people") } - pub async fn show_seasons(&self, id: u64) -> anyhow::Result<Arc<Vec<TraktSeason>>> { - async_cache_memory("api-trakt-seasons", id, || async move { - info!("trakt seasons {id:?}"); - let url = format!("https://api.trakt.tv/shows/{id}/seasons?extended=full"); - let res = self.client.get(url).send().await?.error_for_status()?; - Ok(res.json().await?) + pub fn show_seasons(&self, id: u64, rt: &Handle) -> anyhow::Result<Arc<Vec<TraktSeason>>> { + cache_memory(CacheKey::new_json(("trakt-seasons", id)), move || { + rt.block_on(async { + info!("trakt seasons {id:?}"); + let url = format!("https://api.trakt.tv/shows/{id}/seasons?extended=full"); + let res = self.client.get(url).send().await?.error_for_status()?; + Ok(res.json().await?) + }) }) - .await .context("trakt show seasons") } - pub async fn show_season_episodes( + pub fn show_season_episodes( &self, id: u64, season: usize, + rt: &Handle, ) -> anyhow::Result<Arc<Vec<TraktEpisode>>> { - async_cache_memory("api-trakt-episodes", (id, season), || async move { - info!("trakt episodes {id:?} season={season}"); - let url = format!("https://api.trakt.tv/shows/{id}/seasons/{season}?extended=full"); - let res = self.client.get(url).send().await?.error_for_status()?; - Ok(res.json().await?) - }) - .await + cache_memory( + CacheKey::new_json(("trakt-episodes", id, season)), + move || { + rt.block_on(async { + info!("trakt episodes {id:?} season={season}"); + let url = + format!("https://api.trakt.tv/shows/{id}/seasons/{season}?extended=full"); + let res = self.client.get(url).send().await?.error_for_status()?; + Ok(res.json().await?) + }) + }, + ) .context("trakt show season episodes") } } diff --git a/import/src/vgmdb.rs b/import/src/vgmdb.rs index 20a0038..4e37ba3 100644 --- a/import/src/vgmdb.rs +++ b/import/src/vgmdb.rs @@ -6,7 +6,7 @@ use crate::USER_AGENT; use anyhow::{Context, Result}; -use jellycache::{async_cache_file, async_cache_memory, CachePath}; +use jellycache::{cache_memory, cache_store, CacheContentType, CacheKey}; use log::info; use regex::Regex; use reqwest::{ @@ -18,7 +18,7 @@ use std::{ time::Duration, }; use tokio::{ - io::AsyncWriteExt, + runtime::Handle, sync::Semaphore, time::{sleep_until, Instant}, }; @@ -59,18 +59,26 @@ impl Vgmdb { } } - pub async fn get_artist_image(&self, id: u64) -> Result<Option<CachePath>> { - if let Some(url) = self.get_artist_image_url(id).await? { + pub fn get_artist_image(&self, id: u64, rt: &Handle) -> Result<Option<CacheKey>> { + if let Some(url) = self.get_artist_image_url(id, rt)? { Ok(Some( - async_cache_file("api-vgmdb-media", url.clone(), |mut file| async move { - info!("downloading image {url:?}"); - let mut res = self.client.get(url).send().await?.error_for_status()?; - while let Some(chunk) = res.chunk().await? { - file.write_all(&chunk).await?; - } - Ok(()) - }) - .await + cache_store( + CacheKey::new_image(("vgmdb-artist-media", &url)), + move || { + rt.block_on(async { + info!("downloading image {url:?}"); + Ok(self + .client + .get(url) + .send() + .await? + .error_for_status()? + .bytes() + .await? + .to_vec()) + }) + }, + ) .context("vgmdb media download")?, )) } else { @@ -78,9 +86,9 @@ impl Vgmdb { } } - pub async fn get_artist_image_url(&self, id: u64) -> Result<Option<String>> { - let html = self.scrape_artist_page(id).await?; - if let Some(cap) = RE_IMAGE_URL_FROM_HTML.captures(&html) { + pub fn get_artist_image_url(&self, id: u64, rt: &Handle) -> Result<Option<String>> { + let html = self.scrape_artist_page(id, rt)?; + if let Some(cap) = RE_IMAGE_URL_FROM_HTML.captures(&str::from_utf8(&html).unwrap()) { if let Some(url) = cap.name("url").map(|m| m.as_str()) { return Ok(Some(url.to_string())); } @@ -88,29 +96,34 @@ impl Vgmdb { Ok(None) } - pub async fn scrape_artist_page(&self, id: u64) -> Result<Arc<String>> { - async_cache_memory("api-vgmdb-artist", id, || async move { - let _permit = self.rate_limit.clone().acquire_owned().await?; - let permit_drop_ts = Instant::now() + Duration::from_secs(1); - info!("scrape artist: {id}"); + pub fn scrape_artist_page(&self, id: u64, rt: &Handle) -> Result<Arc<Vec<u8>>> { + cache_memory( + CacheKey::new(CacheContentType::Unknown, ("vgmdb-artist-page", id)), + move || { + rt.block_on(async { + let _permit = self.rate_limit.clone().acquire_owned().await?; + let permit_drop_ts = Instant::now() + Duration::from_secs(1); + info!("scrape artist: {id}"); - let resp = self - .client - .get(format!("https://vgmdb.net/artist/{id}")) - .send() - .await? - .error_for_status()? - .text() - .await?; + let resp = self + .client + .get(format!("https://vgmdb.net/artist/{id}")) + .send() + .await? + .error_for_status()? + .bytes() + .await? + .to_vec(); - tokio::task::spawn(async move { - sleep_until(permit_drop_ts).await; - drop(_permit); - }); + tokio::task::spawn(async move { + sleep_until(permit_drop_ts).await; + drop(_permit); + }); - Ok(resp) - }) - .await + Ok(resp) + }) + }, + ) .context("vgmdb artist page scrape") } } diff --git a/import/src/wikidata.rs b/import/src/wikidata.rs index 71eef9a..40077b9 100644 --- a/import/src/wikidata.rs +++ b/import/src/wikidata.rs @@ -6,26 +6,27 @@ use crate::USER_AGENT; use anyhow::{bail, Context, Result}; -use jellycache::async_cache_memory; +use jellycache::{cache_memory, CacheKey}; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, sync::Arc}; +use tokio::runtime::Handle; pub struct Wikidata { client: Client, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct WikidataResponse { entities: BTreeMap<String, WikidataEntity>, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct WikidataEntity { pub pageid: u64, pub ns: u64, @@ -36,7 +37,7 @@ pub struct WikidataEntity { pub id: String, pub claims: BTreeMap<String, Vec<WikidataClaim>>, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct WikidataClaim { pub r#type: String, pub id: String, @@ -44,7 +45,7 @@ pub struct WikidataClaim { pub mainsnak: WikidataSnak, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct WikidataSnak { pub snaktype: String, pub property: String, @@ -53,7 +54,7 @@ pub struct WikidataSnak { pub datatype: String, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct WikidataValue { pub value: Value, pub r#type: String, @@ -87,8 +88,8 @@ impl Wikidata { Self { client } } - pub async fn query_image_path(&self, id: String) -> Result<Option<String>> { - let response = self.query(id.clone()).await?; + pub fn query_image_path(&self, id: String, rt: &Handle) -> Result<Option<String>> { + let response = self.query(id.clone(), rt)?; if let Some(entity) = response.entities.get(&id) { if let Some(images) = entity.claims.get(properties::IMAGE) { for image in images { @@ -106,24 +107,20 @@ impl Wikidata { Ok(None) } - pub async fn query(&self, id: String) -> Result<WikidataResponse> { - 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) + pub fn query(&self, id: String, rt: &Handle) -> Result<Arc<WikidataResponse>> { + cache_memory(CacheKey::new_json(("wikidata", &id)), move || { + rt.block_on(async { + info!("entity query: {id}"); + Ok(self + .client + .get(format!("https://www.wikidata.org/entity/{id}")) + .send() + .await? + .error_for_status()? + .json() + .await?) + }) }) - .await - .context("wikidata entity")?; - - serde_json::from_str(&json).context("parse wikidata entity") + .context("wikidata entity") } } diff --git a/import/src/wikimedia_commons.rs b/import/src/wikimedia_commons.rs index f8d8f53..0d716f0 100644 --- a/import/src/wikimedia_commons.rs +++ b/import/src/wikimedia_commons.rs @@ -6,13 +6,13 @@ use crate::USER_AGENT; use anyhow::{Context, Result}; -use jellycache::{async_cache_file, CachePath}; +use jellycache::{cache_store, CacheKey}; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, redirect::Policy, Client, ClientBuilder, }; -use tokio::io::AsyncWriteExt; +use tokio::runtime::Handle; pub struct WikimediaCommons { client: Client, @@ -36,28 +36,26 @@ impl WikimediaCommons { Self { client } } - pub async fn image_by_filename(&self, filename: String) -> Result<CachePath> { - async_cache_file( - "api-wikimedia-commons-image", - filename.clone(), - |mut file| async move { - let mut res = self - .client - .get(format!( - "https://commons.wikimedia.org/wiki/Special:FilePath/{}", - filename.replace(" ", "_") - )) - .send() - .await? - .error_for_status()?; - - while let Some(chunk) = res.chunk().await? { - file.write_all(&chunk).await?; - } - Ok(()) + pub fn image_by_filename(&self, filename: String, rt: &Handle) -> Result<CacheKey> { + cache_store( + CacheKey::new_image(("wikimedia-commons-image", &filename)), + move || { + rt.block_on(async { + Ok(self + .client + .get(format!( + "https://commons.wikimedia.org/wiki/Special:FilePath/{}", + filename.replace(" ", "_") + )) + .send() + .await? + .error_for_status()? + .bytes() + .await? + .to_vec()) + }) }, ) - .await .context("mediawiki image by filename") } } |