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

@@ -46,6 +46,11 @@ pub struct ReadProgressForManga {
pub chapter_number: Option<i32>,
pub page: i32,
pub updated_at: DateTime<Utc>,
/// Distinct chapter numbers past the last-read chapter — the detail
/// page's authoritative "new since last read" count (computed over all
/// chapters, not the client's paginated list). `0` when caught up or
/// the position is unknown. See [`ReadProgressSummary::new_chapters_count`].
pub new_chapters_count: i64,
}
#[derive(Debug, Clone, Deserialize)]

View File

@@ -80,7 +80,17 @@ pub async fn get_for_manga(
rp.chapter_id,
c.number AS chapter_number,
rp.page,
rp.updated_at
rp.updated_at,
-- Distinct chapter numbers past the reader's last-read chapter
-- (see list_for_user for the non-unique-number rationale). 0
-- when the last-read chapter is unknown.
(
SELECT count(DISTINCT c2.number)
FROM chapters c2
WHERE c2.manga_id = rp.manga_id
AND c.number IS NOT NULL
AND c2.number > c.number
) AS new_chapters_count
FROM read_progress rp
LEFT JOIN chapters c ON c.id = rp.chapter_id
WHERE rp.user_id = $1 AND rp.manga_id = $2
@@ -133,20 +143,20 @@ pub async fn list_for_user(
c.number AS chapter_number,
rp.page,
rp.updated_at,
-- Personal "new since last read": chapters numbered past the
-- reader's last-read chapter. `c.number` is NULL for
-- manga-level progress or a deleted chapter, in which case
-- the guard yields 0 rather than counting every chapter.
-- (manga_id, number) is non-unique — translations share a
-- number — so this counts positions past the reader, not
-- distinct files.
COALESCE((
SELECT count(*)
-- Personal "new since last read": how many distinct chapter
-- numbers sit past the reader's last-read chapter.
-- COUNT(DISTINCT number) — not COUNT(*) — because
-- (manga_id, number) is non-unique (scanlations share a
-- number, migration 0013), so raw rows would over-count. When
-- `c.number` is NULL (manga-level progress or a deleted
-- chapter) the predicate matches no rows, yielding 0.
(
SELECT count(DISTINCT c2.number)
FROM chapters c2
WHERE c2.manga_id = rp.manga_id
AND c.number IS NOT NULL
AND c2.number > c.number
), 0) AS new_chapters_count
) AS new_chapters_count
FROM read_progress rp
JOIN mangas m ON m.id = rp.manga_id
LEFT JOIN chapters c ON c.id = rp.chapter_id