refactor(handlers): Phase 2 - create MediaHandler with CRUD operations
- Add worker field to MediaHandler struct - Make NewMediaHandler accept optional worker parameter - Move 24 media CRUD methods from ebook.go to MediaHandler: * Media CRUD: ListMediaItems, GetMediaItem, ListMediaItemsFiltered, CreateMediaItem, UpdateMediaItem, DeleteMediaItem, SearchMediaItems * Ratings: CreateMediaRating, GetMediaRating, UpdateMediaRating, DeleteMediaRating * Progress: GetMediaReadingProgress, UpdateMediaReadingProgress, DeleteMediaReadingProgress * Notes: GetMediaNotes, CreateMediaNote, GetMediaNote, UpdateMediaNote, DeleteMediaNote * Highlights: GetMediaHighlights, CreateMediaHighlight, GetMediaHighlight, UpdateMediaHighlight, DeleteMediaHighlight Methods are copied (not moved) to maintain backward compatibility during refactoring. Duplicates will be removed in Phase 7. This is Phase 2 of the ebook.go refactoring plan.
This commit is contained in:
+811
-3
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/services"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,16 +11,24 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaHandler struct {
|
type MediaHandler struct {
|
||||||
db *database.Queries
|
db *database.Queries
|
||||||
|
worker *services.Worker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMediaHandler(db *database.Queries) *MediaHandler {
|
func NewMediaHandler(db *database.Queries, worker ...*services.Worker) *MediaHandler {
|
||||||
return &MediaHandler{db: db}
|
mh := &MediaHandler{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
if len(worker) > 0 && worker[0] != nil {
|
||||||
|
mh.worker = worker[0]
|
||||||
|
}
|
||||||
|
return mh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
||||||
@@ -454,3 +463,802 @@ func (h *MediaHandler) HandleBulkUpdate(c echo.Context) error {
|
|||||||
"failed": failedCount,
|
"failed": failedCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListMediaItems handles GET /api/media-items
|
||||||
|
func (mh *MediaHandler) ListMediaItems(c echo.Context) error {
|
||||||
|
libraryID := c.QueryParam("library_id")
|
||||||
|
sort := c.QueryParam("sort")
|
||||||
|
limit, _ := strconv.Atoi(c.QueryParam("limit"))
|
||||||
|
offset, _ := strconv.Atoi(c.QueryParam("offset"))
|
||||||
|
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "" {
|
||||||
|
sort = "created_at DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedSorts := map[string]bool{
|
||||||
|
"created_at ASC": true,
|
||||||
|
"created_at DESC": true,
|
||||||
|
"title ASC": true,
|
||||||
|
"title DESC": true,
|
||||||
|
"author ASC": true,
|
||||||
|
"author DESC": true,
|
||||||
|
"series ASC": true,
|
||||||
|
"series DESC": true,
|
||||||
|
"date_published ASC": true,
|
||||||
|
"date_published DESC": true,
|
||||||
|
"copyright_year ASC": true,
|
||||||
|
"copyright_year DESC": true,
|
||||||
|
"page_count ASC": true,
|
||||||
|
"page_count DESC": true,
|
||||||
|
"genre ASC": true,
|
||||||
|
"genre DESC": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowedSorts[sort] {
|
||||||
|
sort = "created_at DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > maxPaginationLimit {
|
||||||
|
limit = maxPaginationLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset < 0 {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "offset cannot be negative"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if libraryID != "" {
|
||||||
|
libUUID, err := uuid.Parse(libraryID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := mh.db.ListMediaItemsSorted(c.Request().Context(), database.ListMediaItemsSortedParams{
|
||||||
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||||
|
Sort: pgtype.Text{String: sort, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{"data": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := mh.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
|
||||||
|
Limit: int32(limit),
|
||||||
|
Offset: int32(offset),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{"data": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaItem handles GET /api/media-items/:id
|
||||||
|
func (mh *MediaHandler) GetMediaItem(c echo.Context) error {
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMediaItemsFiltered handles GET /api/media-items/filtered
|
||||||
|
func (mh *MediaHandler) ListMediaItemsFiltered(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
libraryID := c.QueryParam("library_id")
|
||||||
|
sort := c.QueryParam("sort")
|
||||||
|
limit, _ := strconv.Atoi(c.QueryParam("limit"))
|
||||||
|
offset, _ := strconv.Atoi(c.QueryParam("offset"))
|
||||||
|
|
||||||
|
authorFilter := c.QueryParam("author_filter")
|
||||||
|
seriesFilter := c.QueryParam("series_filter")
|
||||||
|
genreFilter := c.QueryParam("genre_filter")
|
||||||
|
languageFilter := c.QueryParam("language_filter")
|
||||||
|
yearMin, _ := strconv.Atoi(c.QueryParam("year_min"))
|
||||||
|
yearMax, _ := strconv.Atoi(c.QueryParam("year_max"))
|
||||||
|
hasCover, _ := strconv.ParseBool(c.QueryParam("has_cover"))
|
||||||
|
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "" {
|
||||||
|
sort = "created_at DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > maxPaginationLimit {
|
||||||
|
limit = maxPaginationLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
libUUID, err := uuid.Parse(libraryID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := mh.db.ListMediaItemsFiltered(c.Request().Context(), database.ListMediaItemsFilteredParams{
|
||||||
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
AuthorFilter: pgtype.Text{String: authorFilter, Valid: true},
|
||||||
|
SeriesFilter: pgtype.Text{String: seriesFilter, Valid: true},
|
||||||
|
GenreFilter: pgtype.Text{String: genreFilter, Valid: true},
|
||||||
|
LanguageFilter: pgtype.Text{String: languageFilter, Valid: true},
|
||||||
|
YearMin: pgtype.Int4{Int32: int32(yearMin), Valid: true},
|
||||||
|
YearMax: pgtype.Int4{Int32: int32(yearMax), Valid: true},
|
||||||
|
HasCover: pgtype.Bool{Bool: hasCover, Valid: true},
|
||||||
|
Sort: pgtype.Text{String: sort, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{"data": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaRating handles POST /api/media-items/:id/rating
|
||||||
|
func (mh *MediaHandler) CreateMediaRating(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Rating int32 `json:"rating" validate:"required,min=1,max=10"`
|
||||||
|
}
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, err := mh.db.CreateMediaRating(c.Request().Context(), database.CreateMediaRatingParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Rating: req.Rating,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusCreated, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaRating handles GET /api/media-items/:id/rating
|
||||||
|
func (mh *MediaHandler) GetMediaRating(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, err := mh.db.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaRating handles PUT /api/media-items/:id/rating
|
||||||
|
func (mh *MediaHandler) UpdateMediaRating(c echo.Context) error {
|
||||||
|
return mh.CreateMediaRating(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMediaRating handles DELETE /api/media-items/:id/rating
|
||||||
|
func (mh *MediaHandler) DeleteMediaRating(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteMediaRating(c.Request().Context(), database.DeleteMediaRatingParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "rating deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaReadingProgress handles GET /api/media-items/:id/progress
|
||||||
|
func (mh *MediaHandler) GetMediaReadingProgress(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
progress, err := mh.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"current_page": 0,
|
||||||
|
"total_pages": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaReadingProgress handles PUT /api/media-items/:id/progress
|
||||||
|
func (mh *MediaHandler) UpdateMediaReadingProgress(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
CurrentPage int32 `json:"current_page"`
|
||||||
|
TotalPages int32 `json:"total_pages"`
|
||||||
|
}
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
progress, err := mh.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
|
||||||
|
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMediaReadingProgress handles DELETE /api/media-items/:id/progress
|
||||||
|
func (mh *MediaHandler) DeleteMediaReadingProgress(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteReadingProgress(c.Request().Context(), database.DeleteReadingProgressParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "reading progress deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaItem handles POST /api/media-items (admin only)
|
||||||
|
func (mh *MediaHandler) CreateMediaItem(c echo.Context) error {
|
||||||
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|
||||||
|
if user.Role != "admin" {
|
||||||
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateMediaItemRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaItemID, err := mh.db.CreateMediaItem(c.Request().Context(), database.CreateMediaItemParams{
|
||||||
|
LibraryID: pgtype.UUID{Bytes: req.LibraryID, Valid: true},
|
||||||
|
Title: req.Title,
|
||||||
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
||||||
|
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
||||||
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
||||||
|
FilePath: req.FilePath,
|
||||||
|
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
||||||
|
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
|
||||||
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
||||||
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
||||||
|
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||||
|
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
|
||||||
|
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||||
|
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||||
|
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||||
|
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
||||||
|
AddedByAdminID: user.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaItemID.ID.Bytes, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusCreated, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaItem handles PUT /api/media-items/:id (admin only)
|
||||||
|
func (mh *MediaHandler) UpdateMediaItem(c echo.Context) error {
|
||||||
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|
||||||
|
if user.Role != "admin" {
|
||||||
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateMediaItemRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{
|
||||||
|
ID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
Title: req.Title,
|
||||||
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
||||||
|
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
||||||
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
||||||
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
||||||
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
||||||
|
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||||
|
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
|
||||||
|
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||||
|
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||||
|
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||||
|
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMediaItem handles DELETE /api/media-items/:id (admin only)
|
||||||
|
func (mh *MediaHandler) DeleteMediaItem(c echo.Context) error {
|
||||||
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|
||||||
|
if user.Role != "admin" {
|
||||||
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaNotes handles GET /api/media-items/:id/notes
|
||||||
|
func (mh *MediaHandler) GetMediaNotes(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
notes, err := mh.db.GetMediaNotes(c.Request().Context(), database.GetMediaNotesParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaNote handles POST /api/media-items/:id/notes
|
||||||
|
func (mh *MediaHandler) CreateMediaNote(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateMediaNoteRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
note, err := mh.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Content: req.Content,
|
||||||
|
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusCreated, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
|
||||||
|
func (mh *MediaHandler) GetMediaNote(c echo.Context) error {
|
||||||
|
noteID := c.Param("noteId")
|
||||||
|
noteUUID, err := uuid.Parse(noteID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
note, err := mh.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaNote handles PUT /api/media-items/:id/notes/:noteId
|
||||||
|
func (mh *MediaHandler) UpdateMediaNote(c echo.Context) error {
|
||||||
|
noteID := c.Param("noteId")
|
||||||
|
noteUUID, err := uuid.Parse(noteID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateMediaNoteRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
note, err := mh.db.UpdateMediaNote(c.Request().Context(), database.UpdateMediaNoteParams{
|
||||||
|
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
|
||||||
|
Content: req.Content,
|
||||||
|
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMediaNote handles DELETE /api/media-items/:id/notes/:noteId
|
||||||
|
func (mh *MediaHandler) DeleteMediaNote(c echo.Context) error {
|
||||||
|
noteID := c.Param("noteId")
|
||||||
|
noteUUID, err := uuid.Parse(noteID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaHighlights handles GET /api/media-items/:id/highlights
|
||||||
|
func (mh *MediaHandler) GetMediaHighlights(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
highlights, err := mh.db.GetMediaHighlights(c.Request().Context(), database.GetMediaHighlightsParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, highlights)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaHighlight handles POST /api/media-items/:id/highlights
|
||||||
|
func (mh *MediaHandler) CreateMediaHighlight(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateMediaHighlightRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteUUID pgtype.UUID
|
||||||
|
if req.NoteID != "" {
|
||||||
|
if noteID, err := uuid.Parse(req.NoteID); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
|
} else {
|
||||||
|
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color := "#ffff00"
|
||||||
|
if req.Color != "" {
|
||||||
|
color = req.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight, err := mh.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
SelectionText: req.SelectionText,
|
||||||
|
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
||||||
|
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
||||||
|
Color: pgtype.Text{String: color, Valid: true},
|
||||||
|
NoteID: noteUUID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusCreated, highlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
|
||||||
|
func (mh *MediaHandler) GetMediaHighlight(c echo.Context) error {
|
||||||
|
highlightID := c.Param("highlightId")
|
||||||
|
highlightUUID, err := uuid.Parse(highlightID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight, err := mh.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, highlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaHighlight handles PUT /api/media-items/:id/highlights/:highlightId
|
||||||
|
func (mh *MediaHandler) UpdateMediaHighlight(c echo.Context) error {
|
||||||
|
highlightID := c.Param("highlightId")
|
||||||
|
highlightUUID, err := uuid.Parse(highlightID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateMediaHighlightRequest
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteUUID pgtype.UUID
|
||||||
|
if req.NoteID != "" {
|
||||||
|
if noteID, err := uuid.Parse(req.NoteID); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
|
} else {
|
||||||
|
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color := "#ffff00"
|
||||||
|
if req.Color != "" {
|
||||||
|
color = req.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight, err := mh.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
|
||||||
|
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
|
||||||
|
SelectionText: req.SelectionText,
|
||||||
|
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
||||||
|
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
||||||
|
Color: pgtype.Text{String: color, Valid: true},
|
||||||
|
NoteID: noteUUID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, highlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
|
||||||
|
func (mh *MediaHandler) DeleteMediaHighlight(c echo.Context) error {
|
||||||
|
highlightID := c.Param("highlightId")
|
||||||
|
highlightUUID, err := uuid.Parse(highlightID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchMediaItems handles GET /api/media-items/search
|
||||||
|
func (mh *MediaHandler) SearchMediaItems(c echo.Context) error {
|
||||||
|
query := c.QueryParam("q")
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "query parameter 'q' is required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := int32(50)
|
||||||
|
offset := int32(0)
|
||||||
|
|
||||||
|
searchPattern := "%" + query + "%"
|
||||||
|
|
||||||
|
partialResults, err := mh.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
|
||||||
|
SearchPattern: pgtype.Text{String: searchPattern, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(partialResults) > 0 {
|
||||||
|
return c.JSON(http.StatusOK, partialResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzzyResults, err := mh.db.SearchMediaItemsFuzzy(c.Request().Context(), database.SearchMediaItemsFuzzyParams{
|
||||||
|
SearchQuery: pgtype.Text{String: query, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fuzzyResults) == 0 {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]interface{}{
|
||||||
|
"error": "no results found",
|
||||||
|
"query": query,
|
||||||
|
"results": []interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, fuzzyResults)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user