use bincode::{Decode, Encode}; use jellybase::cache::async_cache_memory; use jellycommon::TraktKind; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, ClientBuilder, }; use serde::{Deserialize, Serialize}; use std::{fmt::Display, sync::Arc}; pub struct Trakt { client: Client, } impl Trakt { pub fn new(api_key: &str) -> Self { let client = ClientBuilder::new() .default_headers(HeaderMap::from_iter([ ( HeaderName::from_static("trakt-api-key"), HeaderValue::from_str(api_key).unwrap(), ), ( HeaderName::from_static("trakt-api-version"), HeaderValue::from_static("2"), ), ( HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"), ), ])) .build() .unwrap(); Self { client } } pub async fn search( &self, kinds: &[TraktKind], query: &str, extended: bool, ) -> anyhow::Result>> { async_cache_memory( &["api-trakt-lookup", query, if extended { "a" } else { "b" }], || async move { let url = format!( "https://api.trakt.tv/search/{}?query={}{}", kinds .iter() .map(|t| t.singular()) .collect::>() .join(","), urlencoding::encode(query), optext(extended) ); let res = self.client.get(url).send().await?.error_for_status()?; Ok(res.json().await?) }, ) .await } pub async fn lookup( &self, kind: TraktKind, id: u64, extended: bool, ) -> anyhow::Result> { async_cache_memory( &["api-trakt-lookup", &format!("{id} {extended}")], || async move { let url = format!( "https://api.trakt.tv/{}/{}{}", kind.plural(), id, optext2(extended) ); let res = self.client.get(url).send().await?.error_for_status()?; Ok(res.json().await?) }, ) .await } } fn optext(extended: bool) -> &'static str { if extended { "&extended=full" } else { "" } } fn optext2(extended: bool) -> &'static str { if extended { "?extended=full" } else { "" } } #[derive(Debug, Serialize, Deserialize, Encode, Decode)] pub struct TraktSearchResult { pub r#type: TraktKind, pub score: f64, #[serde(flatten)] pub inner: TraktKindObject, } #[derive(Debug, Serialize, Deserialize, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum TraktKindObject { Movie(TraktMediaObject), Show(TraktMediaObject), Season(TraktMediaObject), Episode(TraktMediaObject), Person(TraktMediaObject), User(TraktMediaObject), } impl TraktKindObject { pub fn inner(&self) -> &TraktMediaObject { match self { TraktKindObject::Movie(x) | TraktKindObject::Show(x) | TraktKindObject::Season(x) | TraktKindObject::Episode(x) | TraktKindObject::Person(x) | TraktKindObject::User(x) => x, } } } #[derive(Debug, Serialize, Deserialize, Encode, Decode, Clone)] pub struct TraktMediaObject { pub title: String, pub year: Option, pub ids: TraktMediaObjectIds, pub tagline: Option, pub overview: Option, pub released: Option, pub runtime: Option, pub country: Option, pub trailer: Option, pub homepage: Option, pub status: Option, pub rating: Option, pub votes: Option, pub comment_count: Option, pub language: Option, pub available_translations: Option>, pub genres: Option>, } #[derive(Debug, Serialize, Deserialize, Encode, Decode, Clone)] pub struct TraktMediaObjectIds { pub trakt: u64, pub slug: Option, pub imdb: Option, pub tmdb: Option, pub omdb: Option, pub tvdb: Option, } impl Display for TraktSearchResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{}: {} ({}) \x1b[2m[{}]\x1b[0m", self.r#type.to_string(), self.inner.inner().title, self.inner.inner().year.unwrap_or(0), self.inner.inner().ids )) } } impl Display for TraktMediaObjectIds { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("trakt")?; if self.slug.is_some() { f.write_str(",slug")?; } if self.tmdb.is_some() { f.write_str(",tmdb")?; } if self.imdb.is_some() { f.write_str(",imdb")?; } if self.tvdb.is_some() { f.write_str(",tvdb")?; } if self.omdb.is_some() { f.write_str(",omdb")?; } Ok(()) } }