fix: source anime episode counts from availability
This commit is contained in:
@@ -22,6 +22,11 @@ const (
|
|||||||
episodeCountTimeout = 4 * time.Second
|
episodeCountTimeout = 4 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type animeEpisodeCountDisplay struct {
|
||||||
|
Count int
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
func listedEpisodeCount(episodes []domain.EpisodeData) int {
|
func listedEpisodeCount(episodes []domain.EpisodeData) int {
|
||||||
count := 0
|
count := 0
|
||||||
for _, episode := range episodes {
|
for _, episode := range episodes {
|
||||||
@@ -50,7 +55,29 @@ func releasedEpisodeCount(anime domain.Anime, now time.Time) int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime, now time.Time) int {
|
func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime, now time.Time) animeEpisodeCountDisplay {
|
||||||
|
if h.episodeSvc != nil {
|
||||||
|
episodeCtx, cancel := context.WithTimeout(ctx, episodeCountTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
episodeList, err := h.episodeSvc.GetCanonicalEpisodes(episodeCtx, anime, false)
|
||||||
|
if err == nil {
|
||||||
|
if count := len(episodeList.Episodes); count > 0 {
|
||||||
|
return animeEpisodeCountDisplay{Count: count, Label: "Available episodes"}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
observability.Warn(
|
||||||
|
"anime_episode_availability_count_fetch_failed",
|
||||||
|
"anime",
|
||||||
|
"",
|
||||||
|
map[string]any{
|
||||||
|
"anime_id": anime.MalID,
|
||||||
|
},
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if h.svc != nil && anime.Airing {
|
if h.svc != nil && anime.Airing {
|
||||||
episodeCtx, cancel := context.WithTimeout(ctx, episodeCountTimeout)
|
episodeCtx, cancel := context.WithTimeout(ctx, episodeCountTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -58,7 +85,7 @@ func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime
|
|||||||
episodes, err := h.svc.GetAllEpisodes(episodeCtx, anime.MalID)
|
episodes, err := h.svc.GetAllEpisodes(episodeCtx, anime.MalID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if count := listedEpisodeCount(episodes); count > 0 {
|
if count := listedEpisodeCount(episodes); count > 0 {
|
||||||
return count
|
return animeEpisodeCountDisplay{Count: count, Label: "Listed episodes"}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
observability.Warn(
|
observability.Warn(
|
||||||
@@ -73,7 +100,13 @@ func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return releasedEpisodeCount(anime, now)
|
if anime.Episodes > 0 {
|
||||||
|
return animeEpisodeCountDisplay{Count: anime.Episodes, Label: "Total episodes"}
|
||||||
|
}
|
||||||
|
if count := releasedEpisodeCount(anime, now); count > 0 {
|
||||||
|
return animeEpisodeCountDisplay{Count: count, Label: "Estimated aired episodes"}
|
||||||
|
}
|
||||||
|
return animeEpisodeCountDisplay{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
|
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
|
||||||
@@ -160,7 +193,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
episodesCount := releasedEpisodeCount(anime, time.Now())
|
episodesCount := h.animeEpisodeCount(c.Request.Context(), anime, time.Now())
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
||||||
"Anime": anime,
|
"Anime": anime,
|
||||||
@@ -170,7 +203,8 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
|
|||||||
"WatchlistIDs": watchlistIDs,
|
"WatchlistIDs": watchlistIDs,
|
||||||
"ContinueWatchingEp": ep,
|
"ContinueWatchingEp": ep,
|
||||||
"ContinueWatchingTime": cwSeconds,
|
"ContinueWatchingTime": cwSeconds,
|
||||||
"EpisodesCount": episodesCount,
|
"EpisodesCount": episodesCount.Count,
|
||||||
|
"EpisodesCountLabel": episodesCount.Label,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type stubEpisodeService struct {
|
type stubEpisodeService struct {
|
||||||
episodes domain.CanonicalEpisodeList
|
episodes domain.CanonicalEpisodeList
|
||||||
err error
|
err error
|
||||||
forced bool
|
called int
|
||||||
|
forceRefresh bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubEpisodeService) GetCanonicalEpisodes(ctx context.Context, anime domain.Anime, forceRefresh bool) (domain.CanonicalEpisodeList, error) {
|
func (s *stubEpisodeService) GetCanonicalEpisodes(ctx context.Context, anime domain.Anime, forceRefresh bool) (domain.CanonicalEpisodeList, error) {
|
||||||
s.forced = forceRefresh
|
s.called++
|
||||||
|
s.forceRefresh = forceRefresh
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return domain.CanonicalEpisodeList{}, s.err
|
return domain.CanonicalEpisodeList{}, s.err
|
||||||
}
|
}
|
||||||
@@ -127,6 +129,52 @@ func TestListedEpisodeCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnimeEpisodeCountUsesCanonicalEpisodes(t *testing.T) {
|
||||||
|
episodeSvc := &stubEpisodeService{
|
||||||
|
episodes: domain.CanonicalEpisodeList{
|
||||||
|
Source: "AllAnime",
|
||||||
|
Episodes: []domain.CanonicalEpisode{
|
||||||
|
{Number: 1},
|
||||||
|
{Number: 2},
|
||||||
|
{Number: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler := NewAnimeHandler(nil, nil, episodeSvc)
|
||||||
|
|
||||||
|
got := handler.animeEpisodeCount(context.Background(), domain.Anime{Anime: jikan.Anime{
|
||||||
|
MalID: 59970,
|
||||||
|
Airing: true,
|
||||||
|
Episodes: 12,
|
||||||
|
Aired: jikan.Aired{From: "2026-04-03T00:00:00+00:00"},
|
||||||
|
}}, time.Date(2026, time.June, 21, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
if got.Count != 3 || got.Label != "Available episodes" {
|
||||||
|
t.Fatalf("animeEpisodeCount() = %+v, want count=3 label=%q", got, "Available episodes")
|
||||||
|
}
|
||||||
|
if episodeSvc.called != 1 {
|
||||||
|
t.Fatalf("GetCanonicalEpisodes() calls = %d, want 1", episodeSvc.called)
|
||||||
|
}
|
||||||
|
if episodeSvc.forceRefresh {
|
||||||
|
t.Fatal("animeEpisodeCount() should use fresh cache when available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimeEpisodeCountFallsBackToMetadata(t *testing.T) {
|
||||||
|
episodeSvc := &stubEpisodeService{err: errors.New("provider unavailable")}
|
||||||
|
handler := NewAnimeHandler(nil, nil, episodeSvc)
|
||||||
|
|
||||||
|
got := handler.animeEpisodeCount(context.Background(), domain.Anime{Anime: jikan.Anime{
|
||||||
|
MalID: 59970,
|
||||||
|
Airing: false,
|
||||||
|
Episodes: 12,
|
||||||
|
}}, time.Date(2026, time.June, 21, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
if got.Count != 12 || got.Label != "Total episodes" {
|
||||||
|
t.Fatalf("animeEpisodeCount() = %+v, want count=12 label=%q", got, "Total episodes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
|
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -217,7 +265,7 @@ func TestAnimeAudioAvailabilityRequiresAllAnimeSource(t *testing.T) {
|
|||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Fatalf("animeAudioAvailability() = %q, want %q", got, tt.want)
|
t.Fatalf("animeAudioAvailability() = %q, want %q", got, tt.want)
|
||||||
}
|
}
|
||||||
if !episodeSvc.forced {
|
if !episodeSvc.forceRefresh {
|
||||||
t.Fatal("animeAudioAvailability() did not force provider refresh")
|
t.Fatal("animeAudioAvailability() did not force provider refresh")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user