diff --git a/internal/features/anime/errors.go b/internal/features/anime/errors.go new file mode 100644 index 0000000..a62b60b --- /dev/null +++ b/internal/features/anime/errors.go @@ -0,0 +1,5 @@ +package anime + +import "errors" + +var ErrAnimePendingFetch = errors.New("anime pending fetch") diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index f080323..8ef34dd 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -2,6 +2,7 @@ package anime import ( "encoding/json" + "errors" "log" "net/http" "strconv" @@ -100,13 +101,18 @@ func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { page := parsePageParam(r) - res, err := h.svc.GetTopAnime(r.Context(), page) + res, fallbackPlaceholder, err := h.svc.GetTopAnimeWithPlaceholder(r.Context(), page) if err != nil { log.Printf("top anime error: %v", err) http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError) return } + if fallbackPlaceholder { + templates.CatalogPlaceholderItems(24).Render(r.Context(), w) + return + } + res.Animes = deduplicateAnimes(res.Animes) templates.CatalogItems(res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) @@ -124,6 +130,16 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { anime, currentStatus, err := h.svc.GetAnimeDetails(r.Context(), id, userID) if err != nil { + if errors.Is(err, ErrAnimePendingFetch) { + templates.AnimePending(id).Render(r.Context(), w) + return + } + + if jikan.IsNotFoundError(err) { + http.NotFound(w, r) + return + } + log.Printf("anime fetch error for %d: %v", id, err) http.Error(w, "Failed to fetch anime details", http.StatusInternalServerError) return diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go index ecca9da..059775e 100644 --- a/internal/features/anime/service.go +++ b/internal/features/anime/service.go @@ -29,6 +29,19 @@ func (s *Service) GetTopAnime(ctx context.Context, page int) (jikan.TopAnimeResu return s.jikanClient.GetTopAnime(ctx, page) } +func (s *Service) GetTopAnimeWithPlaceholder(ctx context.Context, page int) (jikan.TopAnimeResult, bool, error) { + result, err := s.jikanClient.GetTopAnime(ctx, page) + if err == nil { + return result, false, nil + } + + if jikan.IsRetryableError(err) { + return jikan.TopAnimeResult{}, true, nil + } + + return jikan.TopAnimeResult{}, false, err +} + func (s *Service) GetAiringAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) { return s.jikanClient.GetSeasonsNow(ctx, page) } @@ -40,6 +53,15 @@ func (s *Service) GetUpcomingAnime(ctx context.Context, page int) (jikan.TopAnim func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) { anime, err := s.jikanClient.GetAnimeByID(ctx, id) if err != nil { + if jikan.IsNotFoundError(err) { + return jikan.Anime{}, "", err + } + + s.jikanClient.EnqueueAnimeFetchRetry(ctx, id, err) + if jikan.IsRetryableError(err) { + return jikan.Anime{}, "", ErrAnimePendingFetch + } + return jikan.Anime{}, "", fmt.Errorf("failed to fetch anime details: %w", err) } @@ -75,6 +97,9 @@ func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templa for _, row := range rows { anime, err := s.jikanClient.GetAnimeByID(ctx, int(row.AnimeID)) if err != nil { + if jikan.IsRetryableError(err) { + s.jikanClient.EnqueueAnimeFetchRetry(ctx, int(row.AnimeID), err) + } // Instead of skipping, we still append it, but without the extra Jikan details // This prevents anime from vanishing from the watchlist when Jikan rate limits us. anime = jikan.Anime{} diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ index 740103e..cd1462c 100644 --- a/internal/templates/anime.templ +++ b/internal/templates/anime.templ @@ -165,6 +165,25 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) { } } +templ AnimePending(id int) { + @Layout("mal - anime pending", true) { +
We could not load this anime right now. A background worker is retrying data fetch for anime #{ fmt.Sprintf("%d", id) }.
+Refresh this page in a few seconds.
+