refactor: update handlers to use unified search endpoint

- Update SearchMediaItems handler to use SearchService
- Add autocomplete detection for field value queries (author=value, genre=value, etc.)
- Add handleFieldValuesSearch method for dropdown population
- Add sort parameter extraction with default "title ASC"
- Remove deprecated ListMediaItemsFiltered handler
- Remove deprecated /api/media-items/filtered route registration
- Update frontend.go to use SearchMediaItemsUnified instead of ListMediaItemsFiltered
- Fix parameter passing (empty filters use Valid:true with empty values, not Valid:false)
- Add SearchQuery, IsExactSearch, SearchPattern parameters for query parsing

Handler is now a thin wrapper that extracts params and delegates to service layer.
This commit is contained in:
2026-03-23 22:37:44 -04:00
parent 871d5eafe6
commit 172536f888
3 changed files with 20 additions and 74 deletions
+7 -63
View File
@@ -704,69 +704,6 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
})
}
// 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 offset < 0 {
offset = 0
}
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)
@@ -1471,6 +1408,12 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
yearMax, _ := strconv.Atoi(c.QueryParam("year_max"))
hasCover := c.QueryParam("has_cover") == "true"
// Extract sort parameter
sortParam := c.QueryParam("sort")
if sortParam == "" {
sortParam = "title ASC" // Default sort
}
// Build search params
params := services.SearchParams{
UserID: userID.ID,
@@ -1483,6 +1426,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
YearMin: yearMin,
YearMax: yearMax,
HasCover: hasCover,
Sort: sortParam,
Limit: limit,
Offset: offset,
}
+13 -10
View File
@@ -175,7 +175,7 @@ func registerFrontendRoutes(cfg *Config) {
}
// Fetch first page of books for SSR
var books []database.ListMediaItemsFilteredRow
var books []database.SearchMediaItemsUnifiedRow
var bookInfoList []handlers.BookInfo
totalCount := 0
limit := 50
@@ -196,23 +196,26 @@ func registerFrontendRoutes(cfg *Config) {
}
}
books, err = cfg.Queries.ListMediaItemsFiltered(c.Request().Context(), database.ListMediaItemsFilteredParams{
books, err = cfg.Queries.SearchMediaItemsUnified(c.Request().Context(), database.SearchMediaItemsUnifiedParams{
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
AuthorFilter: pgtype.Text{String: "", Valid: false},
SeriesFilter: pgtype.Text{String: "", Valid: false},
GenreFilter: pgtype.Text{String: "", Valid: false},
LanguageFilter: pgtype.Text{String: "", Valid: false},
YearMin: pgtype.Int4{Valid: false},
YearMax: pgtype.Int4{Valid: false},
HasCover: pgtype.Bool{Valid: false},
AuthorFilter: pgtype.Text{String: "", Valid: true}, // Changed: Valid=true for empty string
SeriesFilter: pgtype.Text{String: "", Valid: true}, // Changed
GenreFilter: pgtype.Text{String: "", Valid: true}, // Changed
LanguageFilter: pgtype.Text{String: "", Valid: true}, // Changed
YearMin: pgtype.Int4{Int32: 0, Valid: true}, // Changed: use 0 as default
YearMax: pgtype.Int4{Int32: 0, Valid: true}, // Changed
HasCover: pgtype.Bool{Bool: false, Valid: true}, // Changed
SearchQuery: pgtype.Text{String: "", Valid: true}, // NEW: Required
IsExactSearch: pgtype.Bool{Bool: false, Valid: true}, // NEW: Required
SearchPattern: pgtype.Text{Valid: false}, // NEW: Empty for no exact search
Sort: pgtype.Text{String: "created_at DESC", Valid: true},
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
})
if err != nil {
log.Printf("ListMediaItemsFiltered failed: %v", err)
log.Printf("SearchMediaItemsUnified failed: %v", err)
// Continue without books - will show empty state
} else {
// Convert database rows to BookInfo structs (matching BuildSections pattern)
-1
View File
@@ -14,7 +14,6 @@ func registerMediaRoutes(cfg *Config) {
// Media item routes (all authenticated users)
protected.GET("/media-items", cfg.MediaHandler.ListMediaItems)
protected.GET("/media-items/filtered", cfg.MediaHandler.ListMediaItemsFiltered)
protected.GET("/media-items/:id", cfg.MediaHandler.GetMediaItem)
// Media rating routes (all authenticated users)