- Add OPDS handler for device catalog and book downloads - Add collections handler for collection CRUD - Add book matching service for cross-device book linking - Add sidecar handler for Kobo metadata sync - Add sync handler for device synchronization
490 lines
16 KiB
Go
490 lines
16 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookmann/internal/database"
|
|
"bookmann/internal/services"
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type CollectionHandler struct {
|
|
db *database.Queries
|
|
collectionService *services.CollectionService
|
|
}
|
|
|
|
func NewCollectionHandler(db *database.Queries) *CollectionHandler {
|
|
return &CollectionHandler{
|
|
db: db,
|
|
collectionService: services.NewCollectionService(db),
|
|
}
|
|
}
|
|
|
|
type CreateCollectionRequest struct {
|
|
Name string `json:"name" validate:"required"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Icon string `json:"icon"`
|
|
AutoAssignRules []services.Rule `json:"auto_assign_rules"`
|
|
ViewSettings map[string]interface{} `json:"view_settings"`
|
|
}
|
|
|
|
type UpdateCollectionRequest struct {
|
|
Name string `json:"name" validate:"required"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Icon string `json:"icon"`
|
|
AutoAssignRules []services.Rule `json:"auto_assign_rules"`
|
|
ViewSettings map[string]interface{} `json:"view_settings"`
|
|
}
|
|
|
|
type AddBooksRequest struct {
|
|
BookIDs []string `json:"book_ids" validate:"required"`
|
|
}
|
|
|
|
type CreateDeviceMappingRequest struct {
|
|
CollectionID string `json:"collection_id" validate:"required"`
|
|
DeviceShelfName string `json:"device_shelf_name" validate:"required"`
|
|
SyncDirection string `json:"sync_direction" validate:"required,oneof=bidirectional book_to_device device_to_book none"`
|
|
}
|
|
|
|
type UpdateDeviceMappingRequest struct {
|
|
DeviceShelfName string `json:"device_shelf_name" validate:"required"`
|
|
SyncDirection string `json:"sync_direction" validate:"required,oneof=bidirectional book_to_device device_to_book none"`
|
|
}
|
|
|
|
func (h *CollectionHandler) CreateCollection(c echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req CreateCollectionRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
collection, err := h.collectionService.CreateCollection(
|
|
c.Request().Context(),
|
|
userUUID,
|
|
req.Name,
|
|
req.Description,
|
|
req.Color,
|
|
req.Icon,
|
|
req.AutoAssignRules,
|
|
req.ViewSettings,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
bookCount := int32(0)
|
|
return c.JSON(http.StatusCreated, map[string]interface{}{
|
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
|
"name": collection.Name,
|
|
"description": textToString(collection.Description),
|
|
"color": textToString(collection.Color),
|
|
"icon": textToString(collection.Icon),
|
|
"auto_assign_rules": collection.AutoAssignRules,
|
|
"view_settings": collection.ViewSettings,
|
|
"book_count": bookCount,
|
|
"created_at": collection.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) GetCollections(c echo.Context) error {
|
|
includeAuto := c.QueryParam("include_auto") == "true"
|
|
sortBy := c.QueryParam("sort_by")
|
|
|
|
collections, err := h.GetCollectionsData(c)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
type CollectionResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Icon string `json:"icon"`
|
|
AutoAssignRules json.RawMessage `json:"auto_assign_rules"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
response := make([]CollectionResponse, 0, len(collections))
|
|
for _, col := range collections {
|
|
if !includeAuto && len(col.AutoAssignRules) > 0 {
|
|
continue
|
|
}
|
|
response = append(response, CollectionResponse{
|
|
ID: uuid.UUID(col.ID.Bytes),
|
|
Name: col.Name,
|
|
Description: textToString(col.Description),
|
|
Color: textToString(col.Color),
|
|
Icon: textToString(col.Icon),
|
|
AutoAssignRules: json.RawMessage(col.AutoAssignRules),
|
|
CreatedAt: col.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
if sortBy == "name" {
|
|
for i := 0; i < len(response); i++ {
|
|
for j := i + 1; j < len(response); j++ {
|
|
if response[i].Name > response[j].Name {
|
|
response[i], response[j] = response[j], response[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"collections": response,
|
|
"total": len(response),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) GetCollection(c echo.Context) error {
|
|
collectionID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
collection, err := h.GetCollectionData(c, collectionID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "collection not found"})
|
|
}
|
|
|
|
books, err := h.GetCollectionBooksData(c, collectionID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
type BookInfo struct {
|
|
MediaItemID string `json:"media_item_id"`
|
|
Title string `json:"title"`
|
|
Author string `json:"author"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
}
|
|
|
|
bookList := make([]BookInfo, 0, len(books))
|
|
for _, book := range books {
|
|
bookList = append(bookList, BookInfo{
|
|
MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(),
|
|
Title: book.Title,
|
|
Author: textToString(book.Author),
|
|
CoverImagePath: textToString(book.CoverImagePath),
|
|
})
|
|
}
|
|
|
|
var viewSettings map[string]interface{}
|
|
if len(collection.ViewSettings) > 0 {
|
|
json.Unmarshal(collection.ViewSettings, &viewSettings)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
|
"name": collection.Name,
|
|
"description": textToString(collection.Description),
|
|
"color": textToString(collection.Color),
|
|
"icon": textToString(collection.Icon),
|
|
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
|
"view_settings": viewSettings,
|
|
"books": bookList,
|
|
"book_count": len(bookList),
|
|
"created_at": collection.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) UpdateCollection(c echo.Context) error {
|
|
collectionID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
var req UpdateCollectionRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
collection, err := h.collectionService.UpdateCollection(
|
|
c.Request().Context(),
|
|
collectionID,
|
|
req.Name,
|
|
req.Description,
|
|
req.Color,
|
|
req.Icon,
|
|
req.AutoAssignRules,
|
|
req.ViewSettings,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
|
"name": collection.Name,
|
|
"description": textToString(collection.Description),
|
|
"color": textToString(collection.Color),
|
|
"icon": textToString(collection.Icon),
|
|
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
|
"view_settings": json.RawMessage(collection.ViewSettings),
|
|
"created_at": collection.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) DeleteCollection(c echo.Context) error {
|
|
collectionID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
err = h.collectionService.DeleteCollection(c.Request().Context(), collectionID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *CollectionHandler) AddBooks(c echo.Context) error {
|
|
collectionID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req AddBooksRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
for _, bookIDStr := range req.BookIDs {
|
|
bookID, err := uuid.Parse(bookIDStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID)
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *CollectionHandler) RemoveBook(c echo.Context) error {
|
|
collectionID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
bookID, err := uuid.Parse(c.Param("bookId"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book id"})
|
|
}
|
|
|
|
err = h.collectionService.RemoveBookFromCollection(c.Request().Context(), collectionID, bookID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *CollectionHandler) GetDeviceMappings(c echo.Context) error {
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device id"})
|
|
}
|
|
|
|
mappings, err := h.collectionService.GetDeviceShelfMappings(c.Request().Context(), deviceID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
type MappingResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CollectionID uuid.UUID `json:"collection_id"`
|
|
CollectionName string `json:"collection_name"`
|
|
DeviceShelfName string `json:"device_shelf_name"`
|
|
SyncDirection string `json:"sync_direction"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
response := make([]MappingResponse, 0, len(mappings))
|
|
for _, m := range mappings {
|
|
response = append(response, MappingResponse{
|
|
ID: uuid.UUID(m.ID.Bytes),
|
|
CollectionID: uuid.UUID(m.CollectionID.Bytes),
|
|
CollectionName: m.CollectionName,
|
|
DeviceShelfName: textToString(m.DeviceShelfName),
|
|
SyncDirection: textToString(m.SyncDirection),
|
|
CreatedAt: m.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"device_id": deviceID.String(),
|
|
"mappings": response,
|
|
"total": len(response),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) CreateDeviceMapping(c echo.Context) error {
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device id"})
|
|
}
|
|
|
|
var req CreateDeviceMappingRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
collectionID, err := uuid.Parse(req.CollectionID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
|
|
}
|
|
|
|
mapping, err := h.collectionService.CreateDeviceShelfMapping(
|
|
c.Request().Context(),
|
|
collectionID,
|
|
deviceID,
|
|
req.DeviceShelfName,
|
|
req.SyncDirection,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, map[string]interface{}{
|
|
"id": uuid.UUID(mapping.ID.Bytes).String(),
|
|
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
|
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
|
"device_shelf_name": textToString(mapping.DeviceShelfName),
|
|
"sync_direction": textToString(mapping.SyncDirection),
|
|
"created_at": mapping.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) UpdateDeviceMapping(c echo.Context) error {
|
|
mappingID, err := uuid.Parse(c.Param("mappingId"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid mapping id"})
|
|
}
|
|
|
|
var req UpdateDeviceMappingRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
mapping, err := h.collectionService.UpdateDeviceShelfMapping(
|
|
c.Request().Context(),
|
|
mappingID,
|
|
req.DeviceShelfName,
|
|
req.SyncDirection,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"id": uuid.UUID(mapping.ID.Bytes).String(),
|
|
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
|
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
|
"device_shelf_name": textToString(mapping.DeviceShelfName),
|
|
"sync_direction": textToString(mapping.SyncDirection),
|
|
"created_at": mapping.CreatedAt.Time.String(),
|
|
})
|
|
}
|
|
|
|
func (h *CollectionHandler) DeleteDeviceMapping(c echo.Context) error {
|
|
mappingID, err := uuid.Parse(c.Param("mappingId"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid mapping id"})
|
|
}
|
|
|
|
err = h.collectionService.DeleteDeviceShelfMapping(c.Request().Context(), mappingID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *CollectionHandler) GetBookCollections(c echo.Context) error {
|
|
bookID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book id"})
|
|
}
|
|
|
|
collections, err := h.collectionService.GetBookCollections(c.Request().Context(), bookID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
type CollectionResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Icon string `json:"icon"`
|
|
}
|
|
|
|
response := make([]CollectionResponse, 0, len(collections))
|
|
for _, col := range collections {
|
|
response = append(response, CollectionResponse{
|
|
ID: uuid.UUID(col.ID.Bytes),
|
|
Name: col.Name,
|
|
Description: textToString(col.Description),
|
|
Color: textToString(col.Color),
|
|
Icon: textToString(col.Icon),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"book_id": bookID.String(),
|
|
"collections": response,
|
|
"total": len(response),
|
|
})
|
|
}
|
|
|
|
func textToString(t pgtype.Text) string {
|
|
if t.Valid {
|
|
return t.String
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (h *CollectionHandler) GetCollectionsData(c echo.Context) ([]database.Collections, error) {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
return h.collectionService.GetUserCollections(c.Request().Context(), userUUID)
|
|
}
|
|
|
|
func (h *CollectionHandler) GetCollectionData(c echo.Context, collectionID uuid.UUID) (database.Collections, error) {
|
|
return h.collectionService.GetCollection(c.Request().Context(), collectionID)
|
|
}
|
|
|
|
func (h *CollectionHandler) GetCollectionBooksData(c echo.Context, collectionID uuid.UUID) ([]database.GetCollectionItemsRow, error) {
|
|
return h.collectionService.GetCollectionBooks(c.Request().Context(), collectionID)
|
|
}
|