Files
asepharyana-hub-scraper/src/presentation/handler/anime2.rs
T

290 lines
9.8 KiB
Rust

//! Anime2 (Alqanime) API handlers.
use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::Json;
use serde::Deserialize;
use tracing::info;
use utoipa::{IntoParams, ToSchema};
use crate::application::anime2::use_cases::Anime2UseCases;
use crate::application::anime2::use_cases::{
Anime2Response, DetailResponse, FilterResponse, GenresResponse,
};
use crate::domain::entity::anime::{
CompleteAnimeItem, GenreAnimeItem, LatestAnimeItem, OngoingAnimeItemWithScore, SearchAnimeItem,
};
use crate::infrastructure::repository::AlqanimeRepository;
use crate::presentation::dto::common::ApiResponse;
use crate::presentation::error::AppError;
use crate::presentation::state::AppState;
// ============================================================================
// Request DTOs
// ============================================================================
/// Filter query parameters for the anime2 filter endpoint.
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct FilterQuery {
pub page: Option<u32>,
pub genre: Option<String>,
pub status: Option<String>,
pub r#type: Option<String>,
pub order: Option<String>,
}
// ============================================================================
// Helper
// ============================================================================
fn make_use_cases(state: &Arc<AppState>) -> Anime2UseCases {
Anime2UseCases::new(AlqanimeRepository::new(), state.redis_pool.clone())
}
// ============================================================================
// Handlers
// ============================================================================
/// GET /api/anime2 — Anime2 index (ongoing + complete).
#[utoipa::path(
get,
path = "/api/anime2",
tag = "anime2",
operation_id = "anime2_index",
responses(
(status = 200, description = "Anime2 index", body = Anime2Response),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn index(
State(app_state): State<Arc<AppState>>,
) -> Result<Json<Anime2Response>, AppError> {
info!("Handling request for anime2 index");
let data = make_use_cases(&app_state).index().await?;
Ok(Json(data))
}
/// GET /api/anime2/genre_list — List all genres.
#[utoipa::path(
get,
path = "/api/anime2/genre_list",
tag = "anime2",
operation_id = "anime2_genre_list",
responses(
(status = 200, description = "Genre list", body = GenresResponse),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn genre_list(
State(app_state): State<Arc<AppState>>,
) -> Result<Json<GenresResponse>, AppError> {
info!("Handling request for anime2 genre list");
let data = make_use_cases(&app_state).genre_list().await?;
Ok(Json(data))
}
/// GET /api/anime2/filter?page=&genre=&status=&type=&order= — Filter anime.
#[utoipa::path(
get,
path = "/api/anime2/filter",
tag = "anime2",
operation_id = "anime2_filter",
params(FilterQuery),
responses(
(status = 200, description = "Filter results", body = FilterResponse),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn filter(
State(app_state): State<Arc<AppState>>,
Query(params): Query<FilterQuery>,
) -> Result<Json<FilterResponse>, AppError> {
info!("Handling request for anime2 filter");
let page = params.page.unwrap_or(1);
let genre = params.genre.clone();
let status = params.status.clone();
let anime_type = params.r#type.clone();
let order = params.order.clone().unwrap_or_else(|| "update".to_string());
let data = make_use_cases(&app_state)
.filter(page, genre, status, anime_type, order)
.await?;
Ok(Json(data))
}
/// GET /api/anime2/detail/{slug} — Anime detail by slug.
#[utoipa::path(
get,
path = "/api/anime2/detail/{slug}",
tag = "anime2",
operation_id = "anime2_detail_slug",
responses(
(status = 200, description = "Anime detail", body = DetailResponse),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn detail_slug(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<DetailResponse>, AppError> {
info!("Handling request for anime2 detail slug: {}", slug);
let data = make_use_cases(&app_state).detail(slug).await?;
Ok(Json(data))
}
/// GET /api/anime2/genre/{slug} — First page of genre-filtered results.
#[utoipa::path(
get,
path = "/api/anime2/genre/{slug}",
tag = "anime2",
operation_id = "anime2_genre_slug_index",
responses(
(status = 200, description = "Genre page", body = ApiResponse<Vec<GenreAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn genre_slug_index(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<ApiResponse<Vec<GenreAnimeItem>>>, AppError> {
info!("Handling request for anime2 genre slug: {}", slug);
let data = make_use_cases(&app_state).genre_slug(slug, 1).await?;
Ok(Json(data))
}
/// GET /api/anime2/genre/{slug}/{page} — Paginated genre results.
#[utoipa::path(
get,
path = "/api/anime2/genre/{slug}/{page}",
tag = "anime2",
operation_id = "anime2_genre_slug_page",
responses(
(status = 200, description = "Genre page with page", body = ApiResponse<Vec<GenreAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn genre_slug_page(
State(app_state): State<Arc<AppState>>,
Path((slug, page)): Path<(String, u32)>,
) -> Result<Json<ApiResponse<Vec<GenreAnimeItem>>>, AppError> {
info!(
"Handling request for anime2 genre slug: {} page: {}",
slug, page
);
let data = make_use_cases(&app_state).genre_slug(slug, page).await?;
Ok(Json(data))
}
/// GET /api/anime2/search/{slug} — Search anime (first page).
#[utoipa::path(
get,
path = "/api/anime2/search/{slug}",
tag = "anime2",
operation_id = "anime2_search_slug_index",
responses(
(status = 200, description = "Search results", body = ApiResponse<Vec<SearchAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn search_slug_index(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<ApiResponse<Vec<SearchAnimeItem>>>, AppError> {
info!("Handling request for anime2 search slug: {}", slug);
let data = make_use_cases(&app_state).search(slug, 1).await?;
Ok(Json(data))
}
/// GET /api/anime2/search/{slug}/{page} — Paginated search results.
#[utoipa::path(
get,
path = "/api/anime2/search/{slug}/{page}",
tag = "anime2",
operation_id = "anime2_search_slug_page",
responses(
(status = 200, description = "Search results with page", body = ApiResponse<Vec<SearchAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn search_slug_page(
State(app_state): State<Arc<AppState>>,
Path((slug, page)): Path<(String, u32)>,
) -> Result<Json<ApiResponse<Vec<SearchAnimeItem>>>, AppError> {
info!(
"Handling request for anime2 search slug: {} page: {}",
slug, page
);
let data = make_use_cases(&app_state).search(slug, page).await?;
Ok(Json(data))
}
/// GET /api/anime2/latest/{slug} — Latest anime (slug is the page number).
#[utoipa::path(
get,
path = "/api/anime2/latest/{slug}",
tag = "anime2",
operation_id = "anime2_latest_slug",
responses(
(status = 200, description = "Latest anime page", body = ApiResponse<Vec<LatestAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn latest_slug(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<ApiResponse<Vec<LatestAnimeItem>>>, AppError> {
let page = slug
.parse::<u32>()
.map_err(|_| AppError::ScraperError(format!("Invalid page number: {}", slug)))?;
info!("Handling request for anime2 latest page: {}", page);
let data = make_use_cases(&app_state).latest(page).await?;
Ok(Json(data))
}
/// GET /api/anime2/ongoing_anime/{slug} — Ongoing anime list (slug is the page number).
#[utoipa::path(
get,
path = "/api/anime2/ongoing_anime/{slug}",
tag = "anime2",
operation_id = "anime2_ongoing_anime_slug",
responses(
(status = 200, description = "Ongoing anime page", body = ApiResponse<Vec<OngoingAnimeItemWithScore>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn ongoing_anime_slug(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<ApiResponse<Vec<OngoingAnimeItemWithScore>>>, AppError> {
let page = slug
.parse::<u32>()
.map_err(|_| AppError::ScraperError(format!("Invalid page number: {}", slug)))?;
info!("Handling request for anime2 ongoing page: {}", page);
let data = make_use_cases(&app_state).ongoing_anime(page).await?;
Ok(Json(data))
}
/// GET /api/anime2/complete_anime/{slug} — Complete anime list (slug is the page number).
#[utoipa::path(
get,
path = "/api/anime2/complete_anime/{slug}",
tag = "anime2",
operation_id = "anime2_complete_anime_slug",
responses(
(status = 200, description = "Complete anime page", body = ApiResponse<Vec<CompleteAnimeItem>>),
(status = 500, description = "Internal Server Error"),
)
)]
pub async fn complete_anime_slug(
State(app_state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<ApiResponse<Vec<CompleteAnimeItem>>>, AppError> {
let page = slug
.parse::<u32>()
.map_err(|_| AppError::ScraperError(format!("Invalid page number: {}", slug)))?;
info!("Handling request for anime2 complete page: {}", page);
let data = make_use_cases(&app_state).complete_anime(page).await?;
Ok(Json(data))
}