YOLO
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user