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.
155 lines
4.6 KiB
Go
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",
|
|
})
|
|
}
|