package handlers import ( "bookhoard/internal/database" "bookhoard/internal/services" "bookhoard/internal/utils" "context" "net/http" "strconv" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) type SeriesHandler struct { seriesService *services.SeriesService } func NewSeriesHandler(db *database.Queries) *SeriesHandler { return &SeriesHandler{ seriesService: services.NewSeriesService(db), } } func (h *SeriesHandler) GetSeries(c *echo.Context) error { libraryID := c.QueryParam("library_id") var libUUID pgtype.UUID if libraryID != "" { parsed, err := uuid.Parse(libraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) } libUUID = pgtype.UUID{Bytes: parsed, Valid: true} } limit := 20 if l := c.QueryParam("limit"); l != "" { if v, err := strconv.Atoi(l); err == nil && v > 0 { limit = v if limit > 100 { limit = 100 } } } offset := 0 if o := c.QueryParam("offset"); o != "" { if v, err := strconv.Atoi(o); err == nil && v >= 0 { offset = v } } seriesList, total, err := h.seriesService.GetSeriesPage(c.Request().Context(), libUUID, limit, offset) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series"}) } type SeriesResponse struct { Name string `json:"name"` BookCount int64 `json:"book_count"` TotalInSeries int `json:"total_in_series"` CoverPaths []string `json:"cover_paths"` LastEntryAt string `json:"last_entry_at"` } response := make([]SeriesResponse, 0, len(seriesList)) for _, s := range seriesList { response = append(response, SeriesResponse{ Name: s.Name, BookCount: s.BookCount, TotalInSeries: s.TotalInSeries, CoverPaths: s.CoverPaths, LastEntryAt: s.LastEntryAt, }) } return c.JSON(http.StatusOK, map[string]interface{}{ "series": response, "total": total, "limit": limit, "offset": offset, }) } func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error { seriesName := c.QueryParam("name") if seriesName == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "name required"}) } books, err := h.seriesService.GetSeriesBooks(c.Request().Context(), seriesName) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series books"}) } bookCards := make([]BookInfo, 0, len(books)) for _, item := range books { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) bookCards = append(bookCards, BookInfo{ MediaItemID: itemUUID.String(), Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), }) } return c.JSON(http.StatusOK, map[string]interface{}{ "name": seriesName, "books": bookCards, "total": len(bookCards), }) } func GetSeriesCardsData(ctx context.Context, db *database.Queries, libraryID pgtype.UUID, limit, offset int) ([]services.SeriesInfo, int, error) { svc := services.NewSeriesService(db) return svc.GetSeriesPage(ctx, libraryID, limit, offset) }