feat: full clean architecture refactor — domain → application → infrastructure → presentation
- Restructure from Modular MVC to Clean Architecture with 4 strict layers - Domain: entities (anime/komik/proxy), Repository traits, typed errors - Application: use case classes with proper error propagation - Infrastructure: repository impls, parsers, Redis cache, HTTP/scraping, browser - Presentation: Axum handlers, DTOs, AppState, router, AppError+IntoResponse - Migrate all parsers (otakudesu, alqanime, komik) to native infra implementations - Replace once_cell::sync::Lazy/OnceCell with std::sync::LazyLock/OnceLock - Remove 150+ old files in modules/ and shared/ directories - Remove once_cell from Cargo.toml dependencies - Fix test/debug binaries to use new import paths - Zero new clippy warnings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
80c96eaa42
commit
776fa828d8
@@ -0,0 +1,294 @@
|
||||
//! 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(),
|
||||
state.db.clone(),
|
||||
Some(state.image_processing_semaphore.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))
|
||||
}
|
||||
Reference in New Issue
Block a user