fix(history): count distinct new chapter numbers, authoritative on detail

new_chapters_count over-counted when scanlations share a chapter number
((manga_id, number) is non-unique, migration 0013): COUNT(*) tallied rows,
so duplicate-numbered chapters inflated the badge. Switch both the list and
per-manga read-progress queries to COUNT(DISTINCT number), and drop the dead
COALESCE (an empty set counts as 0, and the NULL-number case is handled by
the predicate). Fix the misleading comment.

Expose new_chapters_count on GET /me/read-progress/:manga_id and drive the
detail page's "N new" badge from it, instead of recomputing over the
paginated chapter list — which under-counted series past 50 chapters and
disagreed with the homepage shelf. Read markers stay client-side over the
loaded chapters and are documented as by-number (all we persist).

Adds duplicate-scanlation tests at both endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 21:48:36 +02:00
parent f1349100d7
commit 3824eaafb2
12 changed files with 153 additions and 61 deletions

View File

@@ -242,6 +242,95 @@ async fn list_new_chapters_count_is_zero_without_a_read_chapter(pool: PgPool) {
assert_eq!(body["items"][0]["new_chapters_count"], 0);
}
#[sqlx::test(migrations = "./migrations")]
async fn list_new_chapters_count_dedupes_same_numbered_scanlations(pool: PgPool) {
let h = common::harness(pool);
let (_, cookie) = common::register_user(&h.app).await;
let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
// (manga_id, number) is non-unique — two scanlations of chapter 2.
// "New since last read" should count distinct chapter numbers past the
// reader (2, 3 → 2), not raw rows (2, 2, 3 → 3).
let ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
let _ = upsert_progress(
&h.app,
&cookie,
json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
)
.await;
let resp = h
.app
.clone()
.oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
.await
.unwrap();
let body = common::body_json(resp).await;
assert_eq!(body["items"][0]["new_chapters_count"], 2);
}
#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_reports_new_chapters_count(pool: PgPool) {
let h = common::harness(pool);
let (_, cookie) = common::register_user(&h.app).await;
let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
let ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
// Duplicate scanlation of chapter 3 must not inflate the count.
let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
let _ = upsert_progress(
&h.app,
&cookie,
json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
)
.await;
let resp = h
.app
.clone()
.oneshot(common::get_with_cookie(
&format!("/api/v1/me/read-progress/{manga_id}"),
&cookie,
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = common::body_json(resp).await;
// Distinct numbers past chapter 1: {2, 3} → 2.
assert_eq!(body["new_chapters_count"], 2);
}
#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_new_chapters_count_zero_without_read_chapter(pool: PgPool) {
let h = common::harness(pool);
let (_, cookie) = common::register_user(&h.app).await;
let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 1).await;
let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
// Manga-level progress (no chapter) → position unknown → 0.
let _ = upsert_progress(
&h.app,
&cookie,
json!({ "manga_id": manga_id.to_string(), "page": 1 }),
)
.await;
let resp = h
.app
.clone()
.oneshot(common::get_with_cookie(
&format!("/api/v1/me/read-progress/{manga_id}"),
&cookie,
))
.await
.unwrap();
let body = common::body_json(resp).await;
assert_eq!(body["new_chapters_count"], 0);
}
#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_returns_404_when_unread(pool: PgPool) {
let h = common::harness(pool);