feat: estimate released episode count for airing anime

This commit is contained in:
2026-06-16 18:36:53 +02:00
committed by Milas Holsting
parent 59e25d414c
commit ac91bd945e
2 changed files with 106 additions and 0 deletions

View File

@@ -20,6 +20,23 @@ const (
audioLookupTimeout = 8 * time.Second
)
func releasedEpisodeCount(anime domain.Anime, now time.Time) int {
if !anime.Airing || anime.Aired.From == "" {
return 0
}
firstAired, err := time.Parse(time.RFC3339, anime.Aired.From)
if err != nil || now.Before(firstAired) {
return 0
}
count := int(now.Sub(firstAired)/(7*24*time.Hour)) + 1
if anime.Episodes > 0 && count > anime.Episodes {
return anime.Episodes
}
return count
}
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
hasKnownSub := false
for _, episode := range episodes {
@@ -105,6 +122,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
}
audioAvailability := h.animeAudioAvailability(c.Request.Context(), anime)
episodesCount := releasedEpisodeCount(anime, time.Now())
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
"Anime": anime,
@@ -115,6 +133,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
"WatchlistIDs": watchlistIDs,
"ContinueWatchingEp": ep,
"ContinueWatchingTime": cwSeconds,
"EpisodesCount": episodesCount,
})
}

View File

@@ -6,6 +6,7 @@ import (
"mal/integrations/jikan"
"mal/internal/domain"
"testing"
"time"
)
type stubEpisodeService struct {
@@ -26,6 +27,92 @@ func (s *stubEpisodeService) RefreshTrackedDue(ctx context.Context, limit int) e
return nil
}
type releasedCountTest struct {
name string
anime domain.Anime
now time.Time
want int
}
var releasedCountTests = []releasedCountTest{
{
name: "weekly airing count",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Episodes: 24,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.June, 13, 15, 0, 0, 0, time.UTC),
want: 11,
},
{
name: "before first release",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.April, 4, 14, 59, 0, 0, time.UTC),
want: 0,
},
{
name: "first release counts as one",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.April, 4, 15, 0, 0, 0, time.UTC),
want: 1,
},
{
name: "caps at total episode count",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Episodes: 12,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.December, 1, 15, 0, 0, 0, time.UTC),
want: 12,
},
{
name: "unknown total still estimates current count",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
want: 3,
},
{
name: "non airing anime is not estimated",
anime: domain.Anime{Anime: jikan.Anime{
Airing: false,
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
}},
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
want: 0,
},
{
name: "invalid aired date is ignored",
anime: domain.Anime{Anime: jikan.Anime{
Airing: true,
Aired: jikan.Aired{From: "not-a-date"},
}},
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
want: 0,
},
}
func TestReleasedEpisodeCount(t *testing.T) {
for _, tt := range releasedCountTests {
t.Run(tt.name, func(t *testing.T) {
got := releasedEpisodeCount(tt.anime, tt.now)
if got != tt.want {
t.Fatalf("releasedEpisodeCount() = %d, want %d", got, tt.want)
}
})
}
}
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
tests := []struct {
name string