From 2b8332a16dab3e764cbd0a88b499e63e483ecfe4 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 10 Apr 2026 17:26:28 +0200 Subject: [PATCH] api: pass request context to jikan --- internal/features/anime/handler.go | 77 ++++++++++++++---------------- internal/features/anime/service.go | 32 ++++++------- internal/jikan/anime.go | 9 ++-- internal/jikan/client.go | 42 ++++++++++++---- internal/jikan/recommendations.go | 11 +++-- internal/jikan/relations.go | 21 ++++---- internal/jikan/search.go | 17 +++---- internal/jikan/seasons.go | 29 +++++------ internal/worker/relations.go | 8 ++-- 9 files changed, 134 insertions(+), 112 deletions(-) diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index 99b4501..31f493f 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -28,6 +28,24 @@ type Handler struct { svc *Service } +func parsePageParam(r *http.Request) int { + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + if page < 1 { + return 1 + } + + return page +} + +func userIDFromRequest(r *http.Request) string { + user, ok := r.Context().Value(middleware.UserContextKey).(*database.User) + if !ok || user == nil { + return "" + } + + return user.ID +} + func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } @@ -50,7 +68,7 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { } if r.Header.Get("HX-Request") == "true" { - res, err := h.svc.Search(query, 1) + res, err := h.svc.Search(r.Context(), query, 1) if err != nil { log.Printf("search error: %v", err) http.Error(w, "Failed to search anime", http.StatusInternalServerError) @@ -65,13 +83,9 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } + page := parsePageParam(r) - res, err := h.svc.Search(query, page) + res, err := h.svc.Search(r.Context(), query, page) if err != nil { log.Printf("search pagination error: %v", err) http.Error(w, "Failed to fetch search page", http.StatusInternalServerError) @@ -84,13 +98,9 @@ func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { } func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } + page := parsePageParam(r) - res, err := h.svc.GetTopAnime(page) + res, err := h.svc.GetTopAnime(r.Context(), page) if err != nil { log.Printf("top anime error: %v", err) http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError) @@ -110,10 +120,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { return } - userID := "" - if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil { - userID = user.ID - } + userID := userIDFromRequest(r) anime, currentStatus, err := h.svc.GetAnimeDetails(r.Context(), id, userID) if err != nil { @@ -141,7 +148,7 @@ func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request return } - relations, err := h.svc.GetRelations(id) + relations, err := h.svc.GetRelations(r.Context(), id) if err != nil { log.Printf("failed to get relations for anime %d: %v", id, err) http.Error(w, "Failed to load relations", http.StatusInternalServerError) @@ -169,7 +176,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { switch parts[1] { case "relations": - relations, err := h.svc.GetRelations(id) + relations, err := h.svc.GetRelations(r.Context(), id) if err != nil { log.Printf("relations error for %d: %v", id, err) w.Header().Set("Content-Type", "text/html") @@ -178,7 +185,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { } templates.AnimeRelationsList(relations).Render(r.Context(), w) case "recommendations": - recs, err := h.svc.GetRecommendations(id, 12) + recs, err := h.svc.GetRecommendations(r.Context(), id, 12) if err != nil { log.Printf("recommendations error for %d: %v", id, err) w.Header().Set("Content-Type", "text/html") @@ -219,7 +226,7 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) { return } - res, err := h.svc.Search(query, 1) + res, err := h.svc.Search(r.Context(), query, 1) if err != nil { log.Printf("quick search error: %v", err) w.Header().Set("Content-Type", "application/json") @@ -260,13 +267,9 @@ func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) { } func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) { - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } + page := parsePageParam(r) - res, err := h.svc.GetAiringAnime(page) + res, err := h.svc.GetAiringAnime(r.Context(), page) if err != nil { log.Printf("airing anime error: %v", err) http.Error(w, "Failed to fetch airing anime", http.StatusInternalServerError) @@ -279,13 +282,9 @@ func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request } func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) { - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } + page := parsePageParam(r) - res, err := h.svc.GetUpcomingAnime(page) + res, err := h.svc.GetUpcomingAnime(r.Context(), page) if err != nil { log.Printf("upcoming anime error: %v", err) http.Error(w, "Failed to fetch upcoming anime", http.StatusInternalServerError) @@ -307,7 +306,7 @@ func (h *Handler) HandleAPISchedule(w http.ResponseWriter, r *http.Request) { day = "monday" } - res, err := h.svc.GetSchedule(day) + res, err := h.svc.GetSchedule(r.Context(), day) if err != nil { log.Printf("schedule error for %s: %v", day, err) http.Error(w, "Failed to fetch schedule", http.StatusInternalServerError) @@ -320,10 +319,7 @@ func (h *Handler) HandleAPISchedule(w http.ResponseWriter, r *http.Request) { } func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) { - userID := "" - if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil { - userID = user.ID - } + userID := userIDFromRequest(r) if userID == "" { http.Redirect(w, r, "/login", http.StatusSeeOther) @@ -341,10 +337,7 @@ func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) { } func (h *Handler) HandleNotificationsUpcoming(w http.ResponseWriter, r *http.Request) { - userID := "" - if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil { - userID = user.ID - } + userID := userIDFromRequest(r) if userID == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go index 29832dc..5b3f8ce 100644 --- a/internal/features/anime/service.go +++ b/internal/features/anime/service.go @@ -21,24 +21,24 @@ func NewService(jikanClient *jikan.Client, db database.Querier) *Service { } } -func (s *Service) Search(query string, page int) (jikan.SearchResult, error) { - return s.jikanClient.Search(query, page) +func (s *Service) Search(ctx context.Context, query string, page int) (jikan.SearchResult, error) { + return s.jikanClient.Search(ctx, query, page) } -func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) { - return s.jikanClient.GetTopAnime(page) +func (s *Service) GetTopAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetTopAnime(ctx, page) } -func (s *Service) GetAiringAnime(page int) (jikan.TopAnimeResult, error) { - return s.jikanClient.GetSeasonsNow(page) +func (s *Service) GetAiringAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsNow(ctx, page) } -func (s *Service) GetUpcomingAnime(page int) (jikan.TopAnimeResult, error) { - return s.jikanClient.GetSeasonsUpcoming(page) +func (s *Service) GetUpcomingAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsUpcoming(ctx, page) } func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) { - anime, err := s.jikanClient.GetAnimeByID(id) + anime, err := s.jikanClient.GetAnimeByID(ctx, id) if err != nil { return jikan.Anime{}, "", fmt.Errorf("failed to fetch anime details: %w", err) } @@ -57,16 +57,16 @@ func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (j return anime, currentStatus, nil } -func (s *Service) GetRelations(id int) ([]jikan.RelationEntry, error) { - return s.jikanClient.GetFullRelations(id) +func (s *Service) GetRelations(ctx context.Context, id int) ([]jikan.RelationEntry, error) { + return s.jikanClient.GetFullRelations(ctx, id) } -func (s *Service) GetSchedule(day string) (jikan.ScheduleResult, error) { - return s.jikanClient.GetSchedule(day) +func (s *Service) GetSchedule(ctx context.Context, day string) (jikan.ScheduleResult, error) { + return s.jikanClient.GetSchedule(ctx, day) } -func (s *Service) GetRecommendations(animeID int, limit int) ([]jikan.Anime, error) { - return s.jikanClient.GetRecommendations(animeID, limit) +func (s *Service) GetRecommendations(ctx context.Context, animeID int, limit int) ([]jikan.Anime, error) { + return s.jikanClient.GetRecommendations(ctx, animeID, limit) } func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templates.WatchingAnimeWithDetails, error) { @@ -77,7 +77,7 @@ func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templa var result []templates.WatchingAnimeWithDetails for _, row := range rows { - anime, err := s.jikanClient.GetAnimeByID(int(row.AnimeID)) + anime, err := s.jikanClient.GetAnimeByID(ctx, int(row.AnimeID)) if err != nil { // Instead of skipping, we still append it, but without the extra Jikan details // This prevents anime from vanishing from the watchlist when Jikan rate limits us. diff --git a/internal/jikan/anime.go b/internal/jikan/anime.go index 4855600..34c5f6a 100644 --- a/internal/jikan/anime.go +++ b/internal/jikan/anime.go @@ -1,20 +1,21 @@ package jikan import ( + "context" "fmt" "time" ) -func (c *Client) GetAnimeByID(id int) (Anime, error) { +func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) { cacheKey := fmt.Sprintf("anime:%d", id) var cached Anime - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result AnimeResponse reqURL := fmt.Sprintf("%s/anime/%d/full", c.baseURL, id) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return Anime{}, err } @@ -23,6 +24,6 @@ func (c *Client) GetAnimeByID(id int) (Anime, error) { ttl = time.Hour * 24 * 30 } - c.setCache(cacheKey, result.Data, ttl) + c.setCache(ctx, cacheKey, result.Data, ttl) return result.Data, nil } diff --git a/internal/jikan/client.go b/internal/jikan/client.go index c4a1b00..f638a0e 100644 --- a/internal/jikan/client.go +++ b/internal/jikan/client.go @@ -27,7 +27,7 @@ func NewClient(db database.Querier) *Client { } } -func (c *Client) waitRateLimit() { +func (c *Client) waitRateLimit(ctx context.Context) error { c.mu.Lock() defer c.mu.Unlock() @@ -36,15 +36,24 @@ func (c *Client) waitRateLimit() { // 400ms base delay keeps us safely under the 3/sec limit. nextAllowed := c.lastReqTime.Add(400 * time.Millisecond) if now.Before(nextAllowed) { - time.Sleep(nextAllowed.Sub(now)) + timer := time.NewTimer(nextAllowed.Sub(now)) + defer timer.Stop() + + select { + case <-timer.C: + case <-ctx.Done(): + return fmt.Errorf("request canceled while waiting for rate limit: %w", ctx.Err()) + } c.lastReqTime = time.Now() } else { c.lastReqTime = now } + + return nil } -func (c *Client) getCache(key string, out any) bool { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) +func (c *Client) getCache(parentCtx context.Context, key string, out any) bool { + ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second) defer cancel() data, err := c.db.GetJikanCache(ctx, key) @@ -56,8 +65,8 @@ func (c *Client) getCache(key string, out any) bool { return err == nil } -func (c *Client) setCache(key string, data any, ttl time.Duration) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) +func (c *Client) setCache(parentCtx context.Context, key string, data any, ttl time.Duration) { + ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second) defer cancel() bytes, err := json.Marshal(data) @@ -72,12 +81,19 @@ func (c *Client) setCache(key string, data any, ttl time.Duration) { }) } -func (c *Client) fetchWithRetry(urlStr string, out any) error { +func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error { maxRetries := 5 for range maxRetries { - c.waitRateLimit() + if err := c.waitRateLimit(ctx); err != nil { + return err + } - resp, err := c.httpClient.Get(urlStr) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil) + if err != nil { + return fmt.Errorf("failed to create jikan request: %w", err) + } + + resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("jikan api error: %w", err) } @@ -86,7 +102,13 @@ func (c *Client) fetchWithRetry(urlStr string, out any) error { resp.Body.Close() // Jikan rate limit is hit (usually the 60 requests/minute limit) // Wait for 2 seconds before retrying to let the bucket refill slightly - time.Sleep(2 * time.Second) + timer := time.NewTimer(2 * time.Second) + select { + case <-timer.C: + case <-ctx.Done(): + timer.Stop() + return fmt.Errorf("request canceled while retrying jikan request: %w", ctx.Err()) + } continue } diff --git a/internal/jikan/recommendations.go b/internal/jikan/recommendations.go index bedfe6a..26004ac 100644 --- a/internal/jikan/recommendations.go +++ b/internal/jikan/recommendations.go @@ -1,6 +1,7 @@ package jikan import ( + "context" "fmt" "time" ) @@ -23,10 +24,10 @@ type RecommendationsResponse struct { Data []RecommendationEntry `json:"data"` } -func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) { +func (c *Client) GetRecommendations(ctx context.Context, animeID int, limit int) ([]Anime, error) { cacheKey := fmt.Sprintf("recs:%d", animeID) var cached []Anime - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { if limit > 0 && len(cached) > limit { return cached[:limit], nil } @@ -35,7 +36,7 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) { var result RecommendationsResponse reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return nil, err } @@ -53,7 +54,7 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) { var fullAnime Anime animeCacheKey := fmt.Sprintf("anime:%d", rec.Entry.MalID) - if c.getCache(animeCacheKey, &fullAnime) { + if c.getCache(ctx, animeCacheKey, &fullAnime) { animes = append(animes, fullAnime) } else { // Otherwise, map the basic recommendation data directly into an Anime struct. @@ -79,6 +80,6 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) { } } - c.setCache(cacheKey, animes, time.Hour*24) + c.setCache(ctx, cacheKey, animes, time.Hour*24) return animes, nil } diff --git a/internal/jikan/relations.go b/internal/jikan/relations.go index b86f2a9..c2038f5 100644 --- a/internal/jikan/relations.go +++ b/internal/jikan/relations.go @@ -1,6 +1,9 @@ package jikan -import "maps" +import ( + "context" + "maps" +) func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int { for _, group := range groups { @@ -16,8 +19,8 @@ func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int { return nil } -func (c *Client) fetchChain(startID int, direction string, visited map[int]bool) ([]RelationEntry, error) { - anime, err := c.GetAnimeByID(startID) +func (c *Client) fetchChain(ctx context.Context, startID int, direction string, visited map[int]bool) ([]RelationEntry, error) { + anime, err := c.GetAnimeByID(ctx, startID) if err != nil { return nil, err } @@ -33,13 +36,13 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool) } visited[nextID] = true - nextAnime, err := c.GetAnimeByID(nextID) + nextAnime, err := c.GetAnimeByID(ctx, nextID) if err != nil { return nil, err } entry := RelationEntry{Anime: nextAnime, IsCurrent: false} - rest, err := c.fetchChain(nextID, direction, visited) + rest, err := c.fetchChain(ctx, nextID, direction, visited) if err != nil { return nil, err } @@ -50,20 +53,20 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool) return append([]RelationEntry{entry}, rest...), nil } -func (c *Client) GetFullRelations(id int) ([]RelationEntry, error) { - currentAnime, err := c.GetAnimeByID(id) +func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry, error) { + currentAnime, err := c.GetAnimeByID(ctx, id) if err != nil { return nil, err } visited := map[int]bool{id: true} - prequels, err1 := c.fetchChain(id, "Prequel", visited) + prequels, err1 := c.fetchChain(ctx, id, "Prequel", visited) visitedSeq := make(map[int]bool) maps.Copy(visitedSeq, visited) - sequels, err2 := c.fetchChain(id, "Sequel", visitedSeq) + sequels, err2 := c.fetchChain(ctx, id, "Sequel", visitedSeq) var result []RelationEntry result = append(result, prequels...) diff --git a/internal/jikan/search.go b/internal/jikan/search.go index 5e7e9c9..74a43fe 100644 --- a/internal/jikan/search.go +++ b/internal/jikan/search.go @@ -1,12 +1,13 @@ package jikan import ( + "context" "fmt" "net/url" "time" ) -func (c *Client) Search(query string, page int) (SearchResult, error) { +func (c *Client) Search(ctx context.Context, query string, page int) (SearchResult, error) { if query == "" { return SearchResult{}, nil } @@ -16,14 +17,14 @@ func (c *Client) Search(query string, page int) (SearchResult, error) { cacheKey := fmt.Sprintf("search:limit24:%s:%d", query, page) var cached SearchResult - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result SearchResponse reqURL := fmt.Sprintf("%s/anime?q=%s&limit=24&page=%d", c.baseURL, url.QueryEscape(query), page) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return SearchResult{}, err } @@ -32,24 +33,24 @@ func (c *Client) Search(query string, page int) (SearchResult, error) { HasNextPage: result.Pagination.HasNextPage, } - c.setCache(cacheKey, res, time.Hour*1) + c.setCache(ctx, cacheKey, res, time.Hour*1) return res, nil } -func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) { +func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, error) { if page < 1 { page = 1 } cacheKey := fmt.Sprintf("top:limit24:%d", page) var cached TopAnimeResult - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result TopAnimeResponse reqURL := fmt.Sprintf("%s/top/anime?filter=bypopularity&limit=24&page=%d", c.baseURL, page) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return TopAnimeResult{}, err } @@ -58,6 +59,6 @@ func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) { HasNextPage: result.Pagination.HasNextPage, } - c.setCache(cacheKey, res, time.Hour*1) + c.setCache(ctx, cacheKey, res, time.Hour*1) return res, nil } diff --git a/internal/jikan/seasons.go b/internal/jikan/seasons.go index 818ec27..dcaf75e 100644 --- a/internal/jikan/seasons.go +++ b/internal/jikan/seasons.go @@ -1,6 +1,7 @@ package jikan import ( + "context" "fmt" "strings" "time" @@ -11,18 +12,18 @@ type ScheduleResult struct { HasNextPage bool } -func (c *Client) GetSchedule(day string) (ScheduleResult, error) { +func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, error) { day = strings.ToLower(day) cacheKey := fmt.Sprintf("schedule_limit24_%s", day) var cached ScheduleResult - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result TopAnimeResponse reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true&limit=24", c.baseURL, day) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return ScheduleResult{}, err } @@ -31,16 +32,16 @@ func (c *Client) GetSchedule(day string) (ScheduleResult, error) { HasNextPage: result.Pagination.HasNextPage, } - c.setCache(cacheKey, res, time.Hour*1) + c.setCache(ctx, cacheKey, res, time.Hour*1) return res, nil } -func (c *Client) GetFullSchedule() (map[string][]Anime, error) { +func (c *Client) GetFullSchedule(ctx context.Context) (map[string][]Anime, error) { days := []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} schedule := make(map[string][]Anime) for _, day := range days { - res, err := c.GetSchedule(day) + res, err := c.GetSchedule(ctx, day) if err != nil { return nil, fmt.Errorf("failed to fetch %s schedule: %w", day, err) } @@ -50,19 +51,19 @@ func (c *Client) GetFullSchedule() (map[string][]Anime, error) { return schedule, nil } -func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) { +func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, error) { if page < 1 { page = 1 } cacheKey := fmt.Sprintf("seasons_now_limit24:%d", page) var cached TopAnimeResult - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result TopAnimeResponse reqURL := fmt.Sprintf("%s/seasons/now?limit=24&page=%d", c.baseURL, page) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return TopAnimeResult{}, err } @@ -71,23 +72,23 @@ func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) { HasNextPage: result.Pagination.HasNextPage, } - c.setCache(cacheKey, res, time.Hour*1) + c.setCache(ctx, cacheKey, res, time.Hour*1) return res, nil } -func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) { +func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) { if page < 1 { page = 1 } cacheKey := fmt.Sprintf("seasons_upcoming_limit24:%d", page) var cached TopAnimeResult - if c.getCache(cacheKey, &cached) { + if c.getCache(ctx, cacheKey, &cached) { return cached, nil } var result TopAnimeResponse reqURL := fmt.Sprintf("%s/seasons/upcoming?limit=24&page=%d", c.baseURL, page) - if err := c.fetchWithRetry(reqURL, &result); err != nil { + if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil { return TopAnimeResult{}, err } @@ -96,6 +97,6 @@ func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) { HasNextPage: result.Pagination.HasNextPage, } - c.setCache(cacheKey, res, time.Hour*1) + c.setCache(ctx, cacheKey, res, time.Hour*1) return res, nil } diff --git a/internal/worker/relations.go b/internal/worker/relations.go index 235d990..5741da4 100644 --- a/internal/worker/relations.go +++ b/internal/worker/relations.go @@ -71,7 +71,7 @@ func (w *Worker) syncRelations(ctx context.Context) { for _, a := range animes { func() { - animeData, err := w.client.GetAnimeByID(int(a.ID)) + animeData, err := w.client.GetAnimeByID(ctx, int(a.ID)) if err != nil { log.Printf("worker: failed to fetch anime details for %d: %v", a.ID, err) // Sleep a bit on error to respect rate limits, but DO NOT mark as synced @@ -111,7 +111,7 @@ func (w *Worker) syncRelations(ctx context.Context) { } // Also update the status of the anime itself so we know if it's Not yet aired, etc. - animeDetails, err := w.client.GetAnimeByID(int(a.ID)) + animeDetails, err := w.client.GetAnimeByID(ctx, int(a.ID)) if err == nil { err = w.db.UpdateAnimeStatus(ctx, database.UpdateAnimeStatusParams{ Status: sql.NullString{String: animeDetails.Status, Valid: true}, @@ -130,7 +130,7 @@ func (w *Worker) ensureAnimeExistsAndStatusUpdated(ctx context.Context, malID in _, err := w.db.GetAnime(ctx, int64(malID)) if err != nil { // we don't have it, let's fetch it - animeDetails, err := w.client.GetAnimeByID(malID) + animeDetails, err := w.client.GetAnimeByID(ctx, malID) if err != nil { log.Printf("worker: failed to fetch related anime %d: %v", malID, err) return @@ -163,7 +163,7 @@ func (w *Worker) ensureAnimeExistsAndStatusUpdated(ctx context.Context, malID in // but since it's a Sequel to something they watched, we could fetch it. // For now, let's just let the worker naturally pick it up if it gets added to watchlist, // OR we can explicitly fetch its details to keep sequels up to date. - animeDetails, err := w.client.GetAnimeByID(malID) + animeDetails, err := w.client.GetAnimeByID(ctx, malID) if err == nil { w.db.UpdateAnimeStatus(ctx, database.UpdateAnimeStatusParams{ Status: sql.NullString{String: animeDetails.Status, Valid: true},