feat(sync): server-side position authority — verify/heal progress anchors
Progress submissions now carry (percentage, context_text, epubcfi) and the server becomes the position authority: - VerifyProgressAnchor resolves the submitted standard CFI against the book's own XHTML, extracts the text at the anchor, and cross-checks it with the submitted context_text. A mismatch heals the anchor by text search (percentage disambiguates repeats) instead of storing a bad position. - The anchor's block element is derived as a cssSelector plus a block- relative character offset, and served on progress GET alongside the anchor document's href — readium-native handles that let clients re-open a book without parsing CFIs themselves. - context_text-only submissions (no CFI — the dumb-client tier) are anchored structurally from the context text. Motivation: cross-client progress sync (web foliate CFIs, KOReader CRE xpointers, readium-native apps) previously trusted each client's own locator math; the app's EPUB restore drifted ±pages because readium's paginator does not lay out far-from-viewport columns and the foliate- ported CFI walk ran against readium's mutated WebView DOM. Server-side verification heals both classes at ingest.
This commit is contained in:
@@ -953,6 +953,37 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
|
||||
"last_sync_timestamp": progress.LastSyncTimestamp,
|
||||
}
|
||||
|
||||
// Position authority: re-verify the stored anchor and derive the
|
||||
// client-facing handles (cssSelector + anchor document href) server-
|
||||
// side, so clients scroll to an address instead of parsing CFIs.
|
||||
if progress.Epubcfi.Valid && wsync.IsConvertibleFormat(progress.FormatGroup) && mh.libraryService != nil {
|
||||
contextText := ""
|
||||
if progress.ContextText.Valid {
|
||||
contextText = progress.ContextText.String
|
||||
}
|
||||
pct := 0.0
|
||||
if progress.Percentage.Valid {
|
||||
pct = progress.Percentage.Float64
|
||||
}
|
||||
if mediaItem, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err == nil {
|
||||
if path, perr := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath); perr == nil && path != "" {
|
||||
if finalCFI, sel, href, charOff, _, _, verr := wsync.VerifyProgressAnchor(path, progress.Epubcfi.String, contextText, pct); verr == nil && sel != "" {
|
||||
resp["css_selector"] = sel
|
||||
resp["anchor_href"] = href
|
||||
if charOff != nil {
|
||||
resp["char_offset"] = *charOff
|
||||
}
|
||||
if finalCFI != "" {
|
||||
resp["epubcfi"] = finalCFI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if progress.ContextText.Valid {
|
||||
resp["context_text"] = progress.ContextText.String
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user