feat(api): add library_id filtering to collections endpoints

Add optional library_id query parameter support to GetCollections and
GetCollection API handlers for library-scoped book filtering.

GetCollections (GET /api/collections?library_id=X):
- When library_id is provided, include per-library book_count in the
  response by querying GetCollectionItemsForDashboard for each collection
- When omitted, returns all collections as before (backward compatible)
- Added BookCount field to CollectionResponse struct

GetCollection (GET /api/collections/:id?library_id=X):
- System collections (non-empty QueryType): uses DashboardService to
  fetch library-scoped sections, matching the existing SSR handler logic
- User collections: uses GetCollectionItemsForDashboard for
  library-filtered results, excluding soft-deleted items
- When library_id is omitted, returns all books as before
This commit is contained in:
2026-05-17 21:12:01 -04:00
parent a2fc2613b4
commit faef4d9fff
+99 -12
View File
@@ -138,6 +138,7 @@ func (h *CollectionHandler) CreateCollection(c *echo.Context) error {
func (h *CollectionHandler) GetCollections(c *echo.Context) error { func (h *CollectionHandler) GetCollections(c *echo.Context) error {
includeAuto := c.QueryParam("include_auto") == "true" includeAuto := c.QueryParam("include_auto") == "true"
sortBy := c.QueryParam("sort_by") sortBy := c.QueryParam("sort_by")
libraryID := c.QueryParam("library_id")
collections, err := h.GetCollectionsData(c) collections, err := h.GetCollectionsData(c)
if err != nil { if err != nil {
@@ -152,6 +153,7 @@ func (h *CollectionHandler) GetCollections(c *echo.Context) error {
Icon string `json:"icon"` Icon string `json:"icon"`
AutoAssignRules json.RawMessage `json:"auto_assign_rules"` AutoAssignRules json.RawMessage `json:"auto_assign_rules"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
BookCount int `json:"book_count"`
} }
response := make([]CollectionResponse, 0, len(collections)) response := make([]CollectionResponse, 0, len(collections))
@@ -159,6 +161,26 @@ func (h *CollectionHandler) GetCollections(c *echo.Context) error {
if !includeAuto && len(col.AutoAssignRules) > 0 { if !includeAuto && len(col.AutoAssignRules) > 0 {
continue continue
} }
bookCount := 0
if libraryID != "" {
libUUID, libErr := uuid.Parse(libraryID)
if libErr == nil {
items, countErr := h.db.GetCollectionItemsForDashboard(c.Request().Context(), database.GetCollectionItemsForDashboardParams{
CollectionID: pgtype.UUID{Bytes: col.ID.Bytes, Valid: true},
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Limit: 10000,
})
if countErr == nil {
for _, item := range items {
if !item.Excluded.Valid || !item.Excluded.Bool {
bookCount++
}
}
}
}
}
response = append(response, CollectionResponse{ response = append(response, CollectionResponse{
ID: col.ID.Bytes, ID: col.ID.Bytes,
Name: col.Name, Name: col.Name,
@@ -167,6 +189,7 @@ func (h *CollectionHandler) GetCollections(c *echo.Context) error {
Icon: textToString(col.Icon), Icon: textToString(col.Icon),
AutoAssignRules: col.AutoAssignRules, AutoAssignRules: col.AutoAssignRules,
CreatedAt: col.CreatedAt.Time.String(), CreatedAt: col.CreatedAt.Time.String(),
BookCount: bookCount,
}) })
} }
@@ -197,19 +220,83 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error {
return c.JSON(http.StatusNotFound, map[string]string{"error": "collection not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "collection not found"})
} }
books, err := h.GetCollectionBooksData(c, collectionID) libraryID := c.QueryParam("library_id")
if err != nil { var bookList []BookInfo
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
bookList := make([]BookInfo, 0, len(books)) if libraryID != "" && collection.QueryType.Valid && collection.QueryType.String != "" {
for _, book := range books { libUUID, libErr := uuid.Parse(libraryID)
bookList = append(bookList, BookInfo{ if libErr != nil {
MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(), return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
Title: book.Title, }
Author: textToString(book.Author), user := c.Get("user").(database.Users)
CoverImagePath: utils.ResolveMediaURL(book.LibraryID, book.CoverImagePath), userUUID := uuid.UUID(user.ID.Bytes)
}) dashboardSvc := services.NewDashboardService(h.db)
sections, secErr := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libUUID, 1000, []string{}, []string{})
if secErr != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": secErr.Error()})
}
for _, section := range sections {
if section.CollectionID == collectionID {
bookCards := make([]BookInfo, len(section.Items))
for i, item := range section.Items {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards[i] = BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: textToString(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
bookList = bookCards
break
}
}
} else if libraryID != "" {
libUUID, libErr := uuid.Parse(libraryID)
if libErr != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
}
collItems, collErr := h.db.GetCollectionItemsForDashboard(c.Request().Context(),
database.GetCollectionItemsForDashboardParams{
CollectionID: pgtype.UUID{Bytes: collectionID, Valid: true},
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Limit: 10000,
})
if collErr != nil {
bookList = []BookInfo{}
} else {
var validItems []database.GetCollectionItemsForDashboardRow
for _, item := range collItems {
if !item.Excluded.Valid || !item.Excluded.Bool {
validItems = append(validItems, item)
}
}
bookCards := make([]BookInfo, len(validItems))
for i, item := range validItems {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards[i] = BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: textToString(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
bookList = bookCards
}
} else {
books, booksErr := h.GetCollectionBooksData(c, collectionID)
if booksErr != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": booksErr.Error()})
}
bookList = make([]BookInfo, 0, len(books))
for _, book := range books {
bookList = append(bookList, BookInfo{
MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(),
Title: book.Title,
Author: textToString(book.Author),
CoverImagePath: utils.ResolveMediaURL(book.LibraryID, book.CoverImagePath),
})
}
} }
var viewSettings map[string]interface{} var viewSettings map[string]interface{}