feat(mangas): content-warning filter on search + deduped banner on detail

Surfaces the analysis worker's NSFW moderation at the manga level:

- repo::manga FILTER_WHERE gains cw_include (AND, page-content-warning
  join to mangas) and cw_exclude (NONE) clauses; ListQuery + binds
  renumbered ($6/$7, LIMIT/OFFSET $8/$9).
- repo::page_analysis::warnings_for_manga: deduped, alphabetical union
  across all the manga's pages.
- domain::MangaDetail gains content_warnings; get_detail populates it.
- api::mangas list accepts cw_include/cw_exclude (reusing the shared
  parse_warnings_csv validator).

Tests: detail union (deduped/sorted) + empty case; list include/exclude
filter; unknown-warning 422. Existing manga tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 19:03:26 +02:00
parent af870bd157
commit bd39476ac7
9 changed files with 205 additions and 7 deletions

View File

@@ -77,6 +77,28 @@ pub async fn enqueue_all_pages(pool: &PgPool, only_unanalyzed: bool) -> AppResul
Ok(result.rows_affected())
}
/// Deduplicated union of content warnings across all of a manga's pages,
/// ordered alphabetically. Powers the manga-detail content-warning banner.
pub async fn warnings_for_manga(
pool: &PgPool,
manga_id: uuid::Uuid,
) -> AppResult<Vec<ContentWarning>> {
let rows = sqlx::query_scalar::<_, ContentWarning>(
r#"
SELECT DISTINCT pw.warning
FROM page_content_warnings pw
JOIN pages p ON p.id = pw.page_id
JOIN chapters c ON c.id = p.chapter_id
WHERE c.manga_id = $1
ORDER BY pw.warning
"#,
)
.bind(manga_id)
.fetch_all(pool)
.await?;
Ok(rows)
}
/// Load a page's analysis row, if it has one.
pub async fn load(pool: &PgPool, page_id: Uuid) -> AppResult<Option<PageAnalysis>> {
let row = sqlx::query_as::<_, PageAnalysis>(