feat: expose episode cache metadata
This commit is contained in:
@@ -28,6 +28,10 @@ type CanonicalEpisodeList struct {
|
|||||||
Episodes []CanonicalEpisode `json:"episodes"`
|
Episodes []CanonicalEpisode `json:"episodes"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
NextRefreshAt string `json:"next_refresh_at,omitempty"`
|
NextRefreshAt string `json:"next_refresh_at,omitempty"`
|
||||||
|
RetryUntilAt string `json:"retry_until_at,omitempty"`
|
||||||
|
LastAttemptAt string `json:"last_attempt_at,omitempty"`
|
||||||
|
LastSuccessAt string `json:"last_success_at,omitempty"`
|
||||||
|
FailureCount int64 `json:"failure_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EpisodeService interface {
|
type EpisodeService interface {
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ func (s *EpisodeService) store(ctx context.Context, anime domain.Anime, jikanEpi
|
|||||||
nextRefreshSQL := nextRefreshAt(anime, now)
|
nextRefreshSQL := nextRefreshAt(anime, now)
|
||||||
episodes := mergeEpisodes(jikanEpisodes, availability, anime.Episodes)
|
episodes := mergeEpisodes(jikanEpisodes, availability, anime.Episodes)
|
||||||
payload := domain.CanonicalEpisodeList{
|
payload := domain.CanonicalEpisodeList{
|
||||||
AnimeID: anime.MalID,
|
AnimeID: anime.MalID,
|
||||||
Episodes: episodes,
|
Episodes: episodes,
|
||||||
Source: source,
|
Source: source,
|
||||||
|
LastAttemptAt: now.Format(time.RFC3339),
|
||||||
|
FailureCount: 0,
|
||||||
|
}
|
||||||
|
if providerSuccess {
|
||||||
|
payload.LastSuccessAt = now.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
if nextRefreshSQL.Valid {
|
if nextRefreshSQL.Valid {
|
||||||
payload.NextRefreshAt = nextRefreshSQL.Time.Format(time.RFC3339)
|
payload.NextRefreshAt = nextRefreshSQL.Time.Format(time.RFC3339)
|
||||||
@@ -146,6 +151,7 @@ func (s *EpisodeService) getFreshCached(ctx context.Context, anime domain.Anime)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return domain.CanonicalEpisodeList{}, false
|
return domain.CanonicalEpisodeList{}, false
|
||||||
}
|
}
|
||||||
|
payload = enrichCachedPayload(payload, row)
|
||||||
observability.Info(
|
observability.Info(
|
||||||
"episodes_cache_served",
|
"episodes_cache_served",
|
||||||
"episodes",
|
"episodes",
|
||||||
@@ -168,9 +174,27 @@ func (s *EpisodeService) getDecodedCached(ctx context.Context, anime domain.Anim
|
|||||||
if !ok {
|
if !ok {
|
||||||
return domain.CanonicalEpisodeList{}, false
|
return domain.CanonicalEpisodeList{}, false
|
||||||
}
|
}
|
||||||
|
payload = enrichCachedPayload(payload, row)
|
||||||
return payload, true
|
return payload, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enrichCachedPayload(payload domain.CanonicalEpisodeList, row db.EpisodeAvailabilityCache) domain.CanonicalEpisodeList {
|
||||||
|
if row.NextRefreshAt.Valid {
|
||||||
|
payload.NextRefreshAt = row.NextRefreshAt.Time.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if row.RetryUntilAt.Valid {
|
||||||
|
payload.RetryUntilAt = row.RetryUntilAt.Time.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if row.LastAttemptAt.Valid {
|
||||||
|
payload.LastAttemptAt = row.LastAttemptAt.Time.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if row.LastSuccessAt.Valid {
|
||||||
|
payload.LastSuccessAt = row.LastSuccessAt.Time.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
payload.FailureCount = row.FailureCount
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
func (s *EpisodeService) isFreshEpisodeCache(anime domain.Anime, row db.EpisodeAvailabilityCache, now time.Time) bool {
|
func (s *EpisodeService) isFreshEpisodeCache(anime domain.Anime, row db.EpisodeAvailabilityCache, now time.Time) bool {
|
||||||
if row.NextRefreshAt.Valid && !row.NextRefreshAt.Time.After(now) {
|
if row.NextRefreshAt.Valid && !row.NextRefreshAt.Time.After(now) {
|
||||||
observability.Info(
|
observability.Info(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"mal/integrations/jikan"
|
"mal/integrations/jikan"
|
||||||
|
"mal/internal/db"
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -127,6 +129,37 @@ func TestIsCanonicalEpisodePayloadValidAllowsJikanFallbackWithoutAvailability(t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnrichCachedPayloadAddsRefreshMetadata(t *testing.T) {
|
||||||
|
now := time.Date(2026, time.June, 27, 11, 0, 0, 0, time.UTC)
|
||||||
|
payload := enrichCachedPayload(domain.CanonicalEpisodeList{
|
||||||
|
AnimeID: 59970,
|
||||||
|
Episodes: []domain.CanonicalEpisode{{Number: 1}},
|
||||||
|
Source: "AllAnime",
|
||||||
|
}, db.EpisodeAvailabilityCache{
|
||||||
|
NextRefreshAt: sql.NullTime{Time: now.Add(time.Hour), Valid: true},
|
||||||
|
RetryUntilAt: sql.NullTime{Time: now.Add(30 * time.Minute), Valid: true},
|
||||||
|
LastAttemptAt: sql.NullTime{Time: now.Add(-5 * time.Minute), Valid: true},
|
||||||
|
LastSuccessAt: sql.NullTime{Time: now.Add(-time.Hour), Valid: true},
|
||||||
|
FailureCount: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if payload.NextRefreshAt != "2026-06-27T12:00:00Z" {
|
||||||
|
t.Fatalf("NextRefreshAt = %q, want RFC3339 timestamp", payload.NextRefreshAt)
|
||||||
|
}
|
||||||
|
if payload.RetryUntilAt != "2026-06-27T11:30:00Z" {
|
||||||
|
t.Fatalf("RetryUntilAt = %q, want RFC3339 timestamp", payload.RetryUntilAt)
|
||||||
|
}
|
||||||
|
if payload.LastAttemptAt != "2026-06-27T10:55:00Z" {
|
||||||
|
t.Fatalf("LastAttemptAt = %q, want RFC3339 timestamp", payload.LastAttemptAt)
|
||||||
|
}
|
||||||
|
if payload.LastSuccessAt != "2026-06-27T10:00:00Z" {
|
||||||
|
t.Fatalf("LastSuccessAt = %q, want RFC3339 timestamp", payload.LastSuccessAt)
|
||||||
|
}
|
||||||
|
if payload.FailureCount != 2 {
|
||||||
|
t.Fatalf("FailureCount = %d, want 2", payload.FailureCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNextBroadcastAfterUsesJikanTimezone(t *testing.T) {
|
func TestNextBroadcastAfterUsesJikanTimezone(t *testing.T) {
|
||||||
anime := domain.Anime{Anime: jikan.Anime{MalID: 1}}
|
anime := domain.Anime{Anime: jikan.Anime{MalID: 1}}
|
||||||
anime.Broadcast.Day = "Saturdays"
|
anime.Broadcast.Day = "Saturdays"
|
||||||
|
|||||||
Reference in New Issue
Block a user