feat: estimate released episode count for airing anime
This commit is contained in:
@@ -20,6 +20,23 @@ const (
|
|||||||
audioLookupTimeout = 8 * time.Second
|
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 {
|
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
|
||||||
hasKnownSub := false
|
hasKnownSub := false
|
||||||
for _, episode := range episodes {
|
for _, episode := range episodes {
|
||||||
@@ -105,6 +122,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
audioAvailability := h.animeAudioAvailability(c.Request.Context(), anime)
|
audioAvailability := h.animeAudioAvailability(c.Request.Context(), anime)
|
||||||
|
episodesCount := releasedEpisodeCount(anime, time.Now())
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
||||||
"Anime": anime,
|
"Anime": anime,
|
||||||
@@ -115,6 +133,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
|
|||||||
"WatchlistIDs": watchlistIDs,
|
"WatchlistIDs": watchlistIDs,
|
||||||
"ContinueWatchingEp": ep,
|
"ContinueWatchingEp": ep,
|
||||||
"ContinueWatchingTime": cwSeconds,
|
"ContinueWatchingTime": cwSeconds,
|
||||||
|
"EpisodesCount": episodesCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"mal/integrations/jikan"
|
"mal/integrations/jikan"
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stubEpisodeService struct {
|
type stubEpisodeService struct {
|
||||||
@@ -26,6 +27,92 @@ func (s *stubEpisodeService) RefreshTrackedDue(ctx context.Context, limit int) e
|
|||||||
return nil
|
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) {
|
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user