feat(history): report personal new-chapter count on read-progress list
Add `new_chapters_count` to each `GET /me/read-progress` item: how many chapters sit past the reader's last-read chapter, computed by a correlated subquery on chapter number. It is 0 when the reader is caught up, and 0 when the last-read chapter is unknown (manga-level progress or a deleted chapter) so we never over-claim. (manga_id, number) is non-unique, so this counts positions past the reader rather than distinct files. Backs the personal "new since last read" indicator on the upcoming Continue-reading shelf. Tests cover the mid-series, caught-up, and no-read-chapter cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.94.0"
|
version = "0.94.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.94.1"
|
version = "0.95.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mangalord"
|
default-run = "mangalord"
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ pub struct ReadProgressSummary {
|
|||||||
pub chapter_number: Option<i32>,
|
pub chapter_number: Option<i32>,
|
||||||
pub page: i32,
|
pub page: i32,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
|
/// How many chapters sit past the reader's last-read chapter (by
|
||||||
|
/// chapter number) — a personal "new since you last read" count.
|
||||||
|
/// `0` when the reader is caught up or when `chapter_number` is
|
||||||
|
/// unknown (manga-level progress or a deleted chapter), so we never
|
||||||
|
/// claim chapters are new when we can't place the reader.
|
||||||
|
pub new_chapters_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by `GET /me/read-progress/:manga_id`. Same shape as
|
/// Returned by `GET /me/read-progress/:manga_id`. Same shape as
|
||||||
|
|||||||
@@ -132,7 +132,21 @@ pub async fn list_for_user(
|
|||||||
rp.chapter_id,
|
rp.chapter_id,
|
||||||
c.number AS chapter_number,
|
c.number AS chapter_number,
|
||||||
rp.page,
|
rp.page,
|
||||||
rp.updated_at
|
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(*)
|
||||||
|
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
|
||||||
FROM read_progress rp
|
FROM read_progress rp
|
||||||
JOIN mangas m ON m.id = rp.manga_id
|
JOIN mangas m ON m.id = rp.manga_id
|
||||||
LEFT JOIN chapters c ON c.id = rp.chapter_id
|
LEFT JOIN chapters c ON c.id = rp.chapter_id
|
||||||
|
|||||||
@@ -164,6 +164,84 @@ async fn list_is_per_user_only(pool: PgPool) {
|
|||||||
assert_eq!(body["items"], json!([]));
|
assert_eq!(body["items"], json!([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn list_reports_new_chapters_since_last_read(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;
|
||||||
|
// Three chapters exist; the reader is on chapter 1, so chapters 2 and
|
||||||
|
// 3 are "new since last read".
|
||||||
|
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, 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 list_new_chapters_count_is_zero_at_latest_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 ch2 = seed_chapter(&h.app, &cookie, manga_id, 2).await;
|
||||||
|
// Caught up to the last chapter → nothing newer.
|
||||||
|
let _ = upsert_progress(
|
||||||
|
&h.app,
|
||||||
|
&cookie,
|
||||||
|
json!({ "manga_id": manga_id.to_string(), "chapter_id": ch2, "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"], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn list_new_chapters_count_is_zero_without_a_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;
|
||||||
|
// Progress recorded without a chapter (manga-level) — we can't place
|
||||||
|
// the reader among the chapters, so we don't claim any are "new".
|
||||||
|
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("/api/v1/me/read-progress", &cookie))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let body = common::body_json(resp).await;
|
||||||
|
assert_eq!(body["items"][0]["new_chapters_count"], 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn get_single_manga_returns_404_when_unread(pool: PgPool) {
|
async fn get_single_manga_returns_404_when_unread(pool: PgPool) {
|
||||||
let h = common::harness(pool);
|
let h = common::harness(pool);
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.94.1",
|
"version": "0.95.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.94.1",
|
"version": "0.95.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lucide/svelte": "^1.16.0",
|
"@lucide/svelte": "^1.16.0",
|
||||||
"@playwright/test": "^1.48.0",
|
"@playwright/test": "^1.48.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.94.1",
|
"version": "0.95.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user