feat(sync): add read-only KOReader book resolve endpoint
GET /api/sync/koreader/resolve?sha256={hash} maps a file content hash to
the book's UUID through the shared format-aware BookResolver (primary
media_items hash, then per-format hashes so converted KEPUB/PDF files
match) without touching any progress state.
Devices need the UUID to pull metadata, but a freshly downloaded book has
none cached. The old way of learning it was to push once, which
transmitted the device's first-page position and manufactured a progress
conflict for books already mid-read from another source. A read-only
lookup lets clients link (and pull) without ever pushing bootstrap
progress: resolve, then pull, then push.
Returns 200 {book_uuid, sha256, title, author}, 400 for a missing or
malformed hash, 404 when no library item matches.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user