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 (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/services"
|
||||||
wsync "bookhoard/internal/sync"
|
wsync "bookhoard/internal/sync"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -22,6 +23,7 @@ type KOReaderHandler struct {
|
|||||||
progressSvc *wsync.ProgressService
|
progressSvc *wsync.ProgressService
|
||||||
annotationSvc *wsync.AnnotationService
|
annotationSvc *wsync.AnnotationService
|
||||||
libraryService LibraryPathResolver
|
libraryService LibraryPathResolver
|
||||||
|
bookResolver *services.BookResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
type LibraryPathResolver interface {
|
type LibraryPathResolver interface {
|
||||||
@@ -29,7 +31,12 @@ type LibraryPathResolver interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor) *KOReaderHandler {
|
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) {
|
func (h *KOReaderHandler) SetProgressService(svc *wsync.ProgressService) {
|
||||||
@@ -158,6 +165,7 @@ type KOReaderConflict struct {
|
|||||||
|
|
||||||
type KOReaderMetadata struct {
|
type KOReaderMetadata struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Authors []string `json:"authors"`
|
Authors []string `json:"authors"`
|
||||||
Progress KOReaderProgressData `json:"progress"`
|
Progress KOReaderProgressData `json:"progress"`
|
||||||
@@ -192,6 +200,7 @@ type KOReaderLibraryResponse struct {
|
|||||||
|
|
||||||
type KOReaderLibraryBook struct {
|
type KOReaderLibraryBook struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
ContentType string `json:"content_type"`
|
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)
|
// 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 {
|
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 {
|
if err == nil {
|
||||||
// Create device file alias if FilePath is provided
|
// Create device file alias if FilePath is provided
|
||||||
if book.FilePath != "" {
|
if book.FilePath != "" {
|
||||||
@@ -883,6 +894,7 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
|||||||
|
|
||||||
metadata := KOReaderMetadata{
|
metadata := KOReaderMetadata{
|
||||||
UUID: bookUUID.String(),
|
UUID: bookUUID.String(),
|
||||||
|
SHA256: mediaItem.FileSha256.String,
|
||||||
Title: mediaItem.Title,
|
Title: mediaItem.Title,
|
||||||
Authors: []string{mediaItem.Author.String},
|
Authors: []string{mediaItem.Author.String},
|
||||||
Progress: progressData,
|
Progress: progressData,
|
||||||
@@ -989,6 +1001,7 @@ func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
|
|||||||
|
|
||||||
libraryBooks = append(libraryBooks, KOReaderLibraryBook{
|
libraryBooks = append(libraryBooks, KOReaderLibraryBook{
|
||||||
UUID: uuid.UUID(item.ID.Bytes).String(),
|
UUID: uuid.UUID(item.ID.Bytes).String(),
|
||||||
|
SHA256: item.FileSha256.String,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Author: item.Author.String,
|
Author: item.Author.String,
|
||||||
ContentType: "6",
|
ContentType: "6",
|
||||||
@@ -1045,8 +1058,8 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
pgBookUUID = pgtype.UUID{Bytes: bookUUID, Valid: true}
|
pgBookUUID = pgtype.UUID{Bytes: bookUUID, Valid: true}
|
||||||
} else if req.BookSHA256 != "" && len(req.BookSHA256) == 64 {
|
} else if req.BookSHA256 != "" && len(req.BookSHA256) == 64 {
|
||||||
// Use SHA-256 to find book
|
// Use SHA-256 to find book (format-aware: also checks media_item_formats)
|
||||||
mediaItem, err := h.db.GetMediaItemBySHA256(ctx, pgtype.Text{String: req.BookSHA256, Valid: true})
|
mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, req.BookSHA256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusNotFound, map[string]string{
|
return c.JSON(http.StatusNotFound, map[string]string{
|
||||||
"error": "book not found by SHA-256",
|
"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 has its own SHA-256, use it for matching
|
||||||
if bookmark.BookSHA256 != "" && len(bookmark.BookSHA256) == 64 {
|
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 {
|
if err == nil {
|
||||||
mediaItemID = mediaItem.ID
|
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 has its own SHA-256, use it for matching
|
||||||
if note.BookSHA256 != "" && len(note.BookSHA256) == 64 {
|
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 {
|
if err == nil {
|
||||||
mediaItemID = mediaItem.ID
|
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 has its own SHA-256, use it for matching
|
||||||
if highlight.BookSHA256 != "" && len(highlight.BookSHA256) == 64 {
|
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 {
|
if err == nil {
|
||||||
mediaItemID = mediaItem.ID
|
mediaItemID = mediaItem.ID
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user