Files
bookhoard/internal/handlers/sync.go
john-okeefe 1e05470fbb refactor(handlers): update all handlers for Echo v5 compatibility
Update all handler functions to use *echo.Context (pointer) instead of echo.Context (value) as required by Echo v5.

Changes across all handler files:
- analytics.go: Update handler signatures
- auth.go: Update authentication handler signatures
- book_matching.go: Update matching handler signatures
- collections.go: Update collection handler signatures
- collections_preview_test.go: Update test signatures
- commonhandlers.go: Update common handler signatures
- conflicts.go: Update conflict handler signatures
- context.go: Update context handler signatures
- dashboard.go: Update dashboard handler signatures
- devices.go: Update device handler signatures
- jobs.go: Update job handler signatures
- kobo.go: Update Kobo handler signatures
- koreader.go: Update Koreader handler signatures
- library.go: Update library handler signatures
- matching.go: Update matching handler signatures
- media.go: Update media handler signatures
- opds.go: Update OPDS handler signatures
- progress.go: Update progress handler signatures
- queue.go: Update queue handler signatures
- refresh_token.go: Update token handler signatures
- scanner.go: Update scanner handler signatures
- sidecar.go: Update sidecar handler signatures
- sync.go: Update sync handler signatures
- system_settings.go: Update settings handler signatures
- websocket.go: Update WebSocket handler signatures

All handlers now properly implement Echo v5's pointer-based context pattern.
This change is necessary for type safety and compatibility with Echo v5's
improved context handling and WebSocket support.
2026-03-06 14:00:28 -05:00

155 lines
4.6 KiB
Go

package handlers
import (
"bookhoard/internal/database"
"net/http"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v5"
)
type SyncHandler struct {
db *database.Queries
}
func NewSyncHandler(db *database.Queries) *SyncHandler {
return &SyncHandler{db: db}
}
type UnlinkedBookResponse struct {
Unlinked []UnlinkedBookItem `json:"unlinked"`
Total int `json:"total"`
}
type UnlinkedBookItem struct {
UnlinkedBookID string `json:"unlinked_book_id"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
ContentId string `json:"content_id"`
FilePath string `json:"file_path,omitempty"`
Title string `json:"title,omitempty"`
Author string `json:"author,omitempty"`
ConfidenceScore float64 `json:"confidence_score"`
LastSeenAt time.Time `json:"last_seen_at"`
CreatedAt time.Time `json:"created_at"`
}
type LinkBookRequest struct {
UnlinkedBookID string `json:"unlinked_book_id"`
MediaItemID string `json:"media_item_id"`
ConfidenceScore float64 `json:"confidence_score"`
}
type LinkBookResponse struct {
Status string `json:"status"`
UnlinkedBookID string `json:"unlinked_book_id"`
MediaItemID string `json:"media_item_id"`
Message string `json:"message"`
}
// GetUnlinkedBooks returns all unlinked books for a user's devices
// GET /api/sync/unlinked-books
func (h *SyncHandler) GetUnlinkedBooks(c *echo.Context) error {
// Get user from context (assuming auth middleware sets this)
user := c.Get("user")
if user == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "unauthorized",
})
}
userID := user.(database.Users).ID
// Get all devices for this user
devices, err := h.db.ListDevicesByUser(c.Request().Context(), pgtype.UUID{Bytes: userID.Bytes, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to fetch devices",
})
}
var allUnlinkedBooks []UnlinkedBookItem
// For each device, get unlinked books
for _, device := range devices {
unlinkedBooks, err := h.db.GetUnlinkedBooksByDevice(c.Request().Context(), pgtype.UUID{Bytes: device.ID.Bytes, Valid: true})
if err != nil {
continue
}
for _, ub := range unlinkedBooks {
allUnlinkedBooks = append(allUnlinkedBooks, UnlinkedBookItem{
UnlinkedBookID: uuid.UUID(ub.ID.Bytes).String(),
DeviceID: uuid.UUID(ub.DeviceID.Bytes).String(),
DeviceName: ub.DeviceName,
DeviceType: ub.DeviceType,
ContentId: ub.ContentID,
FilePath: ub.FilePath.String,
Title: ub.Title.String,
Author: ub.Author.String,
ConfidenceScore: ub.ConfidenceScore.Float64,
LastSeenAt: ub.LastSeenAt.Time,
CreatedAt: ub.CreatedAt.Time,
})
}
}
return c.JSON(http.StatusOK, UnlinkedBookResponse{
Unlinked: allUnlinkedBooks,
Total: len(allUnlinkedBooks),
})
}
// LinkBook manually links an unlinked book to a media item
// POST /api/sync/link-book
func (h *SyncHandler) LinkBook(c *echo.Context) error {
var req LinkBookRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request format",
})
}
// Parse UUIDs
unlinkedBookID, err := uuid.Parse(req.UnlinkedBookID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid unlinked_book_id",
})
}
mediaItemID, err := uuid.Parse(req.MediaItemID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid media_item_id",
})
}
// Get the unlinked books to find the one we're looking for
// We need to use GetAllUnlinkedBooks or create a new query
// For now, let's link directly using LinkUnlinkedBook
_, err = h.db.LinkUnlinkedBook(c.Request().Context(), database.LinkUnlinkedBookParams{
ID: pgtype.UUID{Bytes: unlinkedBookID, Valid: true},
MediaItemID: pgtype.UUID{Bytes: mediaItemID, Valid: true},
ConfidenceScore: pgtype.Float8{Float64: req.ConfidenceScore, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to link book",
})
}
// Note: We can't create a device catalog entry without knowing the device_id and content_id
// In a real implementation, we'd first fetch the unlinked book, then create the catalog entry
return c.JSON(http.StatusOK, LinkBookResponse{
Status: "linked",
UnlinkedBookID: req.UnlinkedBookID,
MediaItemID: req.MediaItemID,
Message: "Book successfully linked",
})
}