/* 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) 2024 metamuffin */ use anyhow::{anyhow, bail, Context}; use bincode::{Decode, Encode}; use jellybase::cache::{async_cache_file, async_cache_memory, CachePath}; use jellycommon::{ chrono::{format::Parsed, Utc}, TmdbKind, }; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; use serde::Deserialize; use std::sync::Arc; use tokio::io::AsyncWriteExt; 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"), )])) .build() .unwrap(); let image_client = ClientBuilder::new().build().unwrap(); Self { client, image_client, key: api_key.to_owned(), } } pub async fn search(&self, kind: TmdbKind, query: &str) -> anyhow::Result> { async_cache_memory( &["api-tmdb-search", query, &format!("{kind}")], || async move { info!("searching tmdb: {query:?}"); Ok(self .client .get(&format!( "https://api.themoviedb.org/3/search/{kind}?query={}?api_key={}", query.replace(" ", "+"), // TODO this is horrible, please find a proper urlencoding library that supports + for space self.key )) .send() .await? .error_for_status()? .json::() .await?) }, ) .await } pub async fn details(&self, kind: TmdbKind, id: u64) -> anyhow::Result> { async_cache_memory( &["api-tmdb-details", &format!("{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?) }, ) .await } pub async fn person_image(&self, id: u64) -> anyhow::Result> { async_cache_memory(&["api-tmdb-search", &format!("{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?) }) .await } pub async fn image(&self, path: &str) -> anyhow::Result { 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(()) }) .await } } pub fn parse_release_date(d: &str) -> anyhow::Result> { 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())) } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbPersonImage { pub id: u64, pub profiles: Vec, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbPersonImageProfile { pub aspect_ratio: f64, pub height: u32, pub width: u32, pub file_path: String, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbQuery { pub page: usize, pub results: Vec, pub total_pages: usize, pub total_results: usize, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbQueryResult { pub adult: bool, pub backdrop_path: Option, pub genre_ids: Vec, pub id: u64, pub original_language: Option, pub original_title: Option, pub overview: String, pub popularity: f64, pub poster_path: Option, pub release_date: Option, pub title: Option, pub name: Option, pub vote_average: f64, pub vote_count: usize, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbDetails { pub adult: bool, pub backdrop_path: Option, pub genres: Vec, pub id: u64, pub original_language: Option, pub original_title: Option, pub overview: String, pub popularity: f64, pub poster_path: Option, pub release_date: Option, pub title: Option, pub name: Option, pub vote_average: f64, pub vote_count: usize, pub budget: Option, pub homepage: Option, pub imdb_id: Option, pub production_companies: Vec, pub revenue: Option, pub tagline: Option, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbGenre { pub id: u64, pub name: String, } #[derive(Debug, Clone, Deserialize, Encode, Decode)] pub struct TmdbProductionCompany { pub id: u64, pub name: String, pub logo_path: Option, }