use reqwest::{header::HeaderMap, Client}; use serde_json::{json, Value}; use tracing::info; use crate::{error::NtitledError, ost::data::DownloadResponse}; use super::data::{Login, LoginResponse, SearchResponse}; const BASE_URL: &str = "https://api.opensubtitles.com/api/v1"; #[derive(Debug, Clone)] pub struct OSClient { client: Client, token: String, } impl OSClient { pub async fn init() -> Self { let api_key = std::env::var("API_KEY").expect("missing API_KEY in env"); let username = std::env::var("USERNAME").expect("missing USERNAME in env"); let password = std::env::var("PASSWORD").expect("missing PASSWORD in env"); let token = std::env::var("JWT").ok(); if let Some(token) = token { info!("Initialising client with token"); OSClient::new(api_key, token) .await .expect("error while loading client") } else { info!("Initialising client with login"); OSClient::new_with_login(api_key, username, password) .await .expect("error while loading client") } } async fn new_with_login( api_key: String, username: String, password: String, ) -> Result { let mut headers = HeaderMap::new(); headers.append("Api-Key", api_key.parse().expect("invalid api_key")); let client = reqwest::ClientBuilder::new() .default_headers(headers) .user_agent("Ntitled v1.0") .build() .expect("invalid client configuration"); let login = Login { username: &username, password: &password, }; let req = client .post(format!("{BASE_URL}/login")) .json(&login) .build()?; let res: LoginResponse = client.execute(req).await?.json().await?; let Some((_, token)) = res.token.split_once(' ') else { return Err(NtitledError::Message(format!( "received invalid token: {}", res.token ))); }; #[cfg(feature = "debug")] { dbg!(&token); use std::fmt::Write; let env_file = std::fs::read_to_string(".env").expect("env file required in debug mode"); let mut new_env = String::new(); let mut jwt_written = false; for line in env_file.lines() { if line.starts_with("JWT") { writeln!(new_env, "JWT = \"{token}\"").unwrap(); jwt_written = true; } else { writeln!(new_env, "{line}").unwrap(); } } if !jwt_written { writeln!(new_env, "JWT = \"{token}\"").unwrap(); } println!("New env:\n{new_env}"); std::fs::write(".env", new_env).expect("error while writing new env"); } Ok(Self { client, token: token.to_string(), }) } async fn new(api_key: String, token: String) -> Result { let mut headers = HeaderMap::new(); headers.append("Api-Key", api_key.parse().expect("invalid api_key")); headers.append( "Authorization", format!("Bearer {token}") .parse() .expect("invalid bearer token"), ); let client = reqwest::ClientBuilder::new() .default_headers(headers) .user_agent("Ntitled v1.0") .build() .expect("invalid client configuration"); Ok(Self { client, token }) } pub async fn search( &self, title: &str, hash: Option<&str>, ) -> Result { let mut req = self .client .get(format!("{BASE_URL}/subtitles")) .query(&[("query", title)]); if let Some(hash) = hash { req = req.query(&[("moviehash", hash)]) } let req = req.build()?; let res: Value = self.client.execute(req).await?.json().await?; #[cfg(feature = "debug")] { std::fs::write("response.json", res.to_string()).unwrap(); } Ok(serde_json::from_value(res)?) } pub async fn download_subtitles( &self, file_id: usize, full_path: &str, ) -> Result<(), NtitledError> { let req = self .client .post(format!("{BASE_URL}/download")) .body(json!({"file_id": file_id}).to_string()) .header("content-type", "application/json") .header("accept", "application/json") .bearer_auth(&self.token) .build()?; let res: Value = self.client.execute(req).await?.json().await?; #[cfg(feature = "debug")] dbg!(&res); let res = serde_json::from_value(res)?; let response = match res { DownloadResponse::Error { message } => { println!("Error while downloading subtitles: {message}"); return Err(NtitledError::Message(message)); } DownloadResponse::Success(response) => response, }; let req = self.client.get(response.link).build()?; let res = self.client.execute(req).await?.text().await?; info!("Writing subtitles to {full_path}"); std::fs::write(full_path, res)?; Ok(()) } }