Files
mal/integrations/jikan/seasons.go

238 lines
5.4 KiB
Go

package jikan
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"strconv"
"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
params := url.Values{}
params.Set("page", strconv.Itoa(page))
reqURL := buildRequestURL(c.baseURL, fmt.Sprintf("/seasons/%s", season), params)
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) {
if !c.markRandomPoolInitialized() {
return
}
c.loadCachedRandomPool(ctx)
// Fetch a solid baseline in the background, then start refreshing.
go c.seedRandomPoolBaseline()
}
func (c *Client) markRandomPoolInitialized() bool {
c.poolMu.Lock()
defer c.poolMu.Unlock()
if c.poolInitialized {
return false
}
c.poolInitialized = true
return true
}
func (c *Client) loadCachedRandomPool(ctx context.Context) {
cachedJSONs, err := c.db.GetAllCachedAnime(ctx)
if err != nil || len(cachedJSONs) == 0 {
return
}
loadedAnimes := decodeCachedAnime(cachedJSONs)
if len(loadedAnimes) == 0 {
return
}
c.poolMu.Lock()
c.randomPool = append(c.randomPool, loadedAnimes...)
c.poolMu.Unlock()
}
func decodeCachedAnime(cachedJSONs []string) []Anime {
loadedAnimes := make([]Anime, 0, len(cachedJSONs))
for _, dataStr := range cachedJSONs {
var anime Anime
if err := json.Unmarshal([]byte(dataStr), &anime); err != nil || anime.MalID == 0 {
continue
}
loadedAnimes = append(loadedAnimes, anime)
}
return loadedAnimes
}
func (c *Client) seedRandomPoolBaseline() {
bgCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
fetchedAnimes := c.fetchBaselineAnime(bgCtx)
if len(fetchedAnimes) > 0 {
c.appendUniqueRandomPool(fetchedAnimes)
}
// Start background refresher once seeding completes
c.startPoolRefresher()
}
func (c *Client) fetchBaselineAnime(ctx context.Context) []Anime {
topPageOne := c.fetchTopAnimePage(ctx, 1)
topPageTwo := c.fetchTopAnimePage(ctx, 2)
currentSeason := c.fetchCurrentSeasonAnime(ctx)
fetchedAnimes := make([]Anime, 0, len(topPageOne)+len(topPageTwo)+len(currentSeason))
fetchedAnimes = append(fetchedAnimes, topPageOne...)
fetchedAnimes = append(fetchedAnimes, topPageTwo...)
fetchedAnimes = append(fetchedAnimes, currentSeason...)
return fetchedAnimes
}
func (c *Client) fetchTopAnimePage(ctx context.Context, page int) []Anime {
top, err := c.GetTopAnime(ctx, page)
if err != nil {
return nil
}
return top.Animes
}
func (c *Client) fetchCurrentSeasonAnime(ctx context.Context) []Anime {
now, err := c.GetSeasonsNow(ctx, 1)
if err != nil {
return nil
}
return now.Animes
}
func (c *Client) appendUniqueRandomPool(animes []Anime) {
c.poolMu.Lock()
defer c.poolMu.Unlock()
seen := make(map[int]bool, len(c.randomPool)+len(animes))
for _, anime := range c.randomPool {
seen[anime.MalID] = true
}
for _, anime := range animes {
if seen[anime.MalID] {
continue
}
c.randomPool = append(c.randomPool, anime)
seen[anime.MalID] = true
}
}
// 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
}