refactor(jikan): add getWithCache helper and reduce duplication
This commit is contained in:
@@ -8,21 +8,20 @@ import (
|
|||||||
|
|
||||||
func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) {
|
func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) {
|
||||||
cacheKey := fmt.Sprintf("anime:%d", id)
|
cacheKey := fmt.Sprintf("anime:%d", id)
|
||||||
|
|
||||||
var cached Anime
|
var cached Anime
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
if c.getCache(ctx, cacheKey, &cached) {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale Anime
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result AnimeResponse
|
var result AnimeResponse
|
||||||
reqURL := fmt.Sprintf("%s/anime/%d/full", c.baseURL, id)
|
reqURL := fmt.Sprintf("%s/anime/%d/full", c.baseURL, id)
|
||||||
|
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||||
if hasStale {
|
var stale Anime
|
||||||
|
if c.getStaleCache(ctx, cacheKey, &stale) {
|
||||||
return stale, nil
|
return stale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return Anime{}, err
|
return Anime{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,4 +32,4 @@ func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) {
|
|||||||
|
|
||||||
c.setCache(ctx, cacheKey, result.Data, ttl)
|
c.setCache(ctx, cacheKey, result.Data, ttl)
|
||||||
return result.Data, nil
|
return result.Data, nil
|
||||||
}
|
}
|
||||||
@@ -226,6 +226,32 @@ func (c *Client) setCache(parentCtx context.Context, key string, data any, ttl t
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cacheResult struct {
|
||||||
|
data any
|
||||||
|
hasStale bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Duration, url string, out any) error {
|
||||||
|
if c.getCache(ctx, cacheKey, out) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stale any
|
||||||
|
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||||
|
|
||||||
|
if err := c.fetchWithRetry(ctx, url, out); err != nil {
|
||||||
|
if hasStale {
|
||||||
|
staleBytes, _ := json.Marshal(stale)
|
||||||
|
json.Unmarshal(staleBytes, out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setCache(ctx, cacheKey, out, ttl)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error {
|
func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error {
|
||||||
maxRetries := 5
|
maxRetries := 5
|
||||||
for attempt := range maxRetries {
|
for attempt := range maxRetries {
|
||||||
|
|||||||
@@ -12,24 +12,9 @@ func (c *Client) GetEpisodes(ctx context.Context, animeID int, page int) (Episod
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("anime:%d:episodes:%d", animeID, page)
|
cacheKey := fmt.Sprintf("anime:%d:episodes:%d", animeID, page)
|
||||||
var cached EpisodesResponse
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale EpisodesResponse
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result EpisodesResponse
|
var result EpisodesResponse
|
||||||
reqURL := fmt.Sprintf("%s/anime/%d/episodes?page=%d", c.baseURL, animeID, page)
|
reqURL := fmt.Sprintf("%s/anime/%d/episodes?page=%d", c.baseURL, animeID, page)
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
|
||||||
if hasStale {
|
|
||||||
return stale, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return EpisodesResponse{}, err
|
err := c.getWithCache(ctx, cacheKey, 12*time.Hour, reqURL, &result)
|
||||||
}
|
return result, err
|
||||||
|
}
|
||||||
c.setCache(ctx, cacheKey, result, 12*time.Hour)
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ type RecommendationsResponse struct {
|
|||||||
|
|
||||||
func (c *Client) GetRecommendations(ctx context.Context, 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)
|
cacheKey := fmt.Sprintf("recs:%d", animeID)
|
||||||
|
|
||||||
var cached []Anime
|
var cached []Anime
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
if c.getCache(ctx, cacheKey, &cached) {
|
||||||
if limit > 0 && len(cached) > limit {
|
if limit > 0 && len(cached) > limit {
|
||||||
@@ -34,19 +35,17 @@ func (c *Client) GetRecommendations(ctx context.Context, animeID int, limit int)
|
|||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale []Anime
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result RecommendationsResponse
|
var result RecommendationsResponse
|
||||||
reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID)
|
reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID)
|
||||||
|
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||||
if hasStale {
|
var stale []Anime
|
||||||
|
if c.getStaleCache(ctx, cacheKey, &stale) {
|
||||||
if limit > 0 && len(stale) > limit {
|
if limit > 0 && len(stale) > limit {
|
||||||
return stale[:limit], nil
|
return stale[:limit], nil
|
||||||
}
|
}
|
||||||
return stale, nil
|
return stale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +58,12 @@ func (c *Client) GetRecommendations(ctx context.Context, animeID int, limit int)
|
|||||||
for i := 0; i < max; i++ {
|
for i := 0; i < max; i++ {
|
||||||
rec := result.Data[i]
|
rec := result.Data[i]
|
||||||
|
|
||||||
// Try to see if we already have the full anime details in our local cache.
|
|
||||||
// If we do, we can use it to get the English title without making an API call!
|
|
||||||
var fullAnime Anime
|
var fullAnime Anime
|
||||||
animeCacheKey := fmt.Sprintf("anime:%d", rec.Entry.MalID)
|
animeCacheKey := fmt.Sprintf("anime:%d", rec.Entry.MalID)
|
||||||
|
|
||||||
if c.getCache(ctx, animeCacheKey, &fullAnime) {
|
if c.getCache(ctx, animeCacheKey, &fullAnime) {
|
||||||
animes = append(animes, fullAnime)
|
animes = append(animes, fullAnime)
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, map the basic recommendation data directly into an Anime struct.
|
|
||||||
anime := Anime{
|
anime := Anime{
|
||||||
MalID: rec.Entry.MalID,
|
MalID: rec.Entry.MalID,
|
||||||
Title: rec.Entry.Title,
|
Title: rec.Entry.Title,
|
||||||
@@ -92,4 +88,4 @@ func (c *Client) GetRecommendations(ctx context.Context, animeID int, limit int)
|
|||||||
|
|
||||||
c.setCache(ctx, cacheKey, animes, time.Hour*24)
|
c.setCache(ctx, cacheKey, animes, time.Hour*24)
|
||||||
return animes, nil
|
return animes, nil
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,6 @@ func (c *Client) search(ctx context.Context, query string, page int, limit int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("search:%s:%d:%d", query, page, limit)
|
cacheKey := fmt.Sprintf("search:%s:%d:%d", query, page, limit)
|
||||||
var cached SearchResult
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale SearchResult
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result SearchResponse
|
var result SearchResponse
|
||||||
reqURL := fmt.Sprintf("%s/anime?q=%s&page=%d", c.baseURL, url.QueryEscape(query), page)
|
reqURL := fmt.Sprintf("%s/anime?q=%s&page=%d", c.baseURL, url.QueryEscape(query), page)
|
||||||
@@ -40,7 +33,7 @@ func (c *Client) search(ctx context.Context, query string, page int, limit int)
|
|||||||
reqURL = fmt.Sprintf("%s&limit=%d", reqURL, limit)
|
reqURL = fmt.Sprintf("%s&limit=%d", reqURL, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
if err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result); err != nil {
|
||||||
if limit > 0 && IsRetryableError(err) {
|
if limit > 0 && IsRetryableError(err) {
|
||||||
fallbackURL := fmt.Sprintf("%s/anime?q=%s&page=%d", c.baseURL, url.QueryEscape(query), page)
|
fallbackURL := fmt.Sprintf("%s/anime?q=%s&page=%d", c.baseURL, url.QueryEscape(query), page)
|
||||||
if fallbackErr := c.fetchWithRetry(ctx, fallbackURL, &result); fallbackErr == nil {
|
if fallbackErr := c.fetchWithRetry(ctx, fallbackURL, &result); fallbackErr == nil {
|
||||||
@@ -53,20 +46,18 @@ func (c *Client) search(ctx context.Context, query string, page int, limit int)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStale {
|
var stale SearchResult
|
||||||
|
if c.getStaleCache(ctx, cacheKey, &stale) {
|
||||||
return stale, nil
|
return stale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return SearchResult{}, err
|
return SearchResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := SearchResult{
|
return SearchResult{
|
||||||
Animes: result.Data,
|
Animes: result.Data,
|
||||||
HasNextPage: result.Pagination.HasNextPage,
|
HasNextPage: result.Pagination.HasNextPage,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, error) {
|
func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||||
@@ -74,30 +65,20 @@ func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, err
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
cacheKey := fmt.Sprintf("top:%d", page)
|
cacheKey := fmt.Sprintf("top:%d", page)
|
||||||
var cached TopAnimeResult
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale TopAnimeResult
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result TopAnimeResponse
|
var result TopAnimeResponse
|
||||||
reqURL := fmt.Sprintf("%s/top/anime?page=%d", c.baseURL, page)
|
reqURL := fmt.Sprintf("%s/top/anime?page=%d", c.baseURL, page)
|
||||||
|
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
if err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result); err != nil {
|
||||||
if hasStale {
|
var stale TopAnimeResult
|
||||||
|
if c.getStaleCache(ctx, cacheKey, &stale) {
|
||||||
return stale, nil
|
return stale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return TopAnimeResult{}, err
|
return TopAnimeResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := TopAnimeResult{
|
return TopAnimeResult{
|
||||||
Animes: result.Data,
|
Animes: result.Data,
|
||||||
HasNextPage: result.Pagination.HasNextPage,
|
HasNextPage: result.Pagination.HasNextPage,
|
||||||
}
|
}, nil
|
||||||
|
}
|
||||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -15,31 +15,18 @@ func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, e
|
|||||||
day = strings.ToLower(day)
|
day = strings.ToLower(day)
|
||||||
cacheKey := fmt.Sprintf("schedule_%s", day)
|
cacheKey := fmt.Sprintf("schedule_%s", day)
|
||||||
|
|
||||||
var cached ScheduleResult
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale ScheduleResult
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result TopAnimeResponse
|
var result TopAnimeResponse
|
||||||
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true", c.baseURL, day)
|
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true", c.baseURL, day)
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
|
||||||
if hasStale {
|
|
||||||
return stale, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
|
||||||
|
if err != nil {
|
||||||
return ScheduleResult{}, err
|
return ScheduleResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := ScheduleResult{
|
return ScheduleResult{
|
||||||
Animes: result.Data,
|
Animes: result.Data,
|
||||||
HasNextPage: result.Pagination.HasNextPage,
|
HasNextPage: result.Pagination.HasNextPage,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetFullSchedule(ctx context.Context) (map[string][]Anime, error) {
|
func (c *Client) GetFullSchedule(ctx context.Context) (map[string][]Anime, error) {
|
||||||
@@ -62,31 +49,19 @@ func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, e
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
cacheKey := fmt.Sprintf("seasons_now:%d", page)
|
cacheKey := fmt.Sprintf("seasons_now:%d", page)
|
||||||
var cached TopAnimeResult
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale TopAnimeResult
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result TopAnimeResponse
|
var result TopAnimeResponse
|
||||||
reqURL := fmt.Sprintf("%s/seasons/now?page=%d", c.baseURL, page)
|
reqURL := fmt.Sprintf("%s/seasons/now?page=%d", c.baseURL, page)
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
|
||||||
if hasStale {
|
|
||||||
return stale, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
|
||||||
|
if err != nil {
|
||||||
return TopAnimeResult{}, err
|
return TopAnimeResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := TopAnimeResult{
|
return TopAnimeResult{
|
||||||
Animes: result.Data,
|
Animes: result.Data,
|
||||||
HasNextPage: result.Pagination.HasNextPage,
|
HasNextPage: result.Pagination.HasNextPage,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
|
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||||
@@ -94,29 +69,17 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
cacheKey := fmt.Sprintf("seasons_upcoming:%d", page)
|
cacheKey := fmt.Sprintf("seasons_upcoming:%d", page)
|
||||||
var cached TopAnimeResult
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale TopAnimeResult
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result TopAnimeResponse
|
var result TopAnimeResponse
|
||||||
reqURL := fmt.Sprintf("%s/seasons/upcoming?page=%d", c.baseURL, page)
|
reqURL := fmt.Sprintf("%s/seasons/upcoming?page=%d", c.baseURL, page)
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
|
||||||
if hasStale {
|
|
||||||
return stale, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
|
||||||
|
if err != nil {
|
||||||
return TopAnimeResult{}, err
|
return TopAnimeResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := TopAnimeResult{
|
return TopAnimeResult{
|
||||||
Animes: result.Data,
|
Animes: result.Data,
|
||||||
HasNextPage: result.Pagination.HasNextPage,
|
HasNextPage: result.Pagination.HasNextPage,
|
||||||
}
|
}, nil
|
||||||
|
}
|
||||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,7 @@ func (c *Client) GetAnimeByProducer(ctx context.Context, producerID int, page in
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("producer:%d:%d", producerID, page)
|
cacheKey := fmt.Sprintf("producer:%d:%d", producerID, page)
|
||||||
|
|
||||||
var cached StudioAnimeResult
|
var cached StudioAnimeResult
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
if c.getCache(ctx, cacheKey, &cached) {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
@@ -49,11 +50,9 @@ func (c *Client) GetAnimeByProducer(ctx context.Context, producerID int, page in
|
|||||||
if hasStale {
|
if hasStale {
|
||||||
return stale, nil
|
return stale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return StudioAnimeResult{}, err
|
return StudioAnimeResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get producer info for the name
|
|
||||||
producerName := ""
|
producerName := ""
|
||||||
var producerRes ProducerResponse
|
var producerRes ProducerResponse
|
||||||
producerURL := fmt.Sprintf("%s/producers/%d", c.baseURL, producerID)
|
producerURL := fmt.Sprintf("%s/producers/%d", c.baseURL, producerID)
|
||||||
@@ -78,25 +77,10 @@ func (c *Client) GetAnimeByProducer(ctx context.Context, producerID int, page in
|
|||||||
|
|
||||||
func (c *Client) GetProducerByID(ctx context.Context, producerID int) (ProducerResponse, error) {
|
func (c *Client) GetProducerByID(ctx context.Context, producerID int) (ProducerResponse, error) {
|
||||||
cacheKey := fmt.Sprintf("producer:info:%d", producerID)
|
cacheKey := fmt.Sprintf("producer:info:%d", producerID)
|
||||||
var cached ProducerResponse
|
|
||||||
if c.getCache(ctx, cacheKey, &cached) {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale ProducerResponse
|
|
||||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
|
||||||
|
|
||||||
var result ProducerResponse
|
var result ProducerResponse
|
||||||
reqURL := fmt.Sprintf("%s/producers/%d/full", c.baseURL, producerID)
|
reqURL := fmt.Sprintf("%s/producers/%d/full", c.baseURL, producerID)
|
||||||
|
|
||||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
|
||||||
if hasStale {
|
return result, err
|
||||||
return stale, nil
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ProducerResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.setCache(ctx, cacheKey, result, shortCacheTTL)
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user