ui: add pending and catalog placeholders
This commit is contained in:
5
internal/features/anime/errors.go
Normal file
5
internal/features/anime/errors.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package anime
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrAnimePendingFetch = errors.New("anime pending fetch")
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user