diff --git a/internal/anime/browse_handler.go b/internal/anime/browse_handler.go index 57a658f..af1cf86 100644 --- a/internal/anime/browse_handler.go +++ b/internal/anime/browse_handler.go @@ -35,17 +35,19 @@ type browseQuery struct { func producerQueryParams(c *gin.Context) (string, int, int, error) { q := strings.TrimSpace(c.Query("q")) - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + rawPage := c.DefaultQuery("page", "1") + page, err := strconv.Atoi(rawPage) if err != nil { - return "", 0, 0, fmt.Errorf("invalid page") + return "", 0, 0, fmt.Errorf("invalid page %q: %w", rawPage, err) } if page < 1 { page = 1 } - limit, err := strconv.Atoi(c.DefaultQuery("limit", "50")) + rawLimit := c.DefaultQuery("limit", "50") + limit, err := strconv.Atoi(rawLimit) if err != nil { - return "", 0, 0, fmt.Errorf("invalid limit") + return "", 0, 0, fmt.Errorf("invalid limit %q: %w", rawLimit, err) } if limit < 1 || limit > 12 { limit = 12 @@ -137,8 +139,11 @@ func parseBrowseQuery(c *gin.Context) (browseQuery, error) { studioID := 0 if raw := strings.TrimSpace(c.Query("studio")); raw != "" { id, err := strconv.Atoi(raw) - if err != nil || id < 0 { - return browseQuery{}, fmt.Errorf("invalid studio id") + if err != nil { + return browseQuery{}, fmt.Errorf("invalid studio id %q: %w", raw, err) + } + if id < 0 { + return browseQuery{}, fmt.Errorf("invalid studio id %d", id) } studioID = id } @@ -147,16 +152,17 @@ func parseBrowseQuery(c *gin.Context) (browseQuery, error) { for _, g := range c.QueryArray("genres") { id, err := strconv.Atoi(g) if err != nil { - return browseQuery{}, fmt.Errorf("invalid genre id") + return browseQuery{}, fmt.Errorf("invalid genre id %q: %w", g, err) } if id > 0 { genres = append(genres, id) } } - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + rawPage := c.DefaultQuery("page", "1") + page, err := strconv.Atoi(rawPage) if err != nil { - return browseQuery{}, fmt.Errorf("invalid page") + return browseQuery{}, fmt.Errorf("invalid page %q: %w", rawPage, err) } if page < 1 { page = 1 diff --git a/internal/anime/recommendations/engine.go b/internal/anime/recommendations/engine.go index 372726e..c1a0dd5 100644 --- a/internal/anime/recommendations/engine.go +++ b/internal/anime/recommendations/engine.go @@ -2,6 +2,7 @@ package recommendations import ( "context" + "fmt" "mal/integrations/jikan" "mal/internal/domain" "mal/internal/observability" @@ -35,7 +36,7 @@ func (e engine) getTopPicksForYou(ctx context.Context, userID string, resultLimi watchlist, err := e.repo.GetUserWatchList(ctx, userID) if err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("get user watchlist for %q: %w", userID, err) } now := time.Now() @@ -46,17 +47,17 @@ func (e engine) getTopPicksForYou(ctx context.Context, userID string, resultLimi seedAnimes, err := e.fetchSeedAnimes(ctx, seedPool) if err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("fetch seed animes: %w", err) } profile := buildTasteProfile(now, seedPool, seedAnimes) store := newCandidateStore(watchlist) if err := e.collectCollaborativeCandidates(ctx, seedPool, store); err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("collect collaborative candidates: %w", err) } if err := e.collectProfileSearchCandidates(ctx, profile, store); err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("collect profile search candidates: %w", err) } ranked := store.ranked() @@ -66,7 +67,7 @@ func (e engine) getTopPicksForYou(ctx context.Context, userID string, resultLimi candidates, err := e.scoreRankedCandidates(ctx, now, profile, ranked, resultLimit) if err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("score ranked candidates: %w", err) } return domain.CatalogSectionData{ @@ -83,7 +84,7 @@ func (e engine) fetchSeedAnimes(ctx context.Context, seedPool []recommendationSe g.Go(func() error { anime, err := e.jikan.GetAnimeByID(ctx, seed.animeID) if err != nil { - return err + return fmt.Errorf("get seed anime %d: %w", seed.animeID, err) } seedAnimes[i] = anime return nil @@ -91,7 +92,7 @@ func (e engine) fetchSeedAnimes(ctx context.Context, seedPool []recommendationSe } if err := g.Wait(); err != nil { - return nil, err + return nil, fmt.Errorf("wait for seed anime fetches: %w", err) } return seedAnimes, nil @@ -131,7 +132,10 @@ func (e engine) collectCollaborativeCandidates(ctx context.Context, seedPool []r }) } - return g.Wait() + if err := g.Wait(); err != nil { + return fmt.Errorf("wait for collaborative candidate fetches: %w", err) + } + return nil } func (e engine) collectProfileSearchCandidates(ctx context.Context, profile userTasteProfile, store *candidateStore) error { @@ -183,7 +187,10 @@ func (e engine) collectProfileSearchCandidates(ctx context.Context, profile user }) } - return g.Wait() + if err := g.Wait(); err != nil { + return fmt.Errorf("wait for profile search candidate fetches: %w", err) + } + return nil } func (e engine) scoreRankedCandidates( @@ -233,7 +240,7 @@ func (e engine) scoreRankedCandidates( } if err := g.Wait(); err != nil { - return nil, err + return nil, fmt.Errorf("wait for candidate scoring: %w", err) } sort.Slice(candidates, func(i, j int) bool { diff --git a/internal/anime/reviews_handler.go b/internal/anime/reviews_handler.go index 11cc375..e1debb4 100644 --- a/internal/anime/reviews_handler.go +++ b/internal/anime/reviews_handler.go @@ -15,14 +15,19 @@ type reviewsQuery struct { } func parseReviewsQuery(c *gin.Context) (reviewsQuery, error) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil || id <= 0 { - return reviewsQuery{}, fmt.Errorf("invalid anime id") + rawID := c.Param("id") + id, err := strconv.Atoi(rawID) + if err != nil { + return reviewsQuery{}, fmt.Errorf("invalid anime id %q: %w", rawID, err) + } + if id <= 0 { + return reviewsQuery{}, fmt.Errorf("invalid anime id %d", id) } - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + rawPage := c.DefaultQuery("page", "1") + page, err := strconv.Atoi(rawPage) if err != nil { - return reviewsQuery{}, fmt.Errorf("invalid page") + return reviewsQuery{}, fmt.Errorf("invalid page %q: %w", rawPage, err) } if page < 1 { page = 1 diff --git a/internal/anime/service.go b/internal/anime/service.go index d193c84..96ddcf9 100644 --- a/internal/anime/service.go +++ b/internal/anime/service.go @@ -47,19 +47,25 @@ func (s *animeService) GetCatalogSection(ctx context.Context, userID string, sec case "Popular": res, err = s.jikan.GetTopAnime(gCtx, 1) } - return err + if err != nil { + return fmt.Errorf("get catalog section %q: %w", section, err) + } + return nil }) if userID != "" && section == "Continue" { g.Go(func() error { var err error cw, err = s.repo.GetContinueWatchingEntries(gCtx, userID) - return err + if err != nil { + return fmt.Errorf("get continue watching entries for %q: %w", userID, err) + } + return nil }) } if err := g.Wait(); err != nil { - return domain.CatalogSectionData{}, err + return domain.CatalogSectionData{}, fmt.Errorf("wait for catalog section %q: %w", section, err) } animes := wrapAnimes(res.Animes) @@ -300,7 +306,7 @@ func (s *animeService) GetRandomAnime(ctx context.Context) (domain.Anime, error) return domain.Anime{Anime: res.Animes[r.Intn(len(res.Animes))]}, nil } - return domain.Anime{}, err + return domain.Anime{}, fmt.Errorf("get random anime: %w", err) } func (s *animeService) GetAllEpisodes(ctx context.Context, id int) ([]domain.EpisodeData, error) {