api: pass request context to jikan

This commit is contained in:
2026-04-10 17:26:28 +02:00
parent ecb15782c8
commit 2b8332a16d
9 changed files with 134 additions and 112 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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...)

View File

@@ -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
}

View File

@@ -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
}