diff --git a/internal/handlers/koreader.go b/internal/handlers/koreader.go index 9771a66..d2a2826 100644 --- a/internal/handlers/koreader.go +++ b/internal/handlers/koreader.go @@ -883,6 +883,43 @@ func int64PtrToPgInt8(i *int64) pgtype.Int8 { return pgtype.Int8{} } +type KOReaderResolveResponse struct { + BookUUID string `json:"book_uuid"` + SHA256 string `json:"sha256,omitempty"` + Title string `json:"title,omitempty"` + Author string `json:"author,omitempty"` +} + +// ResolveBook maps a file SHA-256 to the book's UUID without touching any +// progress state. Devices need the UUID to pull metadata, but a freshly +// downloaded book has none cached yet — the old way of learning it was to +// push once, which transmitted the device's first-page position to the +// server and manufactured a progress conflict for books already mid-read +// from another source. This read-only lookup lets the client link (and +// pull) without ever pushing bootstrap progress. +func (h *KOReaderHandler) ResolveBook(c *echo.Context) error { + sha256 := c.QueryParam("sha256") + if sha256 == "" || len(sha256) != 64 { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "sha256 query parameter is required (64 hex characters)", + }) + } + + mediaItem, _, err := h.bookResolver.ResolveBySHA256(c.Request().Context(), sha256) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{ + "error": "book not found", + }) + } + + return c.JSON(http.StatusOK, KOReaderResolveResponse{ + BookUUID: uuid.UUID(mediaItem.ID.Bytes).String(), + SHA256: sha256, + Title: mediaItem.Title, + Author: mediaItem.Author.String, + }) +} + func (h *KOReaderHandler) GetMetadata(c *echo.Context) error { device := c.Get("device").(database.Devices) userID := device.UserID.Bytes diff --git a/internal/router/sync.go b/internal/router/sync.go index 406e453..3e0db37 100644 --- a/internal/router/sync.go +++ b/internal/router/sync.go @@ -24,6 +24,7 @@ func registerSyncRoutes(cfg *Config) { // KOReader sync routes (device authentication required) koreaderSync := e.Group("/api/sync/koreader") koreaderSync.POST("/progress", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.SyncProgress)) + koreaderSync.GET("/resolve", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.ResolveBook)) koreaderSync.GET("/metadata/:uuid", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.GetMetadata)) koreaderSync.GET("/library", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.GetLibrary)) koreaderSync.POST("/bookmarks", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.SyncBookmarks))