diff options
Diffstat (limited to 'import/src/plugins/tmdb.rs')
| -rw-r--r-- | import/src/plugins/tmdb.rs | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/import/src/plugins/tmdb.rs b/import/src/plugins/tmdb.rs new file mode 100644 index 0000000..3d6e832 --- /dev/null +++ b/import/src/plugins/tmdb.rs @@ -0,0 +1,281 @@ +/* + 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 <metamuffin.org> +*/ +use crate::USER_AGENT; +use anyhow::{anyhow, bail, Context, Result}; +use jellycache::{cache_memory, cache_store, EscapeKey, HashKey}; +use jellycommon::{ + chrono::{format::Parsed, Utc}, + Asset, +}; +use log::info; +use reqwest::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Client, ClientBuilder, +}; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, sync::Arc}; +use tokio::runtime::Handle; + +pub struct Tmdb { + client: Client, + image_client: Client, + key: String, +} + +impl Tmdb { + 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(); + let image_client = ClientBuilder::new().build().unwrap(); + Self { + client, + image_client, + key: api_key.to_owned(), + } + } + pub fn search(&self, kind: TmdbKind, query: &str, rt: &Handle) -> Result<Arc<TmdbQuery>> { + cache_memory( + &format!("ext/tmdb/search/{kind}-{}.json", HashKey(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 fn details(&self, kind: TmdbKind, id: u64, rt: &Handle) -> Result<Arc<TmdbDetails>> { + cache_memory(&format!("ext/tmdb/details/{kind}-{id}.json"), 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?) + }) + }) + .context("tmdb details") + } + pub fn person_image(&self, id: u64, rt: &Handle) -> Result<Arc<TmdbPersonImage>> { + cache_memory(&format!("ext/tmdb/person/images/{id}.json"), 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?) + }) + }) + .context("tmdb person images") + } + pub fn image(&self, path: &str, rt: &Handle) -> Result<Asset> { + cache_store( + format!("ext/tmdb/image/{}.image", EscapeKey(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()) + }) + }, + ) + .context("tmdb image download") + .map(Asset) + } + + pub fn episode_details( + &self, + series_id: u64, + season: usize, + episode: usize, + rt: &Handle, + ) -> Result<Arc<TmdbEpisode>> { + cache_memory(&format!("ext/tmdb/episode-details/{series_id}-S{season}-E{episode}.json"), 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?) + }) + }) + .context("tmdb episode details") + } +} + +pub fn parse_release_date(d: &str) -> Result<Option<i64>> { + if d.is_empty() { + return Ok(None); + } else if d.len() < 10 { + bail!(anyhow!("date string too short")) + } + let (year, month, day) = (&d[0..4], &d[5..7], &d[8..10]); + let (year, month, day) = ( + year.parse().context("parsing year")?, + month.parse().context("parsing month")?, + day.parse().context("parsing day")?, + ); + + let mut p = Parsed::new(); + p.year = Some(year); + p.month = Some(month); + p.day = Some(day); + p.hour_div_12 = Some(0); + p.hour_mod_12 = Some(0); + p.minute = Some(0); + p.second = Some(0); + Ok(Some(p.to_datetime_with_timezone(&Utc)?.timestamp_millis())) +} + +impl Display for TmdbKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + TmdbKind::Tv => "tv", + TmdbKind::Movie => "movie", + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbEpisode { + pub air_date: String, + pub overview: String, + pub name: String, + pub id: u64, + pub runtime: f64, + pub still_path: Option<String>, + pub vote_average: f64, + pub vote_count: usize, +} + +#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] +pub enum TmdbKind { + Tv, + Movie, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbPersonImage { + pub id: u64, + pub profiles: Vec<TmdbPersonImageProfile>, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbPersonImageProfile { + pub aspect_ratio: f64, + pub height: u32, + pub width: u32, + pub file_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbQuery { + pub page: usize, + pub results: Vec<TmdbQueryResult>, + pub total_pages: usize, + pub total_results: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbQueryResult { + pub adult: bool, + pub backdrop_path: Option<String>, + pub genre_ids: Vec<u64>, + pub id: u64, + pub original_language: Option<String>, + pub original_title: Option<String>, + pub overview: String, + pub popularity: f64, + pub poster_path: Option<String>, + pub release_date: Option<String>, + pub title: Option<String>, + pub name: Option<String>, + pub vote_average: f64, + pub vote_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbDetails { + pub adult: bool, + pub backdrop_path: Option<String>, + pub genres: Vec<TmdbGenre>, + pub id: u64, + pub original_language: Option<String>, + pub original_title: Option<String>, + pub overview: String, + pub popularity: f64, + pub poster_path: Option<String>, + pub release_date: Option<String>, + pub title: Option<String>, + pub name: Option<String>, + pub vote_average: f64, + pub vote_count: usize, + pub budget: Option<usize>, + pub homepage: Option<String>, + pub imdb_id: Option<String>, + pub production_companies: Vec<TmdbProductionCompany>, + pub revenue: Option<usize>, + pub tagline: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbGenre { + pub id: u64, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TmdbProductionCompany { + pub id: u64, + pub name: String, + pub logo_path: Option<String>, +} |