aboutsummaryrefslogtreecommitdiff
path: root/import/src/plugins/omdb.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-19 18:06:21 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-19 18:06:21 +0100
commit962309ddcb033e0032258d6badebb90415a34e3d (patch)
tree64a0bde3c6ddfefa334c71314d699b9a00fad0bf /import/src/plugins/omdb.rs
parentc545bdbc10ae5a55f991e03260e6a74b92a75fda (diff)
downloadjellything-962309ddcb033e0032258d6badebb90415a34e3d.tar
jellything-962309ddcb033e0032258d6badebb90415a34e3d.tar.bz2
jellything-962309ddcb033e0032258d6badebb90415a34e3d.tar.zst
omdb import plugin
Diffstat (limited to 'import/src/plugins/omdb.rs')
-rw-r--r--import/src/plugins/omdb.rs170
1 files changed, 170 insertions, 0 deletions
diff --git a/import/src/plugins/omdb.rs b/import/src/plugins/omdb.rs
new file mode 100644
index 0000000..bb58c6b
--- /dev/null
+++ b/import/src/plugins/omdb.rs
@@ -0,0 +1,170 @@
+/*
+ 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) 2026 metamuffin <metamuffin.org>
+*/
+
+use std::sync::Arc;
+
+use anyhow::{Context, Result, anyhow};
+use jellycache::Cache;
+use jellycommon::{
+ IDENT_IMDB, NO_IDENTIFIERS, NO_RATINGS, RTYP_IMDB, RTYP_METACRITIC, RTYP_ROTTEN_TOMATOES,
+};
+use jellydb::RowNum;
+use log::info;
+use reqwest::{
+ Client, ClientBuilder,
+ header::{HeaderMap, HeaderName, HeaderValue},
+};
+use serde::{Deserialize, Serialize};
+use tokio::runtime::Handle;
+
+use crate::{
+ USER_AGENT,
+ plugins::{ImportPlugin, PluginContext, PluginInfo},
+};
+
+pub struct Omdb {
+ client: Client,
+ key: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct OmdbMovie {
+ title: String,
+ year: String,
+ rated: String,
+ released: String,
+ runtime: String,
+ director: String,
+ writer: String,
+ actors: String,
+ plot: String,
+ language: String,
+ country: String,
+ awards: String,
+ poster: String,
+ ratings: Vec<OmdbRating>,
+ metascore: String,
+ #[serde(rename = "imdbRating")]
+ imdb_rating: String,
+ #[serde(rename = "imdbVotes")]
+ imdb_votes: String,
+ #[serde(rename = "imdbID")]
+ imdb_id: String,
+ r#type: String,
+ #[serde(rename = "DVD")]
+ dvd: Option<String>,
+ box_office: Option<String>,
+ production: Option<String>,
+ website: Option<String>,
+ response: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct OmdbRating {
+ source: String,
+ value: String,
+}
+
+impl Omdb {
+ pub fn new(api_key: &str) -> 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,
+ key: api_key.to_owned(),
+ }
+ }
+ fn lookup(&self, cache: &Cache, id: &str, rt: &Handle) -> Result<Arc<OmdbMovie>> {
+ cache
+ .cache_memory(&format!("ext/omdb/movie/{id}.json"), move || {
+ rt.block_on(async {
+ info!("lookup {id}");
+ let res = self
+ .client
+ .get(format!(
+ "http://www.omdbapi.com/?apikey={}&i={id}",
+ self.key
+ ))
+ .send()
+ .await?
+ .error_for_status()?;
+ Ok(res.json().await?)
+ })
+ })
+ .context("omdb lookup")
+ }
+}
+
+impl ImportPlugin for Omdb {
+ fn info(&self) -> PluginInfo {
+ PluginInfo {
+ name: "omdb",
+ handle_process: true,
+ ..Default::default()
+ }
+ }
+ fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> {
+ let data = ct.ic.get_node(node)?.unwrap();
+ let data = data.as_object();
+
+ let Some(id) = data.get(NO_IDENTIFIERS).unwrap_or_default().get(IDENT_IMDB) else {
+ return Ok(());
+ };
+
+ let entry = self.lookup(&ct.ic.cache, id, ct.rt)?;
+
+ let imdb = match entry.imdb_rating.as_str() {
+ "N/A" => None,
+ v => Some(v.parse::<f64>().context("parse imdb rating")?),
+ };
+ let metascore = match entry.metascore.as_str() {
+ "N/A" => None,
+ v => Some(v.parse::<f64>().context("parse metascore rating")?),
+ };
+ let rotten_tomatoes = entry
+ .ratings
+ .iter()
+ .find(|r| r.source == "Rotten Tomatoes")
+ .map(|r| {
+ r.value
+ .strip_suffix("%")
+ .ok_or(anyhow!("% missing"))?
+ .parse::<f64>()
+ .context("parse rotten tomatoes rating")
+ })
+ .transpose()?;
+
+ ct.ic.update_node(node, |mut node| {
+ for (typ, val) in [
+ (RTYP_METACRITIC, metascore),
+ (RTYP_IMDB, imdb),
+ (RTYP_ROTTEN_TOMATOES, rotten_tomatoes),
+ ] {
+ if let Some(x) = val {
+ node = node
+ .as_object()
+ .update(NO_RATINGS, |rts| rts.insert(typ, x));
+ }
+ }
+ node
+ })?;
+
+ Ok(())
+ }
+}