Phase 1: Add enhanced database fields and sorting

- Add 9 new fields to media_items table (language, edition, page_count, goodreads_id, openlibrary_id, google_books_id, copyright_year, genre, subjects)
- Add indexes for new fields (language, genre, page_count, copyright_year, series_order, date_published)
- Add ListMediaItemsSorted SQL query for dynamic sorting
- Update ListMediaItems handler to process sort parameter
- Support 16 sorting options (title, author, created_at, date_published, copyright_year, page_count, genre, series)
- Add /api/media-items/filtered endpoint for advanced filtering
- Register new filtered endpoint in routes
This commit is contained in:
2026-01-30 08:32:49 -05:00
parent 3fe44205c5
commit 3b2075fc70
5 changed files with 727 additions and 14 deletions
+95 -1
View File
@@ -96,6 +96,7 @@ func SetupRoutes(g *echo.Group, db *database.Queries) *Handler {
// Media item routes (new library system)
g.GET("/media-items", h.ListMediaItems)
g.GET("/media-items/filtered", h.ListMediaItemsFiltered)
g.GET("/media-items/search", h.SearchMediaItems)
g.GET("/media-items/:id", h.GetMediaItem)
g.POST("/media-items/:id/rating", h.CreateMediaRating)
@@ -824,6 +825,7 @@ func (h *Handler) StopScheduler() {
// 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"))
@@ -831,6 +833,33 @@ func (h *Handler) ListMediaItems(c echo.Context) error {
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
@@ -846,7 +875,12 @@ func (h *Handler) ListMediaItems(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
}
items, err := h.db.ListMediaItemsByLibrary(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
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()})
}
@@ -884,6 +918,66 @@ func (h *Handler) GetMediaItem(c echo.Context) 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)