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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user