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>
89 lines
2.9 KiB
Rust
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::`.
|