This commit is contained in:
2026-05-20 16:47:39 +02:00
committed by Mikkel Elvers
parent 68396c591e
commit d94f1516ce
7 changed files with 196 additions and 14 deletions

View File

@@ -29,6 +29,11 @@ type Client struct {
lastReqTime time.Time // rate limiting: last request timestamp
sf singleflight.Group
refreshSem chan struct{}
// Random anime pool for DDoS-proof truly random "Surprise Me"
randomPool []Anime
poolMu sync.RWMutex
poolInitialized bool
}
const jikanSlowLogThreshold = 750 * time.Millisecond
@@ -48,6 +53,7 @@ func NewClient(queries *db.Queries) *Client {
db: queries,
retrySignal: make(chan struct{}, 1),
refreshSem: make(chan struct{}, 4),
randomPool: make([]Anime, 0),
}
}

View File

@@ -2,7 +2,10 @@ package jikan
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"time"
)
type ScheduleResult struct {
@@ -52,20 +55,148 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
}, nil
}
// seedRandomPool seeds the in-memory pool of random anime
func (c *Client) seedRandomPool(ctx context.Context) error {
c.poolMu.Lock()
if c.poolInitialized {
c.poolMu.Unlock()
return nil
}
c.poolInitialized = true
c.poolMu.Unlock()
// 1. Try to load all cached anime from the database
cachedJSONs, err := c.db.GetAllCachedAnime(ctx)
if err == nil && len(cachedJSONs) > 0 {
var loadedAnimes []Anime
for _, dataStr := range cachedJSONs {
var anime Anime
if err := json.Unmarshal([]byte(dataStr), &anime); err == nil && anime.MalID > 0 {
loadedAnimes = append(loadedAnimes, anime)
}
}
if len(loadedAnimes) > 0 {
c.poolMu.Lock()
c.randomPool = append(c.randomPool, loadedAnimes...)
c.poolMu.Unlock()
}
}
// 2. Fetch Top Anime page 1 & 2 to ensure we have a robust baseline of high-quality popular anime
go func() {
bgCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var fetchedAnimes []Anime
top, err := c.GetTopAnime(bgCtx, 1)
if err == nil && len(top.Animes) > 0 {
fetchedAnimes = append(fetchedAnimes, top.Animes...)
}
top2, err := c.GetTopAnime(bgCtx, 2)
if err == nil && len(top2.Animes) > 0 {
fetchedAnimes = append(fetchedAnimes, top2.Animes...)
}
now, err := c.GetSeasonsNow(bgCtx, 1)
if err == nil && len(now.Animes) > 0 {
fetchedAnimes = append(fetchedAnimes, now.Animes...)
}
if len(fetchedAnimes) > 0 {
c.poolMu.Lock()
// Use map to de-duplicate any anime
seen := make(map[int]bool)
for _, a := range c.randomPool {
seen[a.MalID] = true
}
for _, a := range fetchedAnimes {
if !seen[a.MalID] {
c.randomPool = append(c.randomPool, a)
seen[a.MalID] = true
}
}
c.poolMu.Unlock()
}
// Start background refresher once seeding completes
c.startPoolRefresher()
}()
return nil
}
// startPoolRefresher runs in the background to slowly mix in true random anime
func (c *Client) startPoolRefresher() {
ticker := time.NewTicker(30 * time.Second)
ctx := context.Background()
for range ticker.C {
var result struct {
Data Anime `json:"data"`
}
reqURL := fmt.Sprintf("%s/random/anime", c.baseURL)
err := c.fetchWithRetry(ctx, reqURL, &result)
if err != nil {
continue
}
if result.Data.MalID == 0 {
continue
}
c.poolMu.Lock()
if len(c.randomPool) >= 1000 {
idx := rand.Intn(len(c.randomPool))
c.randomPool[idx] = result.Data
} else {
duplicate := false
for _, a := range c.randomPool {
if a.MalID == result.Data.MalID {
duplicate = true
break
}
}
if !duplicate {
c.randomPool = append(c.randomPool, result.Data)
}
}
c.poolMu.Unlock()
}
}
// GetRandomAnime returns a random anime from the database.
func (c *Client) GetRandomAnime(ctx context.Context) (Anime, error) {
var result struct {
Data Anime `json:"data"`
c.poolMu.Lock()
initialized := c.poolInitialized
c.poolMu.Unlock()
if !initialized {
_ = c.seedRandomPool(ctx)
}
reqURL := fmt.Sprintf("%s/random/anime", c.baseURL)
err := c.fetchWithRetry(ctx, reqURL, &result)
if err != nil {
return Anime{}, err
}
if result.Data.MalID == 0 {
return Anime{}, fmt.Errorf("jikan: empty response for random/anime")
c.poolMu.RLock()
defer c.poolMu.RUnlock()
if len(c.randomPool) == 0 {
var result struct {
Data Anime `json:"data"`
}
reqURL := fmt.Sprintf("%s/random/anime", c.baseURL)
err := c.fetchWithRetry(ctx, reqURL, &result)
if err != nil {
return Anime{}, err
}
if result.Data.MalID == 0 {
return Anime{}, fmt.Errorf("jikan: empty response for random/anime")
}
return result.Data, nil
}
return result.Data, nil
idx := rand.Intn(len(c.randomPool))
return c.randomPool[idx], nil
}