191 lines
4.5 KiB
Go
191 lines
4.5 KiB
Go
package jikan
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
type ScheduleResult struct {
|
|
Animes []Anime
|
|
HasNextPage bool // whether more pages available
|
|
}
|
|
|
|
// GetSeasonsNow returns currently airing anime for the current season.
|
|
func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, error) {
|
|
return c.getSeasonList(ctx, page, "now")
|
|
}
|
|
|
|
// GetSeasonsUpcoming returns anime scheduled to air in upcoming seasons.
|
|
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
|
|
return c.getSeasonList(ctx, page, "upcoming")
|
|
}
|
|
|
|
func (c *Client) getSeasonList(ctx context.Context, page int, season string) (TopAnimeResult, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
cacheKey := fmt.Sprintf("seasons_%s:%d", season, page)
|
|
|
|
var result TopAnimeResponse
|
|
reqURL := fmt.Sprintf("%s/seasons/%s?page=%d", c.baseURL, season, page)
|
|
|
|
err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
|
|
if err != nil {
|
|
return TopAnimeResult{}, err
|
|
}
|
|
|
|
return TopAnimeResult{
|
|
Animes: result.Data,
|
|
HasNextPage: result.Pagination.HasNextPage,
|
|
}, 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) {
|
|
c.poolMu.Lock()
|
|
initialized := c.poolInitialized
|
|
c.poolMu.Unlock()
|
|
|
|
if !initialized {
|
|
_ = c.seedRandomPool(ctx)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
idx := rand.Intn(len(c.randomPool))
|
|
return c.randomPool[idx], nil
|
|
}
|