feat: use sqlite for jikan api cache with hourly cleanup

This commit is contained in:
2026-04-08 16:19:59 +02:00
parent 13b0128c38
commit d25426eda9
13 changed files with 172 additions and 51 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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