feat: add schedule, notifications, and recommendations

This commit is contained in:
2026-04-08 13:02:17 +02:00
parent d035a6406b
commit 6f54ed16eb
21 changed files with 1884 additions and 329 deletions

View File

@@ -18,6 +18,8 @@ type Client struct {
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]
}
func NewClient() *Client {
@@ -27,6 +29,8 @@ func NewClient() *Client {
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)
return &Client{
httpClient: &http.Client{Timeout: 10 * time.Second},
@@ -37,6 +41,8 @@ func NewClient() *Client {
upcomingCache: upcomingCache,
animeCache: animeCache,
relationsCache: relationsCache,
scheduleCache: scheduleCache,
recsCache: recsCache,
}
}

View File

@@ -0,0 +1,61 @@
package jikan
import "fmt"
// RecommendationEntry represents a single recommendation
type RecommendationEntry struct {
Entry struct {
MalID int `json:"mal_id"`
URL string `json:"url"`
Images struct {
Webp struct {
LargeImageURL string `json:"large_image_url"`
} `json:"webp"`
} `json:"images"`
Title string `json:"title"`
} `json:"entry"`
Votes int `json:"votes"`
}
type RecommendationsResponse struct {
Data []RecommendationEntry `json:"data"`
}
// GetRecommendations fetches recommendations for an anime
func (c *Client) GetRecommendations(animeID int) ([]Anime, error) {
if cached, ok := c.recsCache.Get(animeID); ok {
return cached, nil
}
var result RecommendationsResponse
reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID)
if err := c.fetchWithRetry(reqURL, &result); err != nil {
return nil, err
}
// Convert to Anime slice (partial data)
animes := make([]Anime, 0, len(result.Data))
for _, rec := range result.Data {
animes = append(animes, Anime{
MalID: rec.Entry.MalID,
Title: rec.Entry.Title,
Images: struct {
Jpg struct {
LargeImageURL string `json:"large_image_url"`
} `json:"jpg"`
Webp struct {
LargeImageURL string `json:"large_image_url"`
} `json:"webp"`
}{
Webp: struct {
LargeImageURL string `json:"large_image_url"`
}{
LargeImageURL: rec.Entry.Images.Webp.LargeImageURL,
},
},
})
}
c.recsCache.Add(animeID, animes)
return animes, nil
}

View File

@@ -1,6 +1,56 @@
package jikan
import "fmt"
import (
"fmt"
"strings"
)
// ScheduleResult contains anime grouped by day
type ScheduleResult struct {
Animes []Anime
HasNextPage bool
}
// GetSchedule fetches anime airing on a specific day
// day can be: monday, tuesday, wednesday, thursday, friday, saturday, sunday, unknown, other
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 {
return cached, nil
}
var result TopAnimeResponse
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true", c.baseURL, day)
if err := c.fetchWithRetry(reqURL, &result); err != nil {
return ScheduleResult{}, err
}
res := ScheduleResult{
Animes: result.Data,
HasNextPage: result.Pagination.HasNextPage,
}
c.scheduleCache.Add(cacheKey, res)
return res, nil
}
// GetFullSchedule fetches all days at once
func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
days := []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
schedule := make(map[string][]Anime)
for _, day := range days {
res, err := c.GetSchedule(day)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s schedule: %w", day, err)
}
schedule[day] = res.Animes
}
return schedule, nil
}
// GetSeasonsNow fetches currently airing anime
func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {