mal_api/anime/
api.rs

1use super::{
2    error::AnimeApiError,
3    requests::{DeleteMyAnimeListItem, GetUserAnimeList, UpdateMyAnimeListStatus},
4    responses::AnimeListStatus,
5};
6use async_trait::async_trait;
7use oauth2::{AccessToken, ClientId};
8use serde::{de::DeserializeOwned, Serialize};
9use std::marker::{PhantomData, Send, Sync};
10
11use crate::{
12    common::{struct_to_form_data, PagingIter},
13    oauth::{Authenticated, MalClientId, OauthClient},
14    ANIME_URL, USER_URL,
15};
16
17use super::{
18    requests::{
19        GetAnimeDetails, GetAnimeList, GetAnimeRanking, GetSeasonalAnime, GetSuggestedAnime,
20    },
21    responses::{AnimeDetails, AnimeList, AnimeRanking, SeasonalAnime, SuggestedAnime},
22};
23use reqwest;
24
25#[doc(hidden)]
26#[derive(Debug)]
27pub struct Client {}
28
29#[doc(hidden)]
30#[derive(Debug)]
31pub struct Oauth {}
32
33#[doc(hidden)]
34#[derive(Debug)]
35pub struct None {}
36
37/// The AnimeApiClient provides functions for interacting with the various
38/// `anime` and `user animelist` MAL API endpoints. The accessible endpoints
39/// vary depending on if the AnimeApiClient was constructed from a
40/// [MalClientId] or an [OauthClient].
41///
42/// Keep in mind that constructing an AnimeApiClient from an [OauthClient] provides
43/// more access to the MAL API than from a [MalClientId]. Check the MAL API documentation
44/// to view which endpoints require an [OauthClient] versus a [MalClientId] to see which
45/// one is most appropriate for your use case.
46///
47/// # Example
48///
49/// ```rust,ignore
50/// use dotenvy;
51/// use mal_api::oauth::MalClientId;
52/// use mal_api::prelude::*;
53///
54/// #[tokio::main]
55/// async fn main() {
56///     dotenvy::dotenv().ok();
57///
58///     let client_id = MalClientId::from_env().unwrap();
59///     let api_client = AnimeApiClient::from(&client_id);
60///     let common_fields = mal_api::anime::all_common_fields();
61///     let detail_fields = mal_api::anime::all_detail_fields();
62///
63///     // Using the builder pattern for building the query
64///     let query = GetAnimeList::builder("One Piece")
65///         .fields(&common_fields)
66///         .build()
67///         .unwrap();
68///     let response = api_client.get_anime_list(&query).await;
69///     if let Ok(response) = response {
70///         println!("Received response: {}\n", response);
71///         for entry in response.data.iter() {
72///             println!("Id: {}", entry.node.id);
73///         }
74///     }
75///
76///     let query = GetAnimeDetails::builder(9969)
77///         .fields(&detail_fields)
78///         .build()
79///         .unwrap();
80///     let response = api_client.get_anime_details(&query).await;
81///     if let Ok(response) = response {
82///         println!("Received response: {}\n", response);
83///     }
84/// }
85/// ```
86
87#[derive(Debug, Clone)]
88pub struct AnimeApiClient<State = None> {
89    client: reqwest::Client,
90    client_id: Option<String>,
91    access_token: Option<String>,
92    state: PhantomData<State>,
93}
94
95impl From<&AccessToken> for AnimeApiClient<Oauth> {
96    fn from(value: &AccessToken) -> Self {
97        AnimeApiClient::<Oauth> {
98            client: reqwest::Client::new(),
99            client_id: None,
100            access_token: Some(value.secret().clone()),
101            state: PhantomData::<Oauth>,
102        }
103    }
104}
105
106impl From<&ClientId> for AnimeApiClient<Client> {
107    fn from(value: &ClientId) -> Self {
108        AnimeApiClient::<Client> {
109            client: reqwest::Client::new(),
110            client_id: Some(value.clone().to_string()),
111            access_token: None,
112            state: PhantomData::<Client>,
113        }
114    }
115}
116
117impl From<&MalClientId> for AnimeApiClient<Client> {
118    fn from(value: &MalClientId) -> Self {
119        AnimeApiClient::<Client> {
120            client: reqwest::Client::new(),
121            client_id: Some(value.0.to_string()),
122            access_token: None,
123            state: PhantomData::<Client>,
124        }
125    }
126}
127
128impl From<&OauthClient<Authenticated>> for AnimeApiClient<Oauth> {
129    fn from(value: &OauthClient<Authenticated>) -> Self {
130        AnimeApiClient {
131            client: reqwest::Client::new(),
132            client_id: None,
133            access_token: Some(value.get_access_token().secret().clone()),
134            state: PhantomData::<Oauth>,
135        }
136    }
137}
138
139/// This trait defines the common request methods available to both
140/// Client and Oauth AnimeApiClients
141#[async_trait]
142pub trait Request {
143    async fn get<T>(&self, query: &T) -> Result<String, AnimeApiError>
144    where
145        T: Serialize + Send + Sync;
146
147    async fn get_details(&self, query: &GetAnimeDetails) -> Result<String, AnimeApiError>;
148
149    async fn get_ranking(&self, query: &GetAnimeRanking) -> Result<String, AnimeApiError>;
150
151    async fn get_seasonal(&self, query: &GetSeasonalAnime) -> Result<String, AnimeApiError>;
152
153    async fn get_user(&self, query: &GetUserAnimeList) -> Result<String, AnimeApiError>;
154
155    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, AnimeApiError>;
156}
157
158/// This trait defines the shared endpoints for Client and Oauth
159/// AnimeApiClients. It provides default implementations such that
160/// the Oauth AnimeApiClient can override them if needed.
161#[async_trait]
162pub trait AnimeApi {
163    type State: Request + Send + Sync;
164
165    /// Get a list of anime that are similar to the given query
166    ///
167    /// Corresponds to the [Get anime list](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_get) endpoint
168    async fn get_anime_list(&self, query: &GetAnimeList) -> Result<AnimeList, AnimeApiError> {
169        let response = self
170            .get_self()
171            .get(query)
172            .await
173            .map_err(|err| AnimeApiError::new(format!("Failed to get anime list: {}", err)))?;
174        let result: AnimeList = serde_json::from_str(response.as_str()).map_err(|err| {
175            AnimeApiError::new(format!("Failed to parse Anime List result: {}", err))
176        })?;
177        Ok(result)
178    }
179
180    /// Get the details of an anime that matches the given query
181    ///
182    /// Corresponds to the [Get anime details](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get) endpoint
183    async fn get_anime_details(
184        &self,
185        query: &GetAnimeDetails,
186    ) -> Result<AnimeDetails, AnimeApiError> {
187        let response =
188            self.get_self().get_details(query).await.map_err(|err| {
189                AnimeApiError::new(format!("Failed to get anime details: {}", err))
190            })?;
191        let result: AnimeDetails = serde_json::from_str(response.as_str()).map_err(|err| {
192            AnimeApiError::new(format!("Failed to parse Anime Details result: {}", err))
193        })?;
194        Ok(result)
195    }
196
197    /// Get the ranking of anime
198    ///
199    /// Corresponds to the [Get anime ranking](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_ranking_get) endpoint
200    async fn get_anime_ranking(
201        &self,
202        query: &GetAnimeRanking,
203    ) -> Result<AnimeRanking, AnimeApiError> {
204        let response =
205            self.get_self().get_ranking(query).await.map_err(|err| {
206                AnimeApiError::new(format!("Failed to get anime ranking: {}", err))
207            })?;
208        let result: AnimeRanking = serde_json::from_str(response.as_str()).map_err(|err| {
209            AnimeApiError::new(format!("Failed to parse Anime Ranking result: {}", err))
210        })?;
211        Ok(result)
212    }
213
214    /// Get the seasonal anime that fall within the given query
215    ///
216    /// Corresponds to the [Get seasonal anime](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_season_year_season_get) endpoint
217    async fn get_seasonal_anime(
218        &self,
219        query: &GetSeasonalAnime,
220    ) -> Result<SeasonalAnime, AnimeApiError> {
221        let response =
222            self.get_self().get_seasonal(query).await.map_err(|err| {
223                AnimeApiError::new(format!("Failed to get seasonal anime: {}", err))
224            })?;
225        let result: SeasonalAnime = serde_json::from_str(response.as_str()).map_err(|err| {
226            AnimeApiError::new(format!("Failed to parse Seasonal Anime result: {}", err))
227        })?;
228        Ok(result)
229    }
230
231    /// Return the results of the next page, if possible
232    async fn next<T>(&self, response: &T) -> Result<T, AnimeApiError>
233    where
234        T: DeserializeOwned + PagingIter + Sync + Send,
235    {
236        let response = self
237            .get_self()
238            .get_next_or_prev(response.next_page())
239            .await
240            .map_err(|err| AnimeApiError::new(format!("Failed to fetch next page: {}", err)))?;
241        let result: T = serde_json::from_str(response.as_str())
242            .map_err(|err| AnimeApiError::new(format!("Failed to fetch next page: {}", err)))?;
243        Ok(result)
244    }
245
246    /// Return the results of the previous page, if possible
247    async fn prev<T>(&self, response: &T) -> Result<T, AnimeApiError>
248    where
249        T: DeserializeOwned + PagingIter + Sync + Send,
250    {
251        let response = self
252            .get_self()
253            .get_next_or_prev(response.prev_page())
254            .await
255            .map_err(|err| AnimeApiError::new(format!("Failed to fetch previous page: {}", err)))?;
256        let result: T = serde_json::from_str(response.as_str())
257            .map_err(|err| AnimeApiError::new(format!("Failed to parse page: {}", err)))?;
258        Ok(result)
259    }
260
261    /// Utility method for API trait to use the appropriate request method
262    fn get_self(&self) -> &Self::State;
263}
264
265#[async_trait]
266impl Request for AnimeApiClient<Client> {
267    async fn get<T>(&self, query: &T) -> Result<String, AnimeApiError>
268    where
269        T: Serialize + Send + Sync,
270    {
271        let response = self
272            .client
273            .get(ANIME_URL)
274            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
275            .query(&query)
276            .send()
277            .await
278            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
279
280        handle_response(response).await
281    }
282
283    async fn get_details(&self, query: &GetAnimeDetails) -> Result<String, AnimeApiError> {
284        let response = self
285            .client
286            .get(format!("{}/{}", ANIME_URL, query.anime_id))
287            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
288            .query(&query)
289            .send()
290            .await
291            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
292
293        handle_response(response).await
294    }
295
296    async fn get_ranking(&self, query: &GetAnimeRanking) -> Result<String, AnimeApiError> {
297        let response = self
298            .client
299            .get(format!("{}/ranking", ANIME_URL))
300            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
301            .query(&query)
302            .send()
303            .await
304            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
305
306        handle_response(response).await
307    }
308
309    async fn get_seasonal(&self, query: &GetSeasonalAnime) -> Result<String, AnimeApiError> {
310        let response = self
311            .client
312            .get(format!(
313                "{}/season/{}/{}",
314                ANIME_URL, query.year, query.season
315            ))
316            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
317            .query(&query)
318            .send()
319            .await
320            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
321
322        handle_response(response).await
323    }
324
325    async fn get_user(&self, query: &GetUserAnimeList) -> Result<String, AnimeApiError> {
326        let response = self
327            .client
328            .get(format!("{}/{}/animelist", USER_URL, query.user_name))
329            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
330            .query(&query)
331            .send()
332            .await
333            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
334
335        handle_response(response).await
336    }
337
338    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, AnimeApiError> {
339        if let Some(itr) = query {
340            let response = self
341                .client
342                .get(itr)
343                .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
344                .send()
345                .await
346                .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
347
348            handle_response(response).await
349        } else {
350            Err(AnimeApiError::new("Page does not exist".to_string()))
351        }
352    }
353}
354
355#[async_trait]
356impl Request for AnimeApiClient<Oauth> {
357    async fn get<T>(&self, query: &T) -> Result<String, AnimeApiError>
358    where
359        T: Serialize + Send + Sync,
360    {
361        let response = self
362            .client
363            .get(ANIME_URL)
364            .bearer_auth(&self.access_token.as_ref().unwrap())
365            .query(&query)
366            .send()
367            .await
368            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
369
370        handle_response(response).await
371    }
372
373    async fn get_details(&self, query: &GetAnimeDetails) -> Result<String, AnimeApiError> {
374        let response = self
375            .client
376            .get(format!("{}/{}", ANIME_URL, query.anime_id))
377            .bearer_auth(&self.access_token.as_ref().unwrap())
378            .query(&query)
379            .send()
380            .await
381            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
382
383        handle_response(response).await
384    }
385
386    async fn get_ranking(&self, query: &GetAnimeRanking) -> Result<String, AnimeApiError> {
387        let response = self
388            .client
389            .get(format!("{}/ranking", ANIME_URL))
390            .bearer_auth(&self.access_token.as_ref().unwrap())
391            .query(&query)
392            .send()
393            .await
394            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
395
396        handle_response(response).await
397    }
398
399    async fn get_seasonal(&self, query: &GetSeasonalAnime) -> Result<String, AnimeApiError> {
400        let response = self
401            .client
402            .get(format!(
403                "{}/season/{}/{}",
404                ANIME_URL, query.year, query.season
405            ))
406            .bearer_auth(&self.access_token.as_ref().unwrap())
407            .query(&query)
408            .send()
409            .await
410            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
411
412        handle_response(response).await
413    }
414
415    async fn get_user(&self, query: &GetUserAnimeList) -> Result<String, AnimeApiError> {
416        let response = self
417            .client
418            .get(format!("{}/{}/animelist", USER_URL, query.user_name))
419            .bearer_auth(&self.access_token.as_ref().unwrap())
420            .query(&query)
421            .send()
422            .await
423            .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
424
425        handle_response(response).await
426    }
427
428    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, AnimeApiError> {
429        if let Some(itr) = query {
430            let response = self
431                .client
432                .get(itr)
433                .bearer_auth(&self.access_token.as_ref().unwrap())
434                .send()
435                .await
436                .map_err(|err| AnimeApiError::new(format!("Failed get request: {}", err)))?;
437
438            handle_response(response).await
439        } else {
440            Err(AnimeApiError::new("Page does not exist".to_string()))
441        }
442    }
443}
444
445#[async_trait]
446impl AnimeApi for AnimeApiClient<Client> {
447    type State = AnimeApiClient<Client>;
448
449    fn get_self(&self) -> &Self::State {
450        self
451    }
452}
453
454impl AnimeApiClient<Client> {
455    /// Get a users anime list
456    ///
457    /// You **cannot** get the anime list of `@me` with a [ClientId] AnimeApiClient
458    ///
459    /// Corresponds to the [Get user anime list](https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get) endpoint
460    pub async fn get_user_anime_list(
461        &self,
462        query: &GetUserAnimeList,
463    ) -> Result<AnimeList, AnimeApiError> {
464        if query.user_name == "@me".to_string() {
465            return Err(AnimeApiError::new(
466                "You can only get your '@me' list via an Oauth client".to_string(),
467            ));
468        }
469        let response = self.get_self().get_user(query).await.map_err(|err| {
470            AnimeApiError::new(format!(
471                "Failed to fetch {}'s anime list: {}",
472                query.user_name, err
473            ))
474        })?;
475        let result: AnimeList = serde_json::from_str(response.as_str()).map_err(|err| {
476            AnimeApiError::new(format!("Failed to parse Anime List result: {}", err))
477        })?;
478        Ok(result)
479    }
480}
481
482#[async_trait]
483impl AnimeApi for AnimeApiClient<Oauth> {
484    type State = AnimeApiClient<Oauth>;
485
486    fn get_self(&self) -> &Self::State {
487        self
488    }
489}
490
491impl AnimeApiClient<Oauth> {
492    /// Get a list of suggested anime
493    ///
494    /// Corresponds to the [Get suggested anime](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_suggestions_get) endpoint
495    pub async fn get_suggested_anime(
496        &self,
497        query: &GetSuggestedAnime,
498    ) -> Result<SuggestedAnime, AnimeApiError> {
499        let response = self
500            .client
501            .get(format!("{}/suggestions", ANIME_URL))
502            .bearer_auth(&self.access_token.as_ref().unwrap())
503            .query(&query)
504            .send()
505            .await
506            .map_err(|err| {
507                AnimeApiError::new(format!("Failed to fetch suggested anime: {}", err))
508            })?;
509
510        let response = handle_response(response).await?;
511
512        let result: SuggestedAnime = serde_json::from_str(response.as_str()).map_err(|err| {
513            AnimeApiError::new(format!("Failed to parse Suggested Anime result: {}", err))
514        })?;
515        Ok(result)
516    }
517
518    /// Get a users Anime list
519    ///
520    /// You **can** get the anime list of `@me` with an [OauthClient] AnimeApiClient
521    ///
522    /// Corresponds to the [Get user anime list](https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get) endpoint
523    pub async fn get_user_anime_list(
524        &self,
525        query: &GetUserAnimeList,
526    ) -> Result<AnimeList, AnimeApiError> {
527        let response =
528            self.get_self().get_user(query).await.map_err(|err| {
529                AnimeApiError::new(format!("Failed to get user anime list: {}", err))
530            })?;
531        let result: AnimeList = serde_json::from_str(response.as_str()).map_err(|err| {
532            AnimeApiError::new(format!("Failed to parse Anime List result: {}", err))
533        })?;
534        Ok(result)
535    }
536
537    /// Update the status of an anime for the OAuth user's anime list
538    ///
539    /// Corresponds to the [Update my anime list status](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_my_list_status_put) endpoint
540    pub async fn update_anime_list_status(
541        &self,
542        query: &UpdateMyAnimeListStatus,
543    ) -> Result<AnimeListStatus, AnimeApiError> {
544        let form_data = struct_to_form_data(&query).map_err(|err| {
545            AnimeApiError::new(format!("Failed to turn request into form data: {}", err))
546        })?;
547        let response = self
548            .client
549            .put(format!("{}/{}/my_list_status", ANIME_URL, query.anime_id))
550            .bearer_auth(&self.access_token.as_ref().unwrap())
551            .form(&form_data)
552            .send()
553            .await
554            .map_err(|err| {
555                AnimeApiError::new(format!(
556                    "Failed to update user's anime list status: {}",
557                    err
558                ))
559            })?;
560
561        let response = handle_response(response).await?;
562        let result: AnimeListStatus = serde_json::from_str(response.as_str()).map_err(|err| {
563            AnimeApiError::new(format!("Failed to parse Anime List result: {}", err))
564        })?;
565        Ok(result)
566    }
567
568    /// Delete an anime entry from the OAuth user's anime list
569    ///
570    /// Corresponds to the [Delete my anime list item](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_my_list_status_delete) endpoint
571    pub async fn delete_anime_list_item(
572        &self,
573        query: &DeleteMyAnimeListItem,
574    ) -> Result<(), AnimeApiError> {
575        let response = self
576            .client
577            .delete(format!("{}/{}/my_list_status", ANIME_URL, query.anime_id))
578            .bearer_auth(&self.access_token.as_ref().unwrap())
579            .send()
580            .await
581            .map_err(|err| {
582                AnimeApiError::new(format!("Failed to delete the anime list item: {}", err))
583            })?;
584
585        match response.status() {
586            reqwest::StatusCode::OK => Ok(()),
587            reqwest::StatusCode::NOT_FOUND => Err(AnimeApiError::new(
588                "Anime does not exist in user's anime list".to_string(),
589            )),
590            _ => Err(AnimeApiError::new(format!(
591                "Did not recieve expected response: {}",
592                response.status()
593            ))),
594        }
595    }
596}
597
598async fn handle_response(response: reqwest::Response) -> Result<String, AnimeApiError> {
599    match response.status() {
600        reqwest::StatusCode::OK => {
601            let content = response.text().await.map_err(|err| {
602                AnimeApiError::new(format!("Failed to get content from response: {}", err))
603            })?;
604            Ok(content)
605        }
606        _ => Err(AnimeApiError::new(format!(
607            "Did not recieve OK response: {}",
608            response.status()
609        ))),
610    }
611}