refactor(handlers): Phase 7 - cleanup ebook.go, remove duplicate methods
- Remove 24 duplicate media CRUD methods from ebook.go (884 lines removed) - Keep 12 scanner/watch/scheduler methods on Handler - Move request type declarations to media.go: * CreateMediaItemRequest * UpdateMediaItemRequest * CreateMediaNoteRequest * UpdateMediaNoteRequest * CreateMediaHighlightRequest * UpdateMediaHighlightRequest - Remove unused imports from ebook.go (strconv, pgx) - Fix library.go to use MediaHandler.ListMediaItems instead of Handler ebook.go reduced from 1266 lines to 382 lines (70% reduction) Handler now has focused responsibility: scanner and scheduler operations only This completes Phase 7 of the ebook.go refactoring plan. Result: Clean separation of concerns with no duplicate code
This commit is contained in:
@@ -7,12 +7,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -382,884 +380,3 @@ func (h *Handler) StartScheduler() {
|
|||||||
func (h *Handler) StopScheduler() {
|
func (h *Handler) StopScheduler() {
|
||||||
h.scheduler.Stop()
|
h.scheduler.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media Item handlers for new library system
|
|
||||||
|
|
||||||
// ListMediaItems handles GET /api/media-items
|
|
||||||
func (h *Handler) 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce maximum limit
|
|
||||||
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 := h.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 := h.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 (h *Handler) 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 := h.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 (h *Handler) 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce maximum limit
|
|
||||||
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 := h.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 (h *Handler) 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 := h.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 (h *Handler) 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 := h.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 (h *Handler) UpdateMediaRating(c echo.Context) error {
|
|
||||||
return h.CreateMediaRating(c) // Same logic as create due to upsert
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMediaRating handles DELETE /api/media-items/:id/rating
|
|
||||||
func (h *Handler) 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 = h.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 (h *Handler) 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 := h.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 (h *Handler) 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 := h.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 (h *Handler) 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 = h.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"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Media Items handlers
|
|
||||||
|
|
||||||
// CreateMediaItemRequest represents the request for creating a media item
|
|
||||||
type CreateMediaItemRequest struct {
|
|
||||||
LibraryID uuid.UUID `json:"library_id" validate:"required"`
|
|
||||||
Title string `json:"title" validate:"required,min=1,max=500"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
ISBN string `json:"isbn"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
FilePath string `json:"file_path" validate:"required"`
|
|
||||||
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
|
||||||
MimeType string `json:"mime_type" validate:"required"`
|
|
||||||
CoverImagePath string `json:"cover_image_path"`
|
|
||||||
Series string `json:"series"`
|
|
||||||
SeriesNumber int32 `json:"series_number"`
|
|
||||||
Tags string `json:"tags"`
|
|
||||||
ASIN string `json:"asin"`
|
|
||||||
DatePublished string `json:"date_published"`
|
|
||||||
Publisher string `json:"publisher"`
|
|
||||||
Contributors string `json:"contributors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMediaItem handles POST /api/media-items (admin only)
|
|
||||||
func (h *Handler) CreateMediaItem(c echo.Context) error {
|
|
||||||
user := MustGetAuthenticatedUser(c)
|
|
||||||
|
|
||||||
// Verify user is admin
|
|
||||||
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()})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate library exists
|
|
||||||
_, err := h.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 := h.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()})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the created item info
|
|
||||||
item, err := h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaItemRequest represents the request for updating a media item
|
|
||||||
type UpdateMediaItemRequest struct {
|
|
||||||
Title string `json:"title" validate:"required,min=1,max=500"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
ISBN string `json:"isbn"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CoverImagePath string `json:"cover_image_path"`
|
|
||||||
Series string `json:"series"`
|
|
||||||
SeriesNumber int32 `json:"series_number"`
|
|
||||||
Tags string `json:"tags"`
|
|
||||||
ASIN string `json:"asin"`
|
|
||||||
DatePublished string `json:"date_published"`
|
|
||||||
Publisher string `json:"publisher"`
|
|
||||||
Contributors string `json:"contributors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaItem handles PUT /api/media-items/:id (admin only)
|
|
||||||
func (h *Handler) UpdateMediaItem(c echo.Context) error {
|
|
||||||
user := MustGetAuthenticatedUser(c)
|
|
||||||
|
|
||||||
// Verify user is admin
|
|
||||||
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 := h.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 (h *Handler) DeleteMediaItem(c echo.Context) error {
|
|
||||||
user := MustGetAuthenticatedUser(c)
|
|
||||||
|
|
||||||
// Verify user is admin
|
|
||||||
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 = h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Media Notes handlers
|
|
||||||
|
|
||||||
// GetMediaNotes handles GET /api/media-items/:id/notes
|
|
||||||
func (h *Handler) 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 := h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMediaNoteRequest represents the request for creating a media note
|
|
||||||
type CreateMediaNoteRequest struct {
|
|
||||||
Content string `json:"content" validate:"required,min=1,max=10000"`
|
|
||||||
Position string `json:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMediaNote handles POST /api/media-items/:id/notes
|
|
||||||
func (h *Handler) 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 := h.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 (h *Handler) 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 := h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaNoteRequest represents the request for updating a media note
|
|
||||||
type UpdateMediaNoteRequest struct {
|
|
||||||
Content string `json:"content" validate:"required,min=1,max=10000"`
|
|
||||||
Position string `json:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaNote handles PUT /api/media-items/:id/notes/:noteId
|
|
||||||
func (h *Handler) 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 := h.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 (h *Handler) 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 = h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Media Highlights handlers
|
|
||||||
|
|
||||||
// GetMediaHighlights handles GET /api/media-items/:id/highlights
|
|
||||||
func (h *Handler) 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 := h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMediaHighlightRequest represents the request for creating a media highlight
|
|
||||||
type CreateMediaHighlightRequest struct {
|
|
||||||
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
|
|
||||||
StartPosition string `json:"start_position" validate:"required,max=100"`
|
|
||||||
EndPosition string `json:"end_position" validate:"required,max=100"`
|
|
||||||
Color string `json:"color" validate:"omitempty,len=7"` // hex color
|
|
||||||
NoteID string `json:"note_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMediaHighlight handles POST /api/media-items/:id/highlights
|
|
||||||
func (h *Handler) 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" // default yellow
|
|
||||||
if req.Color != "" {
|
|
||||||
color = req.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight, err := h.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 (h *Handler) 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 := h.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaHighlightRequest represents the request for updating a media highlight
|
|
||||||
type UpdateMediaHighlightRequest struct {
|
|
||||||
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
|
|
||||||
StartPosition string `json:"start_position" validate:"required,max=100"`
|
|
||||||
EndPosition string `json:"end_position" validate:"required,max=100"`
|
|
||||||
Color string `json:"color" validate:"omitempty,len=7"` // hex color
|
|
||||||
NoteID string `json:"note_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMediaHighlight handles PUT /api/media-items/:id/highlights/:highlightId
|
|
||||||
func (h *Handler) 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" // default yellow
|
|
||||||
if req.Color != "" {
|
|
||||||
color = req.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight, err := h.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 (h *Handler) 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 = h.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
|
|
||||||
// Performs partial matching search with fuzzy fallback if no results found
|
|
||||||
func (h *Handler) 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 := h.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 := h.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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,6 +16,72 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateMediaItemRequest represents the request for creating a media item
|
||||||
|
type CreateMediaItemRequest struct {
|
||||||
|
LibraryID uuid.UUID `json:"library_id" validate:"required"`
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=500"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
ISBN string `json:"isbn"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
FilePath string `json:"file_path" validate:"required"`
|
||||||
|
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
||||||
|
MimeType string `json:"mime_type" validate:"required"`
|
||||||
|
CoverImagePath string `json:"cover_image_path"`
|
||||||
|
Series string `json:"series"`
|
||||||
|
SeriesNumber int32 `json:"series_number"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
ASIN string `json:"asin"`
|
||||||
|
DatePublished string `json:"date_published"`
|
||||||
|
Publisher string `json:"publisher"`
|
||||||
|
Contributors string `json:"contributors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaItemRequest represents the request for updating a media item
|
||||||
|
type UpdateMediaItemRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=500"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
ISBN string `json:"isbn"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CoverImagePath string `json:"cover_image_path"`
|
||||||
|
Series string `json:"series"`
|
||||||
|
SeriesNumber int32 `json:"series_number"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
ASIN string `json:"asin"`
|
||||||
|
DatePublished string `json:"date_published"`
|
||||||
|
Publisher string `json:"publisher"`
|
||||||
|
Contributors string `json:"contributors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaNoteRequest represents the request for creating a media note
|
||||||
|
type CreateMediaNoteRequest struct {
|
||||||
|
Content string `json:"content" validate:"required,min=1,max=10000"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaNoteRequest represents the request for updating a media note
|
||||||
|
type UpdateMediaNoteRequest struct {
|
||||||
|
Content string `json:"content" validate:"required,min=1,max=10000"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMediaHighlightRequest represents the request for creating a media highlight
|
||||||
|
type CreateMediaHighlightRequest struct {
|
||||||
|
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
|
||||||
|
StartPosition string `json:"start_position" validate:"required,max=100"`
|
||||||
|
EndPosition string `json:"end_position" validate:"required,max=100"`
|
||||||
|
Color string `json:"color" validate:"omitempty,len=7"`
|
||||||
|
NoteID string `json:"note_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaHighlightRequest represents the request for updating a media highlight
|
||||||
|
type UpdateMediaHighlightRequest struct {
|
||||||
|
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
|
||||||
|
StartPosition string `json:"start_position" validate:"required,max=100"`
|
||||||
|
EndPosition string `json:"end_position" validate:"required,max=100"`
|
||||||
|
Color string `json:"color" validate:"omitempty,len=7"`
|
||||||
|
NoteID string `json:"note_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type MediaHandler struct {
|
type MediaHandler struct {
|
||||||
db *database.Queries
|
db *database.Queries
|
||||||
worker *services.Worker
|
worker *services.Worker
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func registerLibraryRoutes(cfg *Config) {
|
|||||||
adminLibrary.GET("/:id/media-items", func(c echo.Context) error {
|
adminLibrary.GET("/:id/media-items", func(c echo.Context) error {
|
||||||
libraryID := c.Param("id")
|
libraryID := c.Param("id")
|
||||||
c.QueryParams().Set("library_id", libraryID)
|
c.QueryParams().Set("library_id", libraryID)
|
||||||
return h.ListMediaItems(c)
|
return cfg.MediaHandler.ListMediaItems(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// User library visibility control
|
// User library visibility control
|
||||||
|
|||||||
Reference in New Issue
Block a user