client.rs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. use reqwest::{header::HeaderMap, Client};
  2. use serde_json::{json, Value};
  3. use tracing::info;
  4. use crate::{error::NtitledError, ost::data::DownloadResponse};
  5. use super::data::{Login, LoginResponse, SearchResponse};
  6. const BASE_URL: &str = "https://api.opensubtitles.com/api/v1";
  7. #[derive(Debug, Clone)]
  8. pub struct OSClient {
  9. client: Client,
  10. token: String,
  11. }
  12. impl OSClient {
  13. pub async fn init() -> Self {
  14. let api_key = std::env::var("API_KEY").expect("missing API_KEY in env");
  15. let username = std::env::var("USERNAME").expect("missing USERNAME in env");
  16. let password = std::env::var("PASSWORD").expect("missing PASSWORD in env");
  17. let token = std::env::var("JWT").ok();
  18. if let Some(token) = token {
  19. info!("Initialising client with token");
  20. OSClient::new(api_key, token)
  21. .await
  22. .expect("error while loading client")
  23. } else {
  24. info!("Initialising client with login");
  25. OSClient::new_with_login(api_key, username, password)
  26. .await
  27. .expect("error while loading client")
  28. }
  29. }
  30. async fn new_with_login(
  31. api_key: String,
  32. username: String,
  33. password: String,
  34. ) -> Result<Self, NtitledError> {
  35. let mut headers = HeaderMap::new();
  36. headers.append("Api-Key", api_key.parse().expect("invalid api_key"));
  37. let client = reqwest::ClientBuilder::new()
  38. .default_headers(headers)
  39. .user_agent("Ntitled v1.0")
  40. .build()
  41. .expect("invalid client configuration");
  42. let login = Login {
  43. username: &username,
  44. password: &password,
  45. };
  46. let req = client
  47. .post(format!("{BASE_URL}/login"))
  48. .json(&login)
  49. .build()?;
  50. let res: LoginResponse = client.execute(req).await?.json().await?;
  51. let Some((_, token)) = res.token.split_once(' ') else {
  52. return Err(NtitledError::Message(format!(
  53. "received invalid token: {}",
  54. res.token
  55. )));
  56. };
  57. #[cfg(feature = "debug")]
  58. {
  59. dbg!(&token);
  60. use std::fmt::Write;
  61. let env_file =
  62. std::fs::read_to_string(".env").expect("env file required in debug mode");
  63. let mut new_env = String::new();
  64. let mut jwt_written = false;
  65. for line in env_file.lines() {
  66. if line.starts_with("JWT") {
  67. writeln!(new_env, "JWT = \"{token}\"").unwrap();
  68. jwt_written = true;
  69. } else {
  70. writeln!(new_env, "{line}").unwrap();
  71. }
  72. }
  73. if !jwt_written {
  74. writeln!(new_env, "JWT = \"{token}\"").unwrap();
  75. }
  76. println!("New env:\n{new_env}");
  77. std::fs::write(".env", new_env).expect("error while writing new env");
  78. }
  79. Ok(Self {
  80. client,
  81. token: token.to_string(),
  82. })
  83. }
  84. async fn new(api_key: String, token: String) -> Result<Self, NtitledError> {
  85. let mut headers = HeaderMap::new();
  86. headers.append("Api-Key", api_key.parse().expect("invalid api_key"));
  87. headers.append(
  88. "Authorization",
  89. format!("Bearer {token}")
  90. .parse()
  91. .expect("invalid bearer token"),
  92. );
  93. let client = reqwest::ClientBuilder::new()
  94. .default_headers(headers)
  95. .user_agent("Ntitled v1.0")
  96. .build()
  97. .expect("invalid client configuration");
  98. Ok(Self { client, token })
  99. }
  100. pub async fn search(
  101. &self,
  102. title: &str,
  103. hash: Option<&str>,
  104. ) -> Result<SearchResponse, NtitledError> {
  105. let mut req = self
  106. .client
  107. .get(format!("{BASE_URL}/subtitles"))
  108. .query(&[("query", title)]);
  109. if let Some(hash) = hash {
  110. req = req.query(&[("moviehash", hash)])
  111. }
  112. let req = req.build()?;
  113. let res: Value = self.client.execute(req).await?.json().await?;
  114. #[cfg(feature = "debug")]
  115. {
  116. std::fs::write("response.json", res.to_string()).unwrap();
  117. }
  118. Ok(serde_json::from_value(res)?)
  119. }
  120. pub async fn download_subtitles(
  121. &self,
  122. file_id: usize,
  123. full_path: &str,
  124. ) -> Result<(), NtitledError> {
  125. let req = self
  126. .client
  127. .post(format!("{BASE_URL}/download"))
  128. .body(json!({"file_id": file_id}).to_string())
  129. .header("content-type", "application/json")
  130. .header("accept", "application/json")
  131. .bearer_auth(&self.token)
  132. .build()?;
  133. let res: Value = self.client.execute(req).await?.json().await?;
  134. #[cfg(feature = "debug")]
  135. dbg!(&res);
  136. let res = serde_json::from_value(res)?;
  137. let response = match res {
  138. DownloadResponse::Error { message } => {
  139. println!("Error while downloading subtitles: {message}");
  140. return Err(NtitledError::Message(message));
  141. }
  142. DownloadResponse::Success(response) => response,
  143. };
  144. let req = self.client.get(response.link).build()?;
  145. let res = self.client.execute(req).await?.text().await?;
  146. info!("Writing subtitles to {full_path}");
  147. std::fs::write(full_path, res)?;
  148. Ok(())
  149. }
  150. }