feat(koreader): resolve pushes by SHA via BookResolver and return SHA on pull
Fixes the 'cannot push until pulling first' wall on books downloaded via OPDS. Root cause chain: the bookhoard koreader plugin only learns the book UUID from a successful push response, but the first push had to match by SHA-256 alone - and that match consulted only media_items.file_sha256, missing converted formats. When the hash missed, no UUID was returned, so pull stayed blocked (it requires the UUID) and the book could not sync at all. Resolution side - route all five SHA-256 match sites through the shared BookResolver so they are format-aware: - resolveBookToMediaItem priority 2 - SyncBookmarks book-level lookup - per-bookmark, per-note, and per-highlight override lookups Exposure side - return the canonical hash so clients can learn and cache it from a pull regardless of how the book was obtained: - KOReaderMetadata gains sha256, populated from mediaItem.FileSha256 - KOReaderLibraryBook gains sha256, populated the same way, so the library list endpoint carries it for every book Together with the plugin-side UUID bootstrap (bookhoard.koplugin), push and pull now work in either order on any format.
This commit is contained in:
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
wsync "bookhoard/internal/sync"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -22,6 +23,7 @@ type KOReaderHandler struct {
|
||||
progressSvc *wsync.ProgressService
|
||||
annotationSvc *wsync.AnnotationService
|
||||
libraryService LibraryPathResolver
|
||||
bookResolver *services.BookResolver
|
||||
}
|
||||
|
||||
type LibraryPathResolver interface {
|
||||
@@ -29,7 +31,12 @@ type LibraryPathResolver interface {
|
||||
}
|
||||
|
||||
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor) *KOReaderHandler {
|
||||
return &KOReaderHandler{db: db, connManager: connManager, queue: queue}
|
||||
return &KOReaderHandler{
|
||||
db: db,
|
||||
connManager: connManager,
|
||||
queue: queue,
|
||||
bookResolver: services.NewBookResolver(db),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) SetProgressService(svc *wsync.ProgressService) {
|
||||
@@ -158,6 +165,7 @@ type KOReaderConflict struct {
|
||||
|
||||
type KOReaderMetadata struct {
|
||||
UUID string `json:"uuid"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Authors []string `json:"authors"`
|
||||
Progress KOReaderProgressData `json:"progress"`
|
||||
@@ -192,6 +200,7 @@ type KOReaderLibraryResponse struct {
|
||||
|
||||
type KOReaderLibraryBook struct {
|
||||
UUID string `json:"uuid"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
ContentType string `json:"content_type"`
|
||||
@@ -303,8 +312,10 @@ func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtyp
|
||||
}
|
||||
|
||||
// Priority 2: SHA-256 provided (medium confidence - 0.9)
|
||||
// Uses the shared BookResolver, which also checks per-format hashes
|
||||
// (media_item_formats) so a converted file (KEPUB/PDF) matches too.
|
||||
if book.SHA256 != "" && len(book.SHA256) == 64 {
|
||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: book.SHA256, Valid: true})
|
||||
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, book.SHA256)
|
||||
if err == nil {
|
||||
// Create device file alias if FilePath is provided
|
||||
if book.FilePath != "" {
|
||||
@@ -883,6 +894,7 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
||||
|
||||
metadata := KOReaderMetadata{
|
||||
UUID: bookUUID.String(),
|
||||
SHA256: mediaItem.FileSha256.String,
|
||||
Title: mediaItem.Title,
|
||||
Authors: []string{mediaItem.Author.String},
|
||||
Progress: progressData,
|
||||
@@ -989,6 +1001,7 @@ func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
|
||||
|
||||
libraryBooks = append(libraryBooks, KOReaderLibraryBook{
|
||||
UUID: uuid.UUID(item.ID.Bytes).String(),
|
||||
SHA256: item.FileSha256.String,
|
||||
Title: item.Title,
|
||||
Author: item.Author.String,
|
||||
ContentType: "6",
|
||||
@@ -1045,8 +1058,8 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
||||
}
|
||||
pgBookUUID = pgtype.UUID{Bytes: bookUUID, Valid: true}
|
||||
} else if req.BookSHA256 != "" && len(req.BookSHA256) == 64 {
|
||||
// Use SHA-256 to find book
|
||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: req.BookSHA256, Valid: true})
|
||||
// Use SHA-256 to find book (format-aware: also checks media_item_formats)
|
||||
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, req.BookSHA256)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "book not found by SHA-256",
|
||||
@@ -1068,7 +1081,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
||||
|
||||
// If bookmark has its own SHA-256, use it for matching
|
||||
if bookmark.BookSHA256 != "" && len(bookmark.BookSHA256) == 64 {
|
||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: bookmark.BookSHA256, Valid: true})
|
||||
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, bookmark.BookSHA256)
|
||||
if err == nil {
|
||||
mediaItemID = mediaItem.ID
|
||||
}
|
||||
@@ -1119,7 +1132,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
||||
|
||||
// If note has its own SHA-256, use it for matching
|
||||
if note.BookSHA256 != "" && len(note.BookSHA256) == 64 {
|
||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: note.BookSHA256, Valid: true})
|
||||
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, note.BookSHA256)
|
||||
if err == nil {
|
||||
mediaItemID = mediaItem.ID
|
||||
}
|
||||
@@ -1169,7 +1182,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
||||
|
||||
// If highlight has its own SHA-256, use it for matching
|
||||
if highlight.BookSHA256 != "" && len(highlight.BookSHA256) == 64 {
|
||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: highlight.BookSHA256, Valid: true})
|
||||
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, highlight.BookSHA256)
|
||||
if err == nil {
|
||||
mediaItemID = mediaItem.ID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user