#4932·Kavita

KOReader sync: a position with fewer than 6 path parts keeps the previous BookScrollId

Author: stonesignalCreated Sep 16, 2026Updated Sep 16, 2026

Version

Kavita v0.9.1.4 (docker jvmilazz0/kavita:latest). Same code is on develop.

What happens

KoreaderHelper.UpdateProgressDto bails out early when the incoming position splits into fewer than 6 parts:

csharp
var path = koreaderPosition.Split('/');
if (path.Length < 6)
{
    if (path.Length >= 3) progress.PageNum = GetPageNumber(path);
    return;                       // BookScrollId is left as it was
}

A position whose element is a direct child of <body> and that carries a .N offset instead of /text().N has exactly 5 parts:

/body/DocFragment[28]/body/p[99].0        ->  ["", "body", "DocFragment[28]", "body", "p[99].0"]
/body/DocFragment[27]/body/p[43]          ->  5 parts as well

Kavita then updates PageNum (the chapter) but keeps whatever BookScrollId was stored earlier, so the reader is sent to a stale paragraph, or to the chapter heading, or to DocFragment[N].0 if the previous scroll id was empty.

From the Debug log:

Saving KOReader progress for User (3): /body/DocFragment[28]/body/p[99].0 - F787...
Converted KOReader progress from /body/DocFragment[28]/body/p[99].0 to Page 27 with ScrollId: //body/p[27].

//body/p[27] is a leftover from an earlier push of /body/DocFragment[25]/body/p[27]/text().709.

KOReader itself usually sends /text().N, which makes 6 parts and works. Other KOReader-sync clients (justRead on iOS sends .../p[43], and progress bridges send .../p[99].0) hit the 5-part case on every push for epubs whose paragraphs sit directly under <body>.

Expected

/body/DocFragment[28]/body/p[99].0 should store PageNum = 27, BookScrollId = //body/p[99].

Suggested fix

Strip the trailing .N / /text()... before counting, and take lastTag from the last element rather than path[5], so that a 5-part path is handled the same as a 6-part one. Two smaller things noticed in the same area, happy to split them out:

  • KoreaderBookDtoBuilder.WithTimestamp does new DateTimeOffset(lastModifiedUtc) on a DateTime whose Kind is Unspecified, so the returned timestamp is shifted by the server's UTC offset (5 hours in the future here). KOReader compares this against its own last page turn to decide forward vs backward sync.
  • percentage is PageNum / file.Pages, i.e. chapter granularity, so a book at 99.9% reports 80/81 = 98.8%.