Backend changes: - Update import paths: bookmann/internal → bookhoard/internal - Rename struct fields: BookmannUUID → BookhoardUUID - Update handler function names: mapContentIdToBookmannUUID → mapContentIdToBookhoardUUID - Update HTTP response headers: X-Bookmann-* → X-Bookhoard-* - Update service and middleware references - Update main.go imports and references This is part 2 of the project rename to Bookhoard.
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/v4"
|
|
)
|
|
|
|
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",
|
|
})
|
|
}
|