diff --git a/internal/anime/handler.go b/internal/anime/handler.go index 175ce07..1e9b39d 100644 --- a/internal/anime/handler.go +++ b/internal/anime/handler.go @@ -121,6 +121,7 @@ func (h *AnimeHandler) Register(r *gin.Engine) { r.GET("/api/catalog/continue", h.HandleCatalogContinue) r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou) r.GET("/discover", h.HandleDiscover) + r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou) r.GET("/api/discover/trending", h.HandleDiscoverTrending) r.GET("/api/discover/upcoming", h.HandleDiscoverUpcoming) r.GET("/api/discover/top", h.HandleDiscoverTop) @@ -302,6 +303,37 @@ func (h *AnimeHandler) HandleDiscover(c *gin.Context) { }) } +func (h *AnimeHandler) HandleDiscoverTopPicksForYou(c *gin.Context) { + user := server.CurrentUser(c) + userID := server.CurrentUserID(c) + + data, err := h.svc.GetTopPicksForYou(c.Request.Context(), userID) + if err != nil { + observability.Warn( + "top_picks_for_you_fetch_failed", + "anime", + "", + map[string]any{ + "user_id": userID, + }, + err, + ) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, data.Animes) + + c.HTML(http.StatusOK, "discover.gohtml", gin.H{ + "_fragment": "", + "CurrentPath": "/discover", + "User": user, + "Animes": data.Animes, + "WatchlistMap": watchlistMap, + "IsTopPicks": true, + }) +} + func (h *AnimeHandler) HandleDiscoverTrending(c *gin.Context) { h.renderDiscoverSection(c, "Trending") } diff --git a/internal/anime/recommendations.go b/internal/anime/recommendations.go index 4476e92..8c86543 100644 --- a/internal/anime/recommendations.go +++ b/internal/anime/recommendations.go @@ -16,6 +16,7 @@ const ( forYouMaxRecommendations = 10 forYouCandidateFetchLimit = 60 forYouResultLimit = 18 + forYouFullResultLimit = 60 forYouProfileSearchLimit = 8 forYouProfileGenreSearches = 2 forYouProfileThemeSearches = 2 diff --git a/internal/anime/service.go b/internal/anime/service.go index bc70afc..d8bac6b 100644 --- a/internal/anime/service.go +++ b/internal/anime/service.go @@ -109,6 +109,18 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se } func (s *animeService) GetTopPickForYou(ctx context.Context, userID string) (domain.CatalogSectionData, error) { + return s.getTopPicksForYou(ctx, userID, forYouResultLimit) +} + +func (s *animeService) GetTopPicksForYou(ctx context.Context, userID string) (domain.CatalogSectionData, error) { + return s.getTopPicksForYou(ctx, userID, forYouFullResultLimit) +} + +func (s *animeService) getTopPicksForYou( + ctx context.Context, + userID string, + resultLimit int, +) (domain.CatalogSectionData, error) { if strings.TrimSpace(userID) == "" { return domain.CatalogSectionData{Animes: []domain.Anime{}}, nil } @@ -342,7 +354,7 @@ func (s *animeService) GetTopPickForYou(ctx context.Context, userID string) (dom }) return domain.CatalogSectionData{ - Animes: rerankRecommendationCandidates(candidates, forYouResultLimit), + Animes: rerankRecommendationCandidates(candidates, resultLimit), }, nil } diff --git a/internal/domain/anime.go b/internal/domain/anime.go index 6f3300c..07d1b1c 100644 --- a/internal/domain/anime.go +++ b/internal/domain/anime.go @@ -134,6 +134,7 @@ type ReviewEntry struct { type AnimeCatalogService interface { GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error) GetTopPickForYou(ctx context.Context, userID string) (CatalogSectionData, error) + GetTopPicksForYou(ctx context.Context, userID string) (CatalogSectionData, error) } type AnimeDiscoverService interface { diff --git a/templates/discover.gohtml b/templates/discover.gohtml index 3dd1e37..8d2b5d2 100644 --- a/templates/discover.gohtml +++ b/templates/discover.gohtml @@ -1,5 +1,34 @@ {{define "title"}}Discover{{end}} {{define "content"}} +{{if .IsTopPicks}} +
The full ranked list from your current watchlist profile.
+No top picks yet
+Add a few anime to your watchlist so the recommender has something to learn from.
+