From f679496a394cf63f48a7a7a487f6ba7918f34071 Mon Sep 17 00:00:00 2001 From: AndrielFR Date: Fri, 3 Jan 2025 00:05:39 -0300 Subject: [PATCH] feat: add support to `search`: anime, manga and user --- queries/search_anime.graphql | 21 +++++ queries/search_manga.graphql | 21 +++++ queries/search_user.graphql | 20 +++++ src/client.rs | 165 +++++++++++++++++++++++++++++++++-- src/models/anime.rs | 74 ++++++++++++---- src/models/manga.rs | 62 +++++++++---- src/models/user.rs | 153 ++++++++++++++++++++++++-------- 7 files changed, 439 insertions(+), 77 deletions(-) create mode 100644 queries/search_anime.graphql create mode 100644 queries/search_manga.graphql create mode 100644 queries/search_user.graphql diff --git a/queries/search_anime.graphql b/queries/search_anime.graphql new file mode 100644 index 0000000..1e42345 --- /dev/null +++ b/queries/search_anime.graphql @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2022-2025 Andriel Ferreira + +query($search: String, $page: Int = 1, $per_page: Int = 10) { + Page(page: $page, perPage: $per_page) { + pageInfo { + total + currentPage + lastPage + } + media(search: $search, type: ANIME, sort: POPULARITY_DESC) { + id + title { + romaji + english + native + } + siteUrl + } + } +} diff --git a/queries/search_manga.graphql b/queries/search_manga.graphql new file mode 100644 index 0000000..9d68f32 --- /dev/null +++ b/queries/search_manga.graphql @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2022-2025 Andriel Ferreira + +query($search: String, $page: Int = 1, $per_page: Int = 10) { + Page(page: $page, perPage: $per_page) { + pageInfo { + total + currentPage + lastPage + } + media(search: $search, type: MANGA, sort: POPULARITY_DESC) { + id + title { + romaji + english + native + } + siteUrl + } + } +} diff --git a/queries/search_user.graphql b/queries/search_user.graphql new file mode 100644 index 0000000..995cf96 --- /dev/null +++ b/queries/search_user.graphql @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2022-2025 Andriel Ferreira + +query($search: String, $page: Int = 1, $per_page: Int = 10) { + Page(page: $page, perPage: $per_page) { + pageInfo { + total + currentPage + lastPage + } + users(search: $search, sort: SEARCH_MATCH) { + id + name + avatar { + large + medium + } + } + } +} diff --git a/src/client.rs b/src/client.rs index d29198b..509612b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022-2025 Andriel Ferreira -use std::io::Write; use std::time::Duration; +use serde::Deserialize; + use crate::models::{Anime, Character, Manga, Person, User}; use crate::Result; @@ -281,20 +282,166 @@ impl Client { } } - /// Search for anime. + /// Search for animes. /// /// # Arguments /// + /// * `title` - The title of the anime to search. + /// * `page` - The page number to get. + /// * `limit` - The number of animes to get per page. + /// + /// # Errors + /// + /// Returns an error if the request fails. + /// + /// # Example + /// + /// ``` + /// # async fn f(client: rust_anilist::Client) -> rust_anilist::Result<()> { + /// let animes = client.search_anime("Naruto", 1, 10).await.unwrap(); + /// + /// # Ok(()) + /// # } + /// ``` pub async fn search_anime( &self, - variables: serde_json::Value, + title: &str, + page: u16, + limit: u16, ) -> Option> { let result = self - .request(MediaType::Anime, Action::Search, variables) + .request( + MediaType::Anime, + Action::Search, + serde_json::json!({ "search": title, "page": page, "per_page": limit, }), + ) .await .unwrap(); - let mut _models = Vec::::new(); - unimplemented!() + + if let Some(medias) = result["data"]["Page"]["media"].as_array() { + let mut animes = Vec::new(); + + for media in medias.iter() { + let mut anime = crate::models::Anime::default(); + anime.id = media["id"].as_i64().unwrap(); + anime.title = crate::models::Title::deserialize(&media["title"]).unwrap(); + anime.url = media["siteUrl"].as_str().unwrap().to_string(); + + animes.push(anime); + } + + return Some(animes); + } + + None + } + + /// Search for mangas. + /// + /// # Arguments + /// + /// * `title` - The title of the manga to search. + /// * `page` - The page number to get. + /// * `limit` - The number of mangas to get per page. + /// + /// # Errors + /// + /// Returns an error if the request fails. + /// + /// # Example + /// + /// ``` + /// # async fn f(client: rust_anilist::Client) -> rust_anilist::Result<()> { + /// let mangas = client.search_manga("Naruto", 1, 10).await.unwrap(); + /// + /// # Ok(()) + /// # } + /// ``` + pub async fn search_manga( + &self, + title: &str, + page: u16, + limit: u16, + ) -> Option> { + let result = self + .request( + MediaType::Manga, + Action::Search, + serde_json::json!({ "search": title, "page": page, "per_page": limit, }), + ) + .await + .unwrap(); + + if let Some(medias) = result["data"]["Page"]["media"].as_array() { + let mut mangas = Vec::new(); + + for media in medias.iter() { + let mut manga = crate::models::Manga::default(); + manga.id = media["id"].as_i64().unwrap(); + manga.title = crate::models::Title::deserialize(&media["title"]).unwrap(); + manga.url = media["siteUrl"].as_str().unwrap().to_string(); + + mangas.push(manga); + } + + return Some(mangas); + } + + None + } + + /// Search for users. + /// + /// # Arguments + /// + /// * `name` - The name of the user to search. + /// * `page` - The page number to get. + /// * `limit` - The number of users to get per page. + /// + /// # Errors + /// + /// Returns an error if the request fails. + /// + /// # Example + /// + /// ``` + /// # async fn f(client: rust_anilist::Client) -> rust_anilist::Result<()> { + /// let users = client.search_user("andrielfr", 1, 10).await.unwrap(); + /// + /// # Ok(()) + /// # } + /// ``` + pub async fn search_user( + &self, + name: &str, + page: u16, + limit: u16, + ) -> Option> { + let result = self + .request( + MediaType::User, + Action::Search, + serde_json::json!({ "search": name, "page": page, "per_page": limit, }), + ) + .await + .unwrap(); + + if let Some(users) = result["data"]["Page"]["users"].as_array() { + let mut vec = Vec::new(); + + for user in users.iter() { + let mut u = crate::models::User::default(); + u.id = user["id"].as_i64().unwrap() as i32; + u.name = user["name"].as_str().unwrap().to_string(); + u.avatar = crate::models::Image::deserialize(&user["avatar"]).ok(); + + vec.push(u); + } + + return Some(vec); + } + + None } /// Send a request to the AniList API. @@ -359,12 +506,12 @@ impl Client { } Action::Search => { match media_type { - // MediaType::Anime => include_str!("../queries/search_anime.graphql").to_string(), - // MediaType::Manga => include_str!("../queries/search_manga.graphql").to_string(), + MediaType::Anime => include_str!("../queries/search_anime.graphql").to_string(), + MediaType::Manga => include_str!("../queries/search_manga.graphql").to_string(), // MediaType::Character => { // include_str!("../queries/search_character.graphql").to_string() // } - // MediaType::User => include_str!("../queries/search_user.graphql").to_string(), + MediaType::User => include_str!("../queries/search_user.graphql").to_string(), // MediaType::Person => { // include_str!("../queries/search_person.graphql").to_string() // } diff --git a/src/models/anime.rs b/src/models/anime.rs index cbf4235..443fe1b 100644 --- a/src/models/anime.rs +++ b/src/models/anime.rs @@ -1,73 +1,103 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022-2025 Andriel Ferreira -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::models::Character; -use crate::models::Cover; -use crate::models::Date; -use crate::models::Format; -use crate::models::Link; -use crate::models::Person; -use crate::models::Relation; -use crate::models::Season; -use crate::models::Source; -use crate::models::Status; -use crate::models::Studio; -use crate::models::Tag; -use crate::models::Title; -use crate::Client; -use crate::Result; +use super::{ + Character, Cover, Date, Format, Link, Person, Relation, Season, Source, Status, Studio, Tag, + Title, +}; +use crate::{Client, Result}; +/// An anime. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Anime { + /// The ID of the anime. pub id: i64, + /// The ID of the anime on MAL. pub id_mal: Option, + /// The title of the anime. pub title: Title, + /// The format of the anime. pub format: Format, + /// The status of the anime. pub status: Status, + /// The description of the anime. pub description: String, + /// The start date of the anime. pub start_date: Option, + /// The end date of the anime. pub end_date: Option, + /// The season of the anime. pub season: Option, + /// The year of the season of the anime. pub season_year: Option, + /// The integer representation of the season of the anime. pub season_int: Option, + /// The number of episodes of the anime. pub episodes: Option, + /// The duration of the episodes of the anime. pub duration: Option, + /// The country of origin of the anime. pub country_of_origin: Option, + /// Whether the anime is licensed or not. pub is_licensed: Option, + /// The source of the anime. pub source: Option, + /// The hashtag of the anime. pub hashtag: Option, + /// The updated date of the anime. pub updated_at: Option, + /// The cover image of the anime. #[serde(rename = "coverImage")] pub cover: Cover, + /// The banner image of the anime. #[serde(rename = "bannerImage")] pub banner: Option, + /// The genres of the anime. pub genres: Option>, + /// The synonyms of the anime. pub synonyms: Option>, + /// The average score of the anime. pub average_score: Option, + /// The mean score of the anime. pub mean_score: Option, + /// The popularity of the anime. pub popularity: Option, + /// Whether the anime is locked or not. pub is_locked: Option, + /// The trending of the anime. pub trending: Option, + /// The number of favourites of the anime. pub favourites: Option, + /// The tags of the anime. pub tags: Option>, + /// The relations of the anime. #[serde(skip)] pub relations: Option>, + /// The characters of the anime. #[serde(skip)] pub characters: Option>, + /// The staff of the anime. #[serde(skip)] pub staff: Option>, + /// The studios of the anime. #[serde(skip)] pub studios: Option>, + /// Whether the anime is favourite or not. pub is_favourite: Option, + /// Whether the anime is favourite blocked or not. pub is_favourite_blocked: Option, + /// Whether the anime is adult or not. pub is_adult: Option, + /// The next airing episode of the anime. pub next_airing_episode: Option, + /// The external links of the anime. pub external_links: Option>, + /// The streaming episodes of the anime. pub streaming_episodes: Option>, + /// The site URL of the anime. #[serde(rename = "siteUrl")] pub url: String, #[serde(skip)] @@ -75,10 +105,16 @@ pub struct Anime { } impl Anime { + /// Load fully the anime. + /// + /// # Errors + /// + /// Returns an error if the anime is already full loaded. pub async fn load_full(self) -> Result { if !self.is_full_loaded { let mut anime = Client::default().get_anime(self.id).await.unwrap(); anime.is_full_loaded = true; + Ok(anime) } else { panic!("This anime is already full loaded!") @@ -88,10 +124,14 @@ impl Anime { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct AiringSchedule { + /// The ID of the airing schedule. id: i64, + /// The airing date. #[serde(rename = "airingAt")] at: i64, + /// Time until the airing. #[serde(rename = "timeUntilAiring")] time_until: i64, + /// The airing episode. episode: i64, } diff --git a/src/models/manga.rs b/src/models/manga.rs index 7bebb10..6c75acb 100644 --- a/src/models/manga.rs +++ b/src/models/manga.rs @@ -1,67 +1,92 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022-2025 Andriel Ferreira -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::models::Character; -use crate::models::Cover; -use crate::models::Date; -use crate::models::Format; -use crate::models::Link; -use crate::models::Person; -use crate::models::Relation; -use crate::models::Source; -use crate::models::Status; -use crate::models::Studio; -use crate::models::Tag; -use crate::models::Title; -use crate::Client; -use crate::Result; +use super::{ + Character, Cover, Date, Format, Link, Person, Relation, Source, Status, Studio, Tag, Title, +}; +use crate::{Client, Result}; +/// A manga. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Manga { + /// The ID of the manga. pub id: i64, + /// The ID of the manga on MAL. pub id_mal: Option, + /// The title of the manga. pub title: Title, + /// The format of the manga. pub format: Format, + /// The status of the manga. pub status: Status, + /// The description of the manga. pub description: String, + /// The start date of the manga. pub start_date: Option, + /// The end date of the manga. pub end_date: Option, + /// The number of chapters of the manga. pub chapters: Option, + /// The number of volumes of the manga. pub volumes: Option, + /// The country of origin of the manga. pub country_of_origin: Option, + /// Whether the manga is licensed or not. pub is_licensed: Option, + /// The source of the manga. pub source: Option, + /// The hashtag of the manga. pub hashtag: Option, + /// The updated date of the manga. pub updated_at: Option, + /// The cover image of the manga. #[serde(rename = "coverImage")] pub cover: Cover, + /// The banner image of the manga. #[serde(rename = "bannerImage")] pub banner: Option, + /// The genres of the manga. pub genres: Option>, + /// The synonyms of the manga. pub synonyms: Option>, + /// The average score of the manga. pub average_score: Option, + /// The mean score of the manga. pub mean_score: Option, + /// The popularity of the manga. pub popularity: Option, + /// Whether the manga is locked or not. pub is_locked: Option, + /// The trending of the manga. pub trending: Option, + /// The number of favourites of the manga. pub favourites: Option, + /// The tags of the manga. pub tags: Option>, + /// The relations of the manga. #[serde(skip)] pub relations: Option>, + /// The characters of the manga. #[serde(skip)] pub characters: Option>, + /// The staff of the manga. #[serde(skip)] pub staff: Option>, + /// The studios of the manga. #[serde(skip)] pub studios: Option>, + /// Whether the manga is favourite or not. pub is_favourite: Option, + /// Whether the manga is blocked or not. pub is_favourite_blocked: Option, + /// Whether the manga is adult or not. pub is_adult: Option, + /// The external links of the manga. pub external_links: Option>, + /// The site URL of the manga. #[serde(rename = "siteUrl")] pub url: String, #[serde(skip)] @@ -69,6 +94,11 @@ pub struct Manga { } impl Manga { + /// Load fully the manga. + /// + /// # Errors + /// + /// Returns an error if the manga is already full loaded. pub async fn load_full(self) -> Result { if !self.is_full_loaded { let mut manga = Client::default().get_manga(self.id).await.unwrap(); diff --git a/src/models/user.rs b/src/models/user.rs index f6221fd..34446a9 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,152 +1,235 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022-2025 Andriel Ferreira -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::models::Anime; -use crate::models::Character; -use crate::models::Color; -use crate::models::Format; -use crate::models::Image; -use crate::models::Manga; -use crate::models::NotificationOption; -use crate::models::Person; -use crate::models::Status; -use crate::models::Studio; +use super::{ + Anime, Character, Color, Format, Image, Manga, NotificationOption, Person, Status, Studio, +}; +/// A user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct User { - id: i32, - name: String, - about: Option, - avatar: Option, + /// The ID of the user. + pub id: i32, + /// The name of the user. + pub name: String, + /// The about of the user. + pub about: Option, + /// The avatar of the user. + pub avatar: Option, + /// The banner of the user. #[serde(rename = "bannerImage")] - banner: Option, - is_following: Option, - is_follower: Option, - is_blocked: Option, - options: Option, - media_list_options: Option, + pub banner: Option, + /// The donator badge of the user. + pub donator_badge: String, + /// The donator tier of the user. + pub donator_tier: i32, + /// The favourites of the user. #[serde(skip)] - favourites: Favourites, - statistics: UserStatisticTypes, + pub favourites: Favourites, + /// Whether the user is blocked or not. + pub is_blocked: Option, + /// Whether the user is a follower or not. + pub is_follower: Option, + /// Whether the user is following or not. + pub is_following: Option, + /// The media list options of the user. + pub media_list_options: Option, + /// The options of the user. + pub options: Option, + #[serde(rename = "siteUrl")] + pub url: String, + /// The statistics of the user. + pub statistics: UserStatisticTypes, + /// The unread notification count of the user. + pub unread_notification_count: Option, + /// The created date of the user. + pub created_at: i64, + /// The updated date of the user. + pub updated_at: i64, } +/// The options of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] -struct Options { - title_language: Option, +pub struct Options { + /// The title language of the user. + pub title_language: Option, #[serde(default)] - display_adult_content: bool, + /// Whether the user wants to display adult content or not. + pub display_adult_content: bool, + /// Whether the user wants to receive airing notifications or not. #[serde(default)] - airing_notifications: bool, - profile_color: Color, - notifications_options: Option>, - timezone: Option, + pub airing_notifications: bool, + /// The profile color of the user. + pub profile_color: Color, + /// The notifications options of the user. + pub notifications_options: Option>, + /// The timezone of the user. + pub timezone: Option, + /// The activity merge time of the user. #[serde(default)] - activity_merge_time: i32, + pub activity_merge_time: i32, + /// The staff name language of the user. #[serde(default)] - staff_name_language: UserStaffNameLanguage, + pub staff_name_language: UserStaffNameLanguage, + /// Whether the user wants to restrict messages to following or not. #[serde(default)] - restrict_messages_to_following: bool, - disabled_list_activity: Option>, + pub restrict_messages_to_following: bool, + /// The disabled list activity of the user. + pub disabled_list_activity: Option>, } +/// The title language of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "SCREAMING_SNAKE_CASE"))] pub enum UserTitleLanguage { + /// The Romaji title language. #[default] Romaji, + /// The English title language. English, + /// The native title language. Native, + /// The Romaji stylised title language. RomajiStylised, + /// The English stylised title language. EnglishStylised, + /// The native stylised title language. NativeStylised, } +/// The staff name language of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "SCREAMING_SNAKE_CASE"))] pub enum UserStaffNameLanguage { + /// The Romaji Western staff name language. RomajiWestern, + /// The Romaji staff name language. #[default] Romaji, + /// The native staff name language. Native, } +/// The list activity option of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct ListActivityOption { + /// The status of the list activity. status: Status, + /// Whether the list activity is disabled or disabled: bool, } +/// The media list options of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct MediaListOptions { + /// The row order of the media list options. row_order: String, + /// The anime list of the media list options. anime_list: MediaListTypeOptions, + /// The manga list of the media list options. manga_list: MediaListTypeOptions, } +/// The media list type options of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct MediaListTypeOptions { + /// The section order of the media list type options. section_order: Vec, + /// Whether the completed section is split by format or not. split_completed_section_by_format: bool, + /// The custom lists of the media list type options. custom_lists: Vec, + /// The advanced scoring of the media list type options. advanced_scoring: Vec, + /// Whether the advanced scoring is enabled or not. advanced_scoring_enabled: bool, } +/// The favourites of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] pub struct Favourites { + /// The favourited animes. anime: Vec, + /// The favourited mangas. manga: Vec, + /// The favourited characters. characters: Vec, + /// The favourited staff. staff: Vec, + /// The favourited studios. studios: Vec, } +/// The statistics of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct UserStatisticTypes { + /// The anime statistics of the user. anime: UserStatistics, + /// The manga statistics of the user. manga: UserStatistics, } +/// The statistics of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct UserStatistics { + /// The count of the statistics. count: i32, + /// The standard deviation of the statistics. standard_deviation: Option, + /// The minutes watched of the statistics. minutes_watched: Option, + /// The episodes watched of the statistics. episodes_watched: Option, + /// The chapters read of the statistics. chapters_read: Option, + /// The volumes read of the statistics. volumes_read: Option, + /// The formats of the statistics. formats: Option>, + /// The statuses of the statistics. statuses: Vec, } +/// The format statistics of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct UserFormatStatistic { + /// The count of the format statistics. count: i32, + /// The minutes watched of the format statistics. minutes_watched: Option, + /// The chapters read of the format statistics. chapters_read: Option, + /// The media IDs of the format statistics. #[serde(default)] media_ids: Vec, + /// The format of the format statistics. format: Format, } +/// The status statistics of a user. #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all(deserialize = "camelCase"))] pub struct UserStatusStatistic { + /// The count of the status statistics. count: i32, + /// The minutes watched of the status statistics. minutes_watched: Option, + /// The episodes watched of the status statistics. chapters_read: Option, + /// The media IDs of the status statistics. #[serde(default)] + /// The status of the status statistics. media_ids: Vec, + /// The status of the status statistics. status: Status, }