api: pass request context to jikan
This commit is contained in:
@@ -28,6 +28,24 @@ type Handler struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
func parsePageParam(r *http.Request) int {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func userIDFromRequest(r *http.Request) string {
|
||||
user, ok := r.Context().Value(middleware.UserContextKey).(*database.User)
|
||||
if !ok || user == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return user.ID
|
||||
}
|
||||
|
||||
func NewHandler(svc *Service) *Handler {
|
||||
return &Handler{svc: svc}
|
||||
}
|
||||
@@ -50,7 +68,7 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
res, err := h.svc.Search(query, 1)
|
||||
res, err := h.svc.Search(r.Context(), query, 1)
|
||||
if err != nil {
|
||||
log.Printf("search error: %v", err)
|
||||
http.Error(w, "Failed to search anime", http.StatusInternalServerError)
|
||||
@@ -65,13 +83,9 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
page := parsePageParam(r)
|
||||
|
||||
res, err := h.svc.Search(query, page)
|
||||
res, err := h.svc.Search(r.Context(), query, page)
|
||||
if err != nil {
|
||||
log.Printf("search pagination error: %v", err)
|
||||
http.Error(w, "Failed to fetch search page", http.StatusInternalServerError)
|
||||
@@ -84,13 +98,9 @@ func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) {
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
page := parsePageParam(r)
|
||||
|
||||
res, err := h.svc.GetTopAnime(page)
|
||||
res, err := h.svc.GetTopAnime(r.Context(), page)
|
||||
if err != nil {
|
||||
log.Printf("top anime error: %v", err)
|
||||
http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError)
|
||||
@@ -110,10 +120,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
userID := ""
|
||||
if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil {
|
||||
userID = user.ID
|
||||
}
|
||||
userID := userIDFromRequest(r)
|
||||
|
||||
anime, currentStatus, err := h.svc.GetAnimeDetails(r.Context(), id, userID)
|
||||
if err != nil {
|
||||
@@ -141,7 +148,7 @@ func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
relations, err := h.svc.GetRelations(id)
|
||||
relations, err := h.svc.GetRelations(r.Context(), id)
|
||||
if err != nil {
|
||||
log.Printf("failed to get relations for anime %d: %v", id, err)
|
||||
http.Error(w, "Failed to load relations", http.StatusInternalServerError)
|
||||
@@ -169,7 +176,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch parts[1] {
|
||||
case "relations":
|
||||
relations, err := h.svc.GetRelations(id)
|
||||
relations, err := h.svc.GetRelations(r.Context(), id)
|
||||
if err != nil {
|
||||
log.Printf("relations error for %d: %v", id, err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
@@ -178,7 +185,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
templates.AnimeRelationsList(relations).Render(r.Context(), w)
|
||||
case "recommendations":
|
||||
recs, err := h.svc.GetRecommendations(id, 12)
|
||||
recs, err := h.svc.GetRecommendations(r.Context(), id, 12)
|
||||
if err != nil {
|
||||
log.Printf("recommendations error for %d: %v", id, err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
@@ -219,7 +226,7 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.svc.Search(query, 1)
|
||||
res, err := h.svc.Search(r.Context(), query, 1)
|
||||
if err != nil {
|
||||
log.Printf("quick search error: %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -260,13 +267,9 @@ func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) {
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
page := parsePageParam(r)
|
||||
|
||||
res, err := h.svc.GetAiringAnime(page)
|
||||
res, err := h.svc.GetAiringAnime(r.Context(), page)
|
||||
if err != nil {
|
||||
log.Printf("airing anime error: %v", err)
|
||||
http.Error(w, "Failed to fetch airing anime", http.StatusInternalServerError)
|
||||
@@ -279,13 +282,9 @@ func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) {
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
page := parsePageParam(r)
|
||||
|
||||
res, err := h.svc.GetUpcomingAnime(page)
|
||||
res, err := h.svc.GetUpcomingAnime(r.Context(), page)
|
||||
if err != nil {
|
||||
log.Printf("upcoming anime error: %v", err)
|
||||
http.Error(w, "Failed to fetch upcoming anime", http.StatusInternalServerError)
|
||||
@@ -307,7 +306,7 @@ func (h *Handler) HandleAPISchedule(w http.ResponseWriter, r *http.Request) {
|
||||
day = "monday"
|
||||
}
|
||||
|
||||
res, err := h.svc.GetSchedule(day)
|
||||
res, err := h.svc.GetSchedule(r.Context(), day)
|
||||
if err != nil {
|
||||
log.Printf("schedule error for %s: %v", day, err)
|
||||
http.Error(w, "Failed to fetch schedule", http.StatusInternalServerError)
|
||||
@@ -320,10 +319,7 @@ func (h *Handler) HandleAPISchedule(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
userID := ""
|
||||
if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil {
|
||||
userID = user.ID
|
||||
}
|
||||
userID := userIDFromRequest(r)
|
||||
|
||||
if userID == "" {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
@@ -341,10 +337,7 @@ func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) HandleNotificationsUpcoming(w http.ResponseWriter, r *http.Request) {
|
||||
userID := ""
|
||||
if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil {
|
||||
userID = user.ID
|
||||
}
|
||||
userID := userIDFromRequest(r)
|
||||
|
||||
if userID == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
|
||||
@@ -21,24 +21,24 @@ func NewService(jikanClient *jikan.Client, db database.Querier) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Search(query string, page int) (jikan.SearchResult, error) {
|
||||
return s.jikanClient.Search(query, page)
|
||||
func (s *Service) Search(ctx context.Context, query string, page int) (jikan.SearchResult, error) {
|
||||
return s.jikanClient.Search(ctx, query, page)
|
||||
}
|
||||
|
||||
func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetTopAnime(page)
|
||||
func (s *Service) GetTopAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetTopAnime(ctx, page)
|
||||
}
|
||||
|
||||
func (s *Service) GetAiringAnime(page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetSeasonsNow(page)
|
||||
func (s *Service) GetAiringAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetSeasonsNow(ctx, page)
|
||||
}
|
||||
|
||||
func (s *Service) GetUpcomingAnime(page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetSeasonsUpcoming(page)
|
||||
func (s *Service) GetUpcomingAnime(ctx context.Context, page int) (jikan.TopAnimeResult, error) {
|
||||
return s.jikanClient.GetSeasonsUpcoming(ctx, page)
|
||||
}
|
||||
|
||||
func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) {
|
||||
anime, err := s.jikanClient.GetAnimeByID(id)
|
||||
anime, err := s.jikanClient.GetAnimeByID(ctx, id)
|
||||
if err != nil {
|
||||
return jikan.Anime{}, "", fmt.Errorf("failed to fetch anime details: %w", err)
|
||||
}
|
||||
@@ -57,16 +57,16 @@ func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (j
|
||||
return anime, currentStatus, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetRelations(id int) ([]jikan.RelationEntry, error) {
|
||||
return s.jikanClient.GetFullRelations(id)
|
||||
func (s *Service) GetRelations(ctx context.Context, id int) ([]jikan.RelationEntry, error) {
|
||||
return s.jikanClient.GetFullRelations(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetSchedule(day string) (jikan.ScheduleResult, error) {
|
||||
return s.jikanClient.GetSchedule(day)
|
||||
func (s *Service) GetSchedule(ctx context.Context, day string) (jikan.ScheduleResult, error) {
|
||||
return s.jikanClient.GetSchedule(ctx, day)
|
||||
}
|
||||
|
||||
func (s *Service) GetRecommendations(animeID int, limit int) ([]jikan.Anime, error) {
|
||||
return s.jikanClient.GetRecommendations(animeID, limit)
|
||||
func (s *Service) GetRecommendations(ctx context.Context, animeID int, limit int) ([]jikan.Anime, error) {
|
||||
return s.jikanClient.GetRecommendations(ctx, animeID, limit)
|
||||
}
|
||||
|
||||
func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templates.WatchingAnimeWithDetails, error) {
|
||||
@@ -77,7 +77,7 @@ func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templa
|
||||
|
||||
var result []templates.WatchingAnimeWithDetails
|
||||
for _, row := range rows {
|
||||
anime, err := s.jikanClient.GetAnimeByID(int(row.AnimeID))
|
||||
anime, err := s.jikanClient.GetAnimeByID(ctx, int(row.AnimeID))
|
||||
if err != nil {
|
||||
// Instead of skipping, we still append it, but without the extra Jikan details
|
||||
// This prevents anime from vanishing from the watchlist when Jikan rate limits us.
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package jikan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) GetAnimeByID(id int) (Anime, error) {
|
||||
func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) {
|
||||
cacheKey := fmt.Sprintf("anime:%d", id)
|
||||
var cached Anime
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result AnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/anime/%d/full", c.baseURL, id)
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return Anime{}, err
|
||||
}
|
||||
|
||||
@@ -23,6 +24,6 @@ func (c *Client) GetAnimeByID(id int) (Anime, error) {
|
||||
ttl = time.Hour * 24 * 30
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, result.Data, ttl)
|
||||
c.setCache(ctx, cacheKey, result.Data, ttl)
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewClient(db database.Querier) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) waitRateLimit() {
|
||||
func (c *Client) waitRateLimit(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -36,15 +36,24 @@ func (c *Client) waitRateLimit() {
|
||||
// 400ms base delay keeps us safely under the 3/sec limit.
|
||||
nextAllowed := c.lastReqTime.Add(400 * time.Millisecond)
|
||||
if now.Before(nextAllowed) {
|
||||
time.Sleep(nextAllowed.Sub(now))
|
||||
timer := time.NewTimer(nextAllowed.Sub(now))
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("request canceled while waiting for rate limit: %w", ctx.Err())
|
||||
}
|
||||
c.lastReqTime = time.Now()
|
||||
} else {
|
||||
c.lastReqTime = now
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getCache(key string, out any) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
func (c *Client) getCache(parentCtx context.Context, key string, out any) bool {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data, err := c.db.GetJikanCache(ctx, key)
|
||||
@@ -56,8 +65,8 @@ func (c *Client) getCache(key string, out any) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *Client) setCache(key string, data any, ttl time.Duration) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
func (c *Client) setCache(parentCtx context.Context, key string, data any, ttl time.Duration) {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
bytes, err := json.Marshal(data)
|
||||
@@ -72,12 +81,19 @@ func (c *Client) setCache(key string, data any, ttl time.Duration) {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) fetchWithRetry(urlStr string, out any) error {
|
||||
func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error {
|
||||
maxRetries := 5
|
||||
for range maxRetries {
|
||||
c.waitRateLimit()
|
||||
if err := c.waitRateLimit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Get(urlStr)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create jikan request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jikan api error: %w", err)
|
||||
}
|
||||
@@ -86,7 +102,13 @@ func (c *Client) fetchWithRetry(urlStr string, out any) error {
|
||||
resp.Body.Close()
|
||||
// Jikan rate limit is hit (usually the 60 requests/minute limit)
|
||||
// Wait for 2 seconds before retrying to let the bucket refill slightly
|
||||
time.Sleep(2 * time.Second)
|
||||
timer := time.NewTimer(2 * time.Second)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return fmt.Errorf("request canceled while retrying jikan request: %w", ctx.Err())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jikan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@@ -23,10 +24,10 @@ type RecommendationsResponse struct {
|
||||
Data []RecommendationEntry `json:"data"`
|
||||
}
|
||||
|
||||
func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
func (c *Client) GetRecommendations(ctx context.Context, animeID int, limit int) ([]Anime, error) {
|
||||
cacheKey := fmt.Sprintf("recs:%d", animeID)
|
||||
var cached []Anime
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
if limit > 0 && len(cached) > limit {
|
||||
return cached[:limit], nil
|
||||
}
|
||||
@@ -35,7 +36,7 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
|
||||
var result RecommendationsResponse
|
||||
reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID)
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
var fullAnime Anime
|
||||
animeCacheKey := fmt.Sprintf("anime:%d", rec.Entry.MalID)
|
||||
|
||||
if c.getCache(animeCacheKey, &fullAnime) {
|
||||
if c.getCache(ctx, animeCacheKey, &fullAnime) {
|
||||
animes = append(animes, fullAnime)
|
||||
} else {
|
||||
// Otherwise, map the basic recommendation data directly into an Anime struct.
|
||||
@@ -79,6 +80,6 @@ func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, animes, time.Hour*24)
|
||||
c.setCache(ctx, cacheKey, animes, time.Hour*24)
|
||||
return animes, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package jikan
|
||||
|
||||
import "maps"
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
)
|
||||
|
||||
func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int {
|
||||
for _, group := range groups {
|
||||
@@ -16,8 +19,8 @@ func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchChain(startID int, direction string, visited map[int]bool) ([]RelationEntry, error) {
|
||||
anime, err := c.GetAnimeByID(startID)
|
||||
func (c *Client) fetchChain(ctx context.Context, startID int, direction string, visited map[int]bool) ([]RelationEntry, error) {
|
||||
anime, err := c.GetAnimeByID(ctx, startID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -33,13 +36,13 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool)
|
||||
}
|
||||
visited[nextID] = true
|
||||
|
||||
nextAnime, err := c.GetAnimeByID(nextID)
|
||||
nextAnime, err := c.GetAnimeByID(ctx, nextID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry := RelationEntry{Anime: nextAnime, IsCurrent: false}
|
||||
rest, err := c.fetchChain(nextID, direction, visited)
|
||||
rest, err := c.fetchChain(ctx, nextID, direction, visited)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,20 +53,20 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool)
|
||||
return append([]RelationEntry{entry}, rest...), nil
|
||||
}
|
||||
|
||||
func (c *Client) GetFullRelations(id int) ([]RelationEntry, error) {
|
||||
currentAnime, err := c.GetAnimeByID(id)
|
||||
func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry, error) {
|
||||
currentAnime, err := c.GetAnimeByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
visited := map[int]bool{id: true}
|
||||
|
||||
prequels, err1 := c.fetchChain(id, "Prequel", visited)
|
||||
prequels, err1 := c.fetchChain(ctx, id, "Prequel", visited)
|
||||
|
||||
visitedSeq := make(map[int]bool)
|
||||
maps.Copy(visitedSeq, visited)
|
||||
|
||||
sequels, err2 := c.fetchChain(id, "Sequel", visitedSeq)
|
||||
sequels, err2 := c.fetchChain(ctx, id, "Sequel", visitedSeq)
|
||||
|
||||
var result []RelationEntry
|
||||
result = append(result, prequels...)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package jikan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) Search(query string, page int) (SearchResult, error) {
|
||||
func (c *Client) Search(ctx context.Context, query string, page int) (SearchResult, error) {
|
||||
if query == "" {
|
||||
return SearchResult{}, nil
|
||||
}
|
||||
@@ -16,14 +17,14 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
|
||||
|
||||
cacheKey := fmt.Sprintf("search:limit24:%s:%d", query, page)
|
||||
var cached SearchResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result SearchResponse
|
||||
reqURL := fmt.Sprintf("%s/anime?q=%s&limit=24&page=%d", c.baseURL, url.QueryEscape(query), page)
|
||||
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return SearchResult{}, err
|
||||
}
|
||||
|
||||
@@ -32,24 +33,24 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
|
||||
func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("top:limit24:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/top/anime?filter=bypopularity&limit=24&page=%d", c.baseURL, page)
|
||||
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return TopAnimeResult{}, err
|
||||
}
|
||||
|
||||
@@ -58,6 +59,6 @@ func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jikan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,18 +12,18 @@ type ScheduleResult struct {
|
||||
HasNextPage bool
|
||||
}
|
||||
|
||||
func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
|
||||
func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, error) {
|
||||
day = strings.ToLower(day)
|
||||
cacheKey := fmt.Sprintf("schedule_limit24_%s", day)
|
||||
|
||||
var cached ScheduleResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true&limit=24", c.baseURL, day)
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return ScheduleResult{}, err
|
||||
}
|
||||
|
||||
@@ -31,16 +32,16 @@ func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
|
||||
func (c *Client) GetFullSchedule(ctx context.Context) (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)
|
||||
res, err := c.GetSchedule(ctx, day)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch %s schedule: %w", day, err)
|
||||
}
|
||||
@@ -50,19 +51,19 @@ func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
|
||||
func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("seasons_now_limit24:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/seasons/now?limit=24&page=%d", c.baseURL, page)
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return TopAnimeResult{}, err
|
||||
}
|
||||
|
||||
@@ -71,23 +72,23 @@ func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
|
||||
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("seasons_upcoming_limit24:%d", page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(cacheKey, &cached) {
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/seasons/upcoming?limit=24&page=%d", c.baseURL, page)
|
||||
if err := c.fetchWithRetry(reqURL, &result); err != nil {
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
return TopAnimeResult{}, err
|
||||
}
|
||||
|
||||
@@ -96,6 +97,6 @@ func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (w *Worker) syncRelations(ctx context.Context) {
|
||||
|
||||
for _, a := range animes {
|
||||
func() {
|
||||
animeData, err := w.client.GetAnimeByID(int(a.ID))
|
||||
animeData, err := w.client.GetAnimeByID(ctx, int(a.ID))
|
||||
if err != nil {
|
||||
log.Printf("worker: failed to fetch anime details for %d: %v", a.ID, err)
|
||||
// Sleep a bit on error to respect rate limits, but DO NOT mark as synced
|
||||
@@ -111,7 +111,7 @@ func (w *Worker) syncRelations(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Also update the status of the anime itself so we know if it's Not yet aired, etc.
|
||||
animeDetails, err := w.client.GetAnimeByID(int(a.ID))
|
||||
animeDetails, err := w.client.GetAnimeByID(ctx, int(a.ID))
|
||||
if err == nil {
|
||||
err = w.db.UpdateAnimeStatus(ctx, database.UpdateAnimeStatusParams{
|
||||
Status: sql.NullString{String: animeDetails.Status, Valid: true},
|
||||
@@ -130,7 +130,7 @@ func (w *Worker) ensureAnimeExistsAndStatusUpdated(ctx context.Context, malID in
|
||||
_, err := w.db.GetAnime(ctx, int64(malID))
|
||||
if err != nil {
|
||||
// we don't have it, let's fetch it
|
||||
animeDetails, err := w.client.GetAnimeByID(malID)
|
||||
animeDetails, err := w.client.GetAnimeByID(ctx, malID)
|
||||
if err != nil {
|
||||
log.Printf("worker: failed to fetch related anime %d: %v", malID, err)
|
||||
return
|
||||
@@ -163,7 +163,7 @@ func (w *Worker) ensureAnimeExistsAndStatusUpdated(ctx context.Context, malID in
|
||||
// but since it's a Sequel to something they watched, we could fetch it.
|
||||
// For now, let's just let the worker naturally pick it up if it gets added to watchlist,
|
||||
// OR we can explicitly fetch its details to keep sequels up to date.
|
||||
animeDetails, err := w.client.GetAnimeByID(malID)
|
||||
animeDetails, err := w.client.GetAnimeByID(ctx, malID)
|
||||
if err == nil {
|
||||
w.db.UpdateAnimeStatus(ctx, database.UpdateAnimeStatusParams{
|
||||
Status: sql.NullString{String: animeDetails.Status, Valid: true},
|
||||
|
||||
Reference in New Issue
Block a user