AniList_Moe

A comprehensive, type-safe Rust wrapper for the AniList GraphQL API.
Features
- Type Safety: Fully typed responses with proper error handling
- Simplified Response Format: Clean and intuitive API responses (v0.3.0+)
- Modular Design: Organized endpoints for all AniList features
- Authentication Support: Both authenticated and unauthenticated clients
- Async/Await: Built with Tokio for high-performance asynchronous operations
- Zero Unsafe Code: Written entirely in safe Rust
- Comprehensive Coverage: Full AniList API support including social features
- Custom Query Support: Execute tailored GraphQL queries via the generic
AniListClient::fetch helper
- Conditional Field Toggles: Fine-grained
include_* flags let you opt into heavy nested data only when you need it
- Pagination Support: Built-in helpers for paginated results
- Well Tested: Extensive test suite covering all endpoints
- Convenience Functions: Easy-to-use helper methods for common queries
Supported Endpoints
Core Content
- Anime: Popular, trending, search, seasonal, top-rated, airing, upcoming
- Manga: Popular, trending, search, top-rated, releasing, completed
- Characters: Popular, search, by ID, birthdays, most favorited
- Staff: Popular, search, by ID, birthdays, most favorited
- Studios: Search, by ID, with media productions
Social & Community
- Users: Profiles, statistics, favorites, lists, followers, following
- Forums: Threads, comments, categories, search, subscriptions
- Activities: Text activities, message activities, list updates, replies
- Reviews: Browse, create, update, delete user reviews
- Recommendations: Browse and manage anime/manga recommendations
Scheduling & Discovery
- Airing Schedules: Upcoming episodes, recently aired, date ranges
- Notifications: Read, manage, and mark notifications as read
- Media Lists: User anime/manga lists with status tracking
Interactions
- Common: Toggle likes, toggle follows, toggle favorites on various content types
Installation
Add this to your Cargo.toml:
[dependencies]
anilist_moe = "0.3"
tokio = { version = "1.0", features = ["full"] }
Quick Start
Example 1: Get Trending Anime
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let response = client.media().get_trending_anime(Some(1), Some(10)).await?;
for anime in &response.data {
println!(
"Title: {} - Score: {}/100",
anime.title.as_ref().and_then(|t| t.romaji.as_ref()).unwrap_or(&"Unknown".to_string()),
anime.average_score.unwrap_or(0)
);
}
Ok(())
}
Example 2: Search and Get Detailed Information
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let search_results = client.media().search_anime("Attack on Titan", Some(1), Some(5)).await?;
if let Some(first_result) = search_results.data.first() {
let anime_id = first_result.id.unwrap_or(0);
let anime = client.media().get_anime_by_id(anime_id).await?;
println!("Title: {}", anime.title.as_ref()
.and_then(|t| t.romaji.as_ref())
.unwrap_or(&"Unknown".to_string()));
println!("Synopsis: {}", anime.description.as_ref()
.unwrap_or(&"No description".to_string()));
println!("Score: {}/100", anime.average_score.unwrap_or(0));
println!("Episodes: {}", anime.episodes.unwrap_or(0));
if let Some(genres) = &anime.genres {
println!("Genres: {}", genres.join(", "));
}
}
Ok(())
}
Advanced Usage
Custom GraphQL Query
use anilist_moe::{
AniListClient,
AniListError,
objects::{media::Media, responses::Page},
};
use serde_json::json;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let mut variables = HashMap::new();
variables.insert("page".to_string(), json!(1));
variables.insert("perPage".to_string(), json!(5));
let query = r#"
query ($page: Int, $perPage: Int) {
Page(page: $page, perPage: $perPage) {
media {
id
title {
romaji
english
native
}
}
}
}
"#;
let page: Page<Vec<Media>> = client.fetch(query, Some(&variables)).await?;
for media in page.data {
if let (Some(id), Some(title)) = (media.id, media.title.as_ref()) {
println!("{} - {:?}", id, title.romaji);
}
}
Ok(())
}
Conditional Fetching Controls
use anilist_moe::{AniListClient, AniListError};
use anilist_moe::endpoints::media::FetchMediaOptions;
use anilist_moe::enums::media::{MediaSort, MediaType};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let options = FetchMediaOptions {
media_type: Some(MediaType::Anime),
sort: Some(vec![MediaSort::PopularityDesc]),
page: Some(1),
per_page: Some(3),
include_characters: Some(true),
include_staff: Some(true),
include_reviews: Some(true),
include_recommendations: Some(true),
..Default::default()
};
let response = client.media().fetch(&options).await?;
for media in response.data {
println!(
"{} has {} character edges",
media.title
.as_ref()
.and_then(|t| t.romaji.as_ref())
.unwrap_or(&"Unknown".to_string()),
media
.characters
.as_ref()
.and_then(|c| c.edges.as_ref())
.map(|edges| edges.len())
.unwrap_or(0)
);
}
Ok(())
}
Detailed Usage
Anime Operations
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let popular = client.media().get_popular_anime(Some(1), Some(5)).await?;
for anime in &popular.data {
println!("{}", anime.title.as_ref().unwrap().romaji.as_ref().unwrap());
}
let trending = client.media().get_trending_anime(Some(1), Some(5)).await?;
let anime = client.media().get_anime_by_id(16498).await?;
let search_results = client.media().search_anime("Naruto", Some(1), Some(10)).await?;
use anilist_moe::enums::media::MediaSeason;
let fall_2023 = client.media().get_by_season(MediaSeason::Fall, 2023, Some(1), Some(10)).await?;
let top_rated = client.media().get_top_rated_anime(Some(1), Some(10)).await?;
let airing = client.media().get_airing_anime(Some(1), Some(10)).await?;
Ok(())
}
Manga Operations
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let popular = client.manga().get_popular_manga(Some(1), Some(5)).await?;
let search_results = client.manga().search_manga("One Piece", Some(1), Some(10)).await?;
let top_rated = client.manga().get_top_rated_manga(Some(1), Some(10)).await?;
let releasing = client.manga().get_releasing_manga(Some(1), Some(10)).await?;
Ok(())
}
Character Operations
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let popular = client.character().get_popular_characters(Some(1), Some(10)).await?;
let character = client.character().get_by_id(40).await?;
let search_results = client.character().search_character("Luffy", Some(1), Some(10)).await?;
let birthday_chars = client.character().get_by_birthday(5, 5, Some(1), Some(10)).await?;
let most_favorited = client.character().get_most_favorited_characters(Some(1), Some(10)).await?;
Ok(())
}
Forum Operations
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let recent = client.forum().get_recent_threads(Some(1), Some(10)).await?;
for thread in &recent.data {
println!("Thread: {}", thread.title.as_ref().unwrap_or(&"Untitled".to_string()));
}
let search = client.forum().search_thread("recommendation", Some(1), Some(10)).await?;
let thread = client.forum().get_thread_by_id(12345).await?;
let comments = client.forum().get_thread_comments(12345, Some(1), Some(20)).await?;
Ok(())
}
Authentication
For endpoints requiring authentication:
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let token = std::env::var("ANILIST_TOKEN")?;
let client = AniListClient::with_token(&token);
let current_user = client.user().get_current_user().await?;
println!("Logged in as: {}", current_user.name.unwrap_or_default());
let anime_list = client.medialist()
.get_user_anime_list(¤t_user.name.unwrap_or_default(), None, Some(1), Some(50))
.await?;
Ok(())
}
Getting an Access Token
To get an access token for authentication:
- Register your application at AniList Developer Console
- Implement OAuth2 flow to get user authorization
- Exchange authorization code for access token
- Use the access token with
AniListClient::with_token()
Error Handling
The library provides comprehensive error handling:
use anilist_moe::{AniListClient, errors::AniListError};
#[tokio::main]
async fn main() {
let client = AniListClient::new();
match client.media().get_anime_by_id(999999).await {
Ok(anime) => println!("Found anime: {:?}", anime),
Err(AniListError::Network(e)) => eprintln!("Network error: {}", e),
Err(AniListError::GraphQL { message }) => eprintln!("GraphQL error: {}", message),
Err(AniListError::Json(e)) => eprintln!("JSON parsing error: {}", e),
Err(AniListError::RateLimit { limit, remaining, reset_at, retry_after }) => {
eprintln!("Rate limited - retry after {} seconds", retry_after);
}
Err(AniListError::RateLimitSimple) => eprintln!("Rate limited"),
Err(AniListError::BurstLimit) => eprintln!("Too many requests too quickly"),
Err(AniListError::NotFound) => eprintln!("Not found"),
Err(AniListError::AuthenticationRequired) => eprintln!("Authentication required"),
Err(AniListError::AccessDenied) => eprintln!("Access denied"),
Err(e) => eprintln!("Error: {:?}", e),
}
}
Type Safety
All responses are fully typed. No more dealing with serde_json::Value:
let response = client.media().get_trending_anime(Some(1), Some(10)).await?;
let anime = &response.data[0];
let title = anime.title.as_ref().unwrap();
let romaji_title = &title.romaji;
let score = anime.average_score.unwrap_or(0);
let genres = anime.genres.as_ref().unwrap();
if let Some(page_info) = &response.page_info {
println!("Current page: {:?}", page_info.current_page);
println!("Has next page: {:?}", page_info.has_next_page);
}
Data Models
The library includes comprehensive data models for all AniList entities:
- Media: Complete anime/manga information including titles, descriptions, episodes, genres
- Character: Character details including names, images, descriptions, birthdays
- Staff: Staff information including names, roles, occupations
- User: User profiles including statistics, favorites, and preferences
- Thread: Forum threads with comments, categories, and user information
- Activity: User activities including text posts, list updates, and replies
- Review: User reviews with ratings and detailed content
- Recommendation: Anime/manga recommendations with ratings
Testing
Run the test suite:
cargo test
Run specific endpoint tests:
cargo test --test endpoint_tests
The library includes comprehensive tests for:
- All endpoint methods
- Error handling scenarios
- Pagination functionality
- Type safety verification
Rate Limiting
The AniList API has rate limiting (90 requests per minute). The client handles basic error responses, but you should implement your own rate limiting logic for production applications:
use anilist_moe::{AniListClient, errors::AniListError};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
for page in 1..=10 {
match client.media().get_popular_anime(Some(page), Some(50)).await {
Ok(response) => {
}
Err(AniListError::RateLimit { retry_after, .. }) => {
sleep(Duration::from_secs(retry_after as u64)).await;
continue;
}
Err(AniListError::RateLimitSimple) => {
sleep(Duration::from_secs(60)).await;
continue;
}
Err(e) => return Err(e),
}
sleep(Duration::from_millis(700)).await;
}
Ok(())
}
Pagination
All list endpoints support pagination:
use anilist_moe::{AniListClient, AniListError};
#[tokio::main]
async fn main() -> Result<(), AniListError> {
let client = AniListClient::new();
let mut page = 1;
let per_page = 50;
loop {
let response = client.media().get_popular_anime(Some(page), Some(per_page)).await?;
for anime in &response.data {
println!("{:?}", anime.title);
}
if let Some(page_info) = &response.page_info {
if !page_info.has_next_page.unwrap_or(false) {
break;
}
} else {
break;
}
page += 1;
}
Ok(())
}
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. See CONTRIBUTING.md for detailed guidelines.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Resources
Changelog
See CHANGELOG.md for detailed version history.