refactor: rename bulk operations from /api/books/ to /api/media-items/

- Rename routes: /api/books/bulk-{delete,update} → /api/media-items/bulk-{delete,update}
- Rename route: /api/books/:uuid/download → /api/media-items/:uuid/download
- Update request fields: book_ids → media_item_ids
- Update response fields: success → deleted/updated
- Update result fields: book_id → media_item_id
- Update collection handler: success → added

This change improves API semantic correctness as the system handles
multiple media types (ebooks, comics, manga), not just books.

BREAKING CHANGE: All bulk operation endpoints and field names renamed
This commit is contained in:
2026-02-10 19:56:59 -05:00
parent f72c552241
commit 03225cf6e2
3 changed files with 78 additions and 78 deletions
+1 -1
View File
@@ -810,7 +810,7 @@ func (h *CollectionHandler) HandleBulkAddBooks(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"results": results, "results": results,
"total": len(results), "total": len(results),
"success": successCount, "added": successCount,
"failed": failedCount, "failed": failedCount,
}) })
} }
+72 -72
View File
@@ -341,156 +341,156 @@ func (h *MediaHandler) ClearShelf(c echo.Context) error {
// Bulk Operations // Bulk Operations
// POST /api/books/bulk-delete // POST /api/media-items/bulk-delete
// Bulk delete books // Bulk delete media items
func (h *MediaHandler) HandleBulkDelete(c echo.Context) error { func (h *MediaHandler) HandleBulkDelete(c echo.Context) error {
var req struct { var req struct {
BookIDs []string `json:"book_ids" validate:"required"` MediaItemIDs []string `json:"media_item_ids" validate:"required"`
} }
if err := c.Bind(&req); err != nil { if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
} }
if len(req.BookIDs) == 0 { if len(req.MediaItemIDs) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "book_ids required"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_item_ids required"})
} }
results := make([]map[string]interface{}, 0) results := make([]map[string]interface{}, 0)
successCount := 0 successCount := 0
failedCount := 0 failedCount := 0
for _, bookIDStr := range req.BookIDs { for _, mediaIDStr := range req.MediaItemIDs {
bookID, err := uuid.Parse(bookIDStr) mediaID, err := uuid.Parse(mediaIDStr)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": bookIDStr, "media_item_id": mediaIDStr,
"status": "error", "status": "error",
"error": "Invalid UUID", "error": "Invalid UUID",
}) })
failedCount++ failedCount++
continue continue
} }
bookUUID := pgtype.UUID{Bytes: bookID, Valid: true} mediaUUID := pgtype.UUID{Bytes: mediaID, Valid: true}
book, err := h.db.GetMediaItem(c.Request().Context(), bookUUID) media, err := h.db.GetMediaItem(c.Request().Context(), mediaUUID)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": bookIDStr, "media_item_id": mediaIDStr,
"status": "error", "status": "error",
"error": "Book not found", "error": "Media item not found",
}) })
failedCount++ failedCount++
continue continue
} }
err = h.db.DeleteMediaItem(c.Request().Context(), bookUUID) err = h.db.DeleteMediaItem(c.Request().Context(), mediaUUID)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": bookIDStr, "media_item_id": mediaIDStr,
"status": "error", "status": "error",
"error": err.Error(), "error": err.Error(),
}) })
failedCount++ failedCount++
continue continue
} }
if book.FilePath != "" { if media.FilePath != "" {
os.Remove(book.FilePath) os.Remove(media.FilePath)
} }
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": bookIDStr, "media_item_id": mediaIDStr,
"status": "success", "status": "success",
}) })
successCount++ successCount++
} }
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"results": results, "results": results,
"total": len(req.BookIDs), "total": len(req.MediaItemIDs),
"success": successCount, "deleted": successCount,
"failed": failedCount, "failed": failedCount,
}) })
} }
// POST /api/books/bulk-update // POST /api/media-items/bulk-update
// Bulk update book metadata // Bulk update media item metadata
func (h *MediaHandler) HandleBulkUpdate(c echo.Context) error { func (h *MediaHandler) HandleBulkUpdate(c echo.Context) error {
var req struct { var req struct {
Updates []struct { MediaItemUpdates []struct {
BookID string `json:"book_id" validate:"required"` MediaItemID string `json:"media_item_id" validate:"required"`
Updates struct { Updates struct {
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
Author *string `json:"author,omitempty"` Author *string `json:"author,omitempty"`
Genre *string `json:"genre,omitempty"` Genre *string `json:"genre,omitempty"`
Language *string `json:"language,omitempty"` Language *string `json:"language,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
} `json:"updates"` } `json:"updates"`
} `json:"updates" validate:"required"` } `json:"media_item_updates" validate:"required"`
} }
if err := c.Bind(&req); err != nil { if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
} }
if len(req.Updates) == 0 { if len(req.MediaItemUpdates) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "updates required"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_item_updates required"})
} }
results := make([]map[string]interface{}, 0) results := make([]map[string]interface{}, 0)
successCount := 0 successCount := 0
failedCount := 0 failedCount := 0
for _, update := range req.Updates { for _, update := range req.MediaItemUpdates {
bookID, err := uuid.Parse(update.BookID) mediaID, err := uuid.Parse(update.MediaItemID)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": update.BookID, "media_item_id": update.MediaItemID,
"status": "error", "status": "error",
"error": "Invalid UUID", "error": "Invalid UUID",
}) })
failedCount++ failedCount++
continue continue
} }
bookUUID := pgtype.UUID{Bytes: bookID, Valid: true} mediaUUID := pgtype.UUID{Bytes: mediaID, Valid: true}
existingBook, err := h.db.GetMediaItem(c.Request().Context(), bookUUID) existingMedia, err := h.db.GetMediaItem(c.Request().Context(), mediaUUID)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": update.BookID, "media_item_id": update.MediaItemID,
"status": "error", "status": "error",
"error": "Book not found", "error": "Media item not found",
}) })
failedCount++ failedCount++
continue continue
} }
updateParams := database.UpdateMediaItemParams{ updateParams := database.UpdateMediaItemParams{
ID: bookUUID, ID: mediaUUID,
Title: existingBook.Title, Title: existingMedia.Title,
Author: existingBook.Author, Author: existingMedia.Author,
Genre: existingBook.Genre, Genre: existingMedia.Genre,
Language: existingBook.Language, Language: existingMedia.Language,
Tags: existingBook.Tags, Tags: existingMedia.Tags,
TagsSearch: existingBook.TagsSearch, TagsSearch: existingMedia.TagsSearch,
Description: existingBook.Description, Description: existingMedia.Description,
Publisher: existingBook.Publisher, Publisher: existingMedia.Publisher,
CopyrightYear: existingBook.CopyrightYear, CopyrightYear: existingMedia.CopyrightYear,
Isbn: existingBook.Isbn, Isbn: existingMedia.Isbn,
Series: existingBook.Series, Series: existingMedia.Series,
SeriesNumber: existingBook.SeriesNumber, SeriesNumber: existingMedia.SeriesNumber,
Asin: existingBook.Asin, Asin: existingMedia.Asin,
DatePublished: existingBook.DatePublished, DatePublished: existingMedia.DatePublished,
Contributors: existingBook.Contributors, Contributors: existingMedia.Contributors,
ContributorsSearch: existingBook.ContributorsSearch, ContributorsSearch: existingMedia.ContributorsSearch,
Edition: existingBook.Edition, Edition: existingMedia.Edition,
PageCount: existingBook.PageCount, PageCount: existingMedia.PageCount,
GoodreadsID: existingBook.GoodreadsID, GoodreadsID: existingMedia.GoodreadsID,
OpenlibraryID: existingBook.OpenlibraryID, OpenlibraryID: existingMedia.OpenlibraryID,
CoverImagePath: existingBook.CoverImagePath, CoverImagePath: existingMedia.CoverImagePath,
} }
if update.Updates.Title != nil { if update.Updates.Title != nil {
@@ -515,25 +515,25 @@ func (h *MediaHandler) HandleBulkUpdate(c echo.Context) error {
_, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams) _, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams)
if err != nil { if err != nil {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": update.BookID, "media_item_id": update.MediaItemID,
"status": "error", "status": "error",
"error": err.Error(), "error": err.Error(),
}) })
failedCount++ failedCount++
continue continue
} }
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"book_id": update.BookID, "media_item_id": update.MediaItemID,
"status": "success", "status": "success",
}) })
successCount++ successCount++
} }
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"results": results, "results": results,
"total": len(req.Updates), "total": len(req.MediaItemUpdates),
"success": successCount, "updated": successCount,
"failed": failedCount, "failed": failedCount,
}) })
} }
+5 -5
View File
@@ -48,7 +48,7 @@ func registerMediaRoutes(cfg *Config) {
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem) admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
// Download route (public) // Download route (public)
e.GET("/api/books/:uuid/download", cfg.MediaHandler.DownloadBook) e.GET("/api/media-items/:uuid/download", cfg.MediaHandler.DownloadBook)
// Shelf management (protected) // Shelf management (protected)
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf) protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
@@ -56,8 +56,8 @@ func registerMediaRoutes(cfg *Config) {
protected.DELETE("/devices/:id/shelves", cfg.MediaHandler.RemoveFromShelf) protected.DELETE("/devices/:id/shelves", cfg.MediaHandler.RemoveFromShelf)
protected.DELETE("/devices/:id/shelves/clear", cfg.MediaHandler.ClearShelf) protected.DELETE("/devices/:id/shelves/clear", cfg.MediaHandler.ClearShelf)
// Bulk book operations (protected) // Bulk media operations (protected)
books := protected.Group("/books") mediaItems := protected.Group("/media-items")
books.POST("/bulk-delete", cfg.MediaHandler.HandleBulkDelete) mediaItems.POST("/bulk-delete", cfg.MediaHandler.HandleBulkDelete)
books.POST("/bulk-update", cfg.MediaHandler.HandleBulkUpdate) mediaItems.POST("/bulk-update", cfg.MediaHandler.HandleBulkUpdate)
} }