feat: use sqlite for jikan api cache with hourly cleanup
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
package jikan
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetAnimeByID fetches full details for a single anime
|
||||
func (c *Client) GetAnimeByID(id int) (Anime, error) {
|
||||
if cached, ok := c.animeCache.Get(id); ok {
|
||||
cacheKey := fmt.Sprintf("anime:%d", id)
|
||||
var cached Anime
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +19,6 @@ func (c *Client) GetAnimeByID(id int) (Anime, error) {
|
||||
return Anime{}, err
|
||||
}
|
||||
|
||||
c.animeCache.Add(id, result.Data)
|
||||
c.setCache(cacheKey, result.Data, time.Hour*24)
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
@@ -1,51 +1,58 @@
|
||||
package jikan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"mal/internal/database"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
cache *expirable.LRU[string, SearchResult]
|
||||
topCache *expirable.LRU[int, TopAnimeResult]
|
||||
airingCache *expirable.LRU[int, TopAnimeResult]
|
||||
upcomingCache *expirable.LRU[int, TopAnimeResult]
|
||||
animeCache *expirable.LRU[int, Anime]
|
||||
relationsCache *expirable.LRU[int, JikanRelationsResponse]
|
||||
scheduleCache *expirable.LRU[string, ScheduleResult]
|
||||
recsCache *expirable.LRU[int, []Anime]
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
db database.Querier
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
cache := expirable.NewLRU[string, SearchResult](500, nil, time.Hour*1)
|
||||
topCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
|
||||
airingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
|
||||
upcomingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
|
||||
animeCache := expirable.NewLRU[int, Anime](1000, nil, time.Hour*24)
|
||||
relationsCache := expirable.NewLRU[int, JikanRelationsResponse](1000, nil, time.Hour*24)
|
||||
scheduleCache := expirable.NewLRU[string, ScheduleResult](50, nil, time.Hour*1)
|
||||
recsCache := expirable.NewLRU[int, []Anime](500, nil, time.Hour*24)
|
||||
|
||||
func NewClient(db database.Querier) *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
baseURL: "https://api.jikan.moe/v4",
|
||||
cache: cache,
|
||||
topCache: topCache,
|
||||
airingCache: airingCache,
|
||||
upcomingCache: upcomingCache,
|
||||
animeCache: animeCache,
|
||||
relationsCache: relationsCache,
|
||||
scheduleCache: scheduleCache,
|
||||
recsCache: recsCache,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
baseURL: "https://api.jikan.moe/v4",
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getCache(key string, out interface{}) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data, err := c.db.GetJikanCache(ctx, key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(data), out)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *Client) setCache(key string, data interface{}, ttl time.Duration) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.db.SetJikanCache(ctx, database.SetJikanCacheParams{
|
||||
Key: key,
|
||||
Data: string(bytes),
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
})
|
||||
}
|
||||
|
||||
// fetchWithRetry provides robust fetching respecting Jikan's strict 3 req/sec rate limit
|
||||
func (c *Client) fetchWithRetry(urlStr string, out interface{}) error {
|
||||
maxRetries := 3
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package jikan
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RecommendationEntry represents a single recommendation
|
||||
type RecommendationEntry struct {
|
||||
@@ -23,7 +26,9 @@ type RecommendationsResponse struct {
|
||||
|
||||
// GetRecommendations fetches full details for the top recommended anime
|
||||
func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
if cached, ok := c.recsCache.Get(animeID); ok {
|
||||
cacheKey := fmt.Sprintf("recs:%d", animeID)
|
||||
var cached []Anime
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if len(cached) > limit {
|
||||
return cached[:limit], nil
|
||||
}
|
||||
@@ -71,6 +76,6 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.recsCache.Add(animeID, animes)
|
||||
c.setCache(cacheKey, animes, time.Hour*24)
|
||||
return animes, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package jikan
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetRelationsData fetches the raw relationships for an anime
|
||||
func (c *Client) GetRelationsData(id int) (JikanRelationsResponse, error) {
|
||||
if cached, ok := c.relationsCache.Get(id); ok {
|
||||
cacheKey := fmt.Sprintf("relations:%d", id)
|
||||
var cached JikanRelationsResponse
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -14,7 +19,7 @@ func (c *Client) GetRelationsData(id int) (JikanRelationsResponse, error) {
|
||||
return JikanRelationsResponse{}, err
|
||||
}
|
||||
|
||||
c.relationsCache.Add(id, result)
|
||||
c.setCache(cacheKey, result, time.Hour*24)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package jikan
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Search returns the anime list with pagination support
|
||||
@@ -15,7 +16,8 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("search:%s:%d", query, page)
|
||||
if cached, ok := c.cache.Get(cacheKey); ok {
|
||||
var cached SearchResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -30,7 +32,7 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.cache.Add(cacheKey, res)
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -39,7 +41,9 @@ func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if cached, ok := c.topCache.Get(page); ok {
|
||||
cacheKey := fmt.Sprintf("top:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -54,6 +58,6 @@ func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.topCache.Add(page, res)
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package jikan
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScheduleResult contains anime grouped by day
|
||||
@@ -17,7 +18,8 @@ func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
|
||||
day = strings.ToLower(day)
|
||||
cacheKey := fmt.Sprintf("schedule_%s", day)
|
||||
|
||||
if cached, ok := c.scheduleCache.Get(cacheKey); ok {
|
||||
var cached ScheduleResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.scheduleCache.Add(cacheKey, res)
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -57,7 +59,9 @@ func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if cached, ok := c.airingCache.Get(page); ok {
|
||||
cacheKey := fmt.Sprintf("seasons_now:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -72,7 +76,7 @@ func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.airingCache.Add(page, res)
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -81,7 +85,9 @@ func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if cached, ok := c.upcomingCache.Get(page); ok {
|
||||
cacheKey := fmt.Sprintf("seasons_upcoming:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
@@ -96,6 +102,6 @@ func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.upcomingCache.Add(page, res)
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user