//! Chapter list + get. Reads are public — anyone can browse a manga's //! table of contents and individual chapter metadata. Uploads land in //! feat/uploads under POST /api/v1/mangas/{id}/chapters. use axum::extract::{Path, Query, State}; use axum::routing::get; use axum::{Json, Router}; use serde::Deserialize; use uuid::Uuid; use crate::api::pagination::PagedResponse; use crate::app::AppState; use crate::domain::Chapter; use crate::error::AppResult; use crate::repo; pub fn routes() -> Router { Router::new() .route("/mangas/:manga_id/chapters", get(list)) .route("/mangas/:manga_id/chapters/:number", get(get_one)) } #[derive(Debug, Deserialize)] pub struct ListParams { #[serde(default = "default_limit")] pub limit: i64, #[serde(default)] pub offset: i64, } fn default_limit() -> i64 { 50 } async fn list( State(state): State, Path(manga_id): Path, Query(params): Query, ) -> AppResult>> { // Surface 404 when the parent manga doesn't exist so an empty result // can't be mistaken for "no chapters yet" on a real manga. repo::manga::get(&state.db, manga_id).await?; let limit = params.limit.clamp(1, 200); let offset = params.offset.max(0); let items = repo::chapter::list_for_manga(&state.db, manga_id, limit, offset).await?; Ok(Json(PagedResponse::new(items, limit, offset))) } async fn get_one( State(state): State, Path((manga_id, number)): Path<(Uuid, i32)>, ) -> AppResult> { repo::manga::get(&state.db, manga_id).await?; let chapter = repo::chapter::find_by_manga_and_number(&state.db, manga_id, number) .await? .ok_or(crate::error::AppError::NotFound)?; Ok(Json(chapter)) }