anidb_api/http/
client.rs

1use super::models::{
2    anime::Anime,
3    common::{ApiError, ResponseError},
4};
5use async_trait::async_trait;
6use governor::Quota;
7use reqwest::Method;
8use reqwest_middleware::ClientBuilder;
9use reqwest_tracing::TracingMiddleware;
10use std::time::Duration;
11
12const API_URL: &str = "http://api.anidb.net:9001/httpapi";
13const CLIENT_NAME: &str = "anidbapirs";
14const CLIENT_VER: usize = 1;
15const HTTP_PROTO_VER: usize = 1;
16
17#[derive(Clone)]
18pub struct AniDbHttpClient {
19    base_url: reqwest::Url,
20    client: reqwest_middleware::ClientWithMiddleware,
21}
22
23struct MyRateLimiter {
24    limiter: governor::DefaultDirectRateLimiter,
25}
26
27#[async_trait]
28impl reqwest_ratelimit::RateLimiter for MyRateLimiter {
29    async fn acquire_permit(&self) {
30        self.limiter.until_ready().await;
31    }
32}
33
34impl AniDbHttpClient {
35    /// Returns client with default rate limit (1 request per 2 seconds).
36    /// # Errors
37    /// This method fails if reqwest client has failed to initialized
38    pub fn new() -> Result<Self, ApiError> {
39        let limit = Duration::from_secs(2);
40        Self::with_ratelimit(limit)
41    }
42
43    /// Returns client with custom rate limit.
44    /// Disables rate limiting if `limit` equals 0.
45    /// # Errors
46    /// This method fails if reqwest client has failed to initialized
47    pub fn with_ratelimit(limit: Duration) -> Result<Self, ApiError> {
48        let base_url = format!(
49            "{API_URL}?client={CLIENT_NAME}&clientver={CLIENT_VER}&protover={HTTP_PROTO_VER}"
50        );
51        let base_url = reqwest::Url::parse(&base_url).map_err(ApiError::UrlParse)?;
52
53        let req_client = reqwest::Client::builder()
54            .gzip(true)
55            .build()
56            .map_err(|e| ApiError::Reqwest(e.into()))?;
57
58        let client = if let Some(quota) = Quota::with_period(limit) {
59            let rate_limiter = MyRateLimiter {
60                limiter: governor::RateLimiter::direct(quota),
61            };
62            ClientBuilder::new(req_client).with(reqwest_ratelimit::all(rate_limiter))
63        } else {
64            ClientBuilder::new(req_client)
65        };
66
67        let client = client.with(TracingMiddleware::default()).build();
68        Ok(Self { base_url, client })
69    }
70
71    /// Retrieve information for a specific anime by AID (anidb anime id).
72    /// # Errors
73    /// This method fails if reqwest cannot get response, url cannot be parsed, anidb server returns error or deserialization failed.
74    pub async fn get_anime(&self, anime_id: &str) -> Result<Anime, ApiError> {
75        let params = [("request", "anime"), ("aid", anime_id)];
76        let url = reqwest::Url::parse_with_params(self.base_url.as_str(), &params)
77            .map_err(ApiError::UrlParse)?;
78        let request = self.client.request(Method::GET, url);
79        let response = request.send().await.map_err(ApiError::Reqwest)?;
80
81        // AniDB always return success status code
82        // Check status from body
83        let body_text = response
84            .text()
85            .await
86            .map_err(|e| ApiError::Reqwest(e.into()))?;
87
88        let anidb_error = serde_xml_rs::from_str::<ResponseError>(&body_text);
89
90        if let Ok(err) = anidb_error {
91            Err(ApiError::HttpError {
92                status: err.status.unwrap_or_default(),
93                message: err.text.unwrap_or("Empty error message".to_string()),
94            })
95        } else {
96            Ok(serde_xml_rs::from_str::<Anime>(&body_text).map_err(ApiError::Deserialize)?)
97        }
98    }
99}