feat(chapter): preserve source-site order in chapter list (0.52.0)
The user-facing chapter list ordered by (number ASC, created_at ASC),
which broke the source site's order in two ways: non-numeric entries
("notice. : Officials") parsed to number=0 and clustered at the top,
even though the site placed them mid-list, and variants sharing a
number ("Ch.14 : PH" / "Ch.14 : Official") were torn apart by the
created_at tiebreak.
Capture each chapter's position in the source DOM as `source_index`
(0 = first = newest on this site) on every crawler sync, including the
UPDATE branch so a new chapter prepended on the source shifts every
existing row down by one on the next tick. The list query reverses
this with `ORDER BY source_index DESC NULLS LAST, number ASC,
created_at ASC` so the oldest chapter appears first, variants stay
adjacent in the order the site shows them, and non-numeric entries
land where the site placed them. User-uploaded chapters and pre-
migration rows keep their NULL source_index and fall through to the
prior number/created_at tiebreak via NULLS LAST.
The reader's client-side `[...chapters].sort((a,b) => a.number - b.number)`
is dropped; prev/next now walks the server-ordered array positionally
so it traverses variants and non-numeric entries in display order.
Existing data populates on the next cron tick or via admin force-resync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,7 +352,14 @@ pub async fn sync_manga_chapters(
|
||||
.map(|c| c.source_chapter_key.clone())
|
||||
.collect();
|
||||
|
||||
for c in chapters {
|
||||
for (idx, c) in chapters.iter().enumerate() {
|
||||
// `source_index` captures the chapter's position in the source
|
||||
// DOM (0 = first = newest on this site) so the list query can
|
||||
// reverse it for the user-facing list — see migration 0021.
|
||||
// Every sync overwrites the value on both branches, so a new
|
||||
// chapter inserted at the top of the source shifts every other
|
||||
// row down by one on the next tick.
|
||||
let source_index = idx as i32;
|
||||
// Lookup is constrained by manga_id (via the chapters join) so a
|
||||
// source whose chapter slugs collide across mangas (e.g.
|
||||
// "chapter-1" appearing under two different mangas) attributes
|
||||
@@ -382,14 +389,15 @@ pub async fn sync_manga_chapters(
|
||||
// identity is the UUID, not the number.
|
||||
let (chapter_id,): (Uuid,) = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO chapters (manga_id, number, title, page_count)
|
||||
VALUES ($1, $2, $3, 0)
|
||||
INSERT INTO chapters (manga_id, number, title, page_count, source_index)
|
||||
VALUES ($1, $2, $3, 0, $4)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(manga_id)
|
||||
.bind(c.number)
|
||||
.bind(c.title.as_deref())
|
||||
.bind(source_index)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
@@ -408,8 +416,11 @@ pub async fn sync_manga_chapters(
|
||||
diff.new += 1;
|
||||
}
|
||||
Some((chapter_id,)) => {
|
||||
sqlx::query("UPDATE chapters SET title = $1 WHERE id = $2")
|
||||
sqlx::query(
|
||||
"UPDATE chapters SET title = $1, source_index = $2 WHERE id = $3",
|
||||
)
|
||||
.bind(c.title.as_deref())
|
||||
.bind(source_index)
|
||||
.bind(chapter_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user