From 704b03655b71fb9a090734a063d32cc22ecea8d4 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 29 May 2026 21:12:53 +0200 Subject: [PATCH] fix: episode refresh resilience and allanime fallback --- integrations/playback/allanime/client.go | 7 ++++++ internal/episodes/service/service.go | 31 +++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/integrations/playback/allanime/client.go b/integrations/playback/allanime/client.go index 31ee635..2e9f7bc 100644 --- a/integrations/playback/allanime/client.go +++ b/integrations/playback/allanime/client.go @@ -220,6 +220,7 @@ func (c *AllAnimeProvider) GetEpisodeAvailabilityByProviderID(ctx context.Contex func (c *AllAnimeProvider) resolveShowIDStrict(ctx context.Context, animeID int, titleCandidates []string, mode string) (string, error) { targetMalIDStr := strconv.Itoa(animeID) + var firstAvailableShowID string for _, title := range titleCandidates { searchResults, err := c.Search(ctx, title, mode) if err != nil { @@ -229,8 +230,14 @@ func (c *AllAnimeProvider) resolveShowIDStrict(ctx context.Context, animeID int, if res.MalID == targetMalIDStr { return res.ID, nil } + if firstAvailableShowID == "" { + firstAvailableShowID = res.ID + } } } + if firstAvailableShowID != "" { + return firstAvailableShowID, nil + } return "", fmt.Errorf("allanime: no strict mal id match for %d", animeID) } diff --git a/internal/episodes/service/service.go b/internal/episodes/service/service.go index 50a8d4d..460bee4 100644 --- a/internal/episodes/service/service.go +++ b/internal/episodes/service/service.go @@ -80,7 +80,20 @@ func (s *EpisodeService) RefreshTrackedDue(ctx context.Context, limit int) error return fmt.Errorf("get due tracked anime: %w", err) } - for _, id := range ids { + for i, id := range ids { + if ctx.Err() != nil { + observability.Warn( + "episodes_worker_tick_interrupted", + "episodes", + "", + map[string]any{ + "anime_id": id, + "remaining": len(ids) - i, + }, + ctx.Err(), + ) + break + } anime, err := s.jikan.GetAnimeByID(ctx, int(id)) if err != nil { observability.Warn( @@ -152,7 +165,13 @@ func (s *EpisodeService) refresh(ctx context.Context, anime domain.Anime) (domai return cached, nil } if jikanErr == nil { - return s.store(ctx, anime, jikanEpisodes, domain.EpisodeAvailability{}, "jikan_fallback", now, false) + storeCtx := ctx + if ctx.Err() != nil { + var cancel context.CancelFunc + storeCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + } + return s.store(storeCtx, anime, jikanEpisodes, domain.EpisodeAvailability{}, "jikan_fallback", now, false) } return domain.CanonicalEpisodeList{}, providerErr } @@ -385,7 +404,13 @@ func (s *EpisodeService) markFailure(ctx context.Context, anime domain.Anime, ca nextSQL = sql.NullTime{Time: next, Valid: true} } - err := s.queries.MarkEpisodeAvailabilityRefreshFailed(ctx, db.MarkEpisodeAvailabilityRefreshFailedParams{ + writeCtx := ctx + if ctx.Err() != nil { + var cancel context.CancelFunc + writeCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + } + err := s.queries.MarkEpisodeAvailabilityRefreshFailed(writeCtx, db.MarkEpisodeAvailabilityRefreshFailedParams{ LastAttemptAt: sql.NullTime{Time: now, Valid: true}, LastError: truncate(cause.Error(), 400), NextRefreshAt: nextSQL,