Files
Mangalord/backend/src/domain/manga.rs
MechaCat02 790549636f
Some checks failed
deploy / test-backend (push) Waiting to run
deploy / build-and-push (push) Has been cancelled
deploy / deploy (push) Has been cancelled
deploy / test-frontend (push) Has been cancelled
feat(storage): admin storage-usage stats + per-manga/chapter sizes
Persist blob sizes (covers + chapter pages) and surface upload storage
usage to admins in two places:

- Admin System tab: total/covers/chapters totals, ratio of disk, average
  image sizes, and top-5 largest mangas/chapters leaderboards, behind a
  new GET /api/v1/admin/storage endpoint (separate from /admin/system so
  the cheap DB aggregates aren't coupled to its 250ms CPU sample).
- Manga detail page: total chapter-content size and per-chapter size,
  with an em-dash for uncrawled or not-yet-measured chapters.

Sizes are captured at write time (crawler put_stream return, uploaded
page/cover byte length) into nullable pages.size_bytes /
mangas.cover_size_bytes columns; NULL means "not yet measured", distinct
from a real 0. A one-shot, idempotent admin "Backfill sizes" action
(POST /api/v1/admin/storage/backfill) stats pre-existing blobs in
keyset-paginated, capped batches and reports more_remaining so a large
legacy library can be drained over multiple runs.

Aggregates and leaderboards treat NULL honestly (only fully-measured
entities are ranked); a dashboard banner flags unmeasured rows so partial
figures aren't read as complete. persist_pages now prunes stale page rows
on a shrinking re-crawl so totals and page_count stay consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:18:41 +02:00

89 lines
2.9 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use super::author::AuthorRef;
use super::genre::GenreRef;
use super::page_analysis::ContentWarning;
use super::patch::Patch;
use super::tag::TagRef;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Manga {
pub id: Uuid,
pub title: String,
pub status: String,
pub alt_titles: Vec<String>,
pub description: Option<String>,
pub cover_image_path: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Shape returned by list endpoints. Cards show the title, authors and
/// genres; tags are intentionally elided here to keep the response
/// proportional to what a card actually renders.
#[derive(Debug, Clone, Serialize)]
pub struct MangaCard {
#[serde(flatten)]
pub manga: Manga,
pub authors: Vec<AuthorRef>,
pub genres: Vec<GenreRef>,
}
/// Shape returned by `GET /mangas/:id`. Adds user-added tags on top of
/// the card fields, plus the deduped content warnings derived by the
/// analysis worker across all the manga's pages.
#[derive(Debug, Clone, Serialize)]
pub struct MangaDetail {
#[serde(flatten)]
pub manga: Manga,
pub authors: Vec<AuthorRef>,
pub genres: Vec<GenreRef>,
pub tags: Vec<TagRef>,
pub content_warnings: Vec<ContentWarning>,
/// Total bytes of all this manga's stored chapter pages (cover
/// excluded), over every chapter. `None` when any page is unmeasured
/// (frontend shows an em-dash); `Some(0)` when there are no pages.
/// Computed in the DB rather than summed from the chapter list, which
/// is paginated and would undercount mangas with many chapters.
pub chapter_storage_bytes: Option<i64>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct NewManga {
pub title: String,
#[serde(default)]
pub status: Option<String>,
/// Author display names. Looked up case-insensitively; created on
/// the fly when unseen.
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub alt_titles: Vec<String>,
#[serde(default)]
pub genre_ids: Vec<Uuid>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct MangaPatch {
pub title: Option<String>,
pub status: Option<String>,
/// Three-state: missing key leaves the description alone; explicit
/// `null` clears it; a string sets it. See `Patch` for details.
#[serde(default)]
pub description: Patch<String>,
pub alt_titles: Option<Vec<String>>,
/// When provided, replaces the manga's authors atomically.
pub authors: Option<Vec<String>>,
/// When provided, replaces the manga's genres atomically.
pub genre_ids: Option<Vec<Uuid>>,
}
// `Patch<T>` lives in `super::patch` so other resources (collections,
// future PATCH endpoints) can reuse the same three-state semantics
// without re-importing through `manga::`.