diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index 953eb03..e056018 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -172,3 +172,41 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } + +func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) { + templates.Discover().Render(r.Context(), w) +} + +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 + } + + res, err := h.svc.GetAiringAnime(page) + if err != nil { + log.Printf("airing anime error: %v", err) + http.Error(w, "Failed to fetch airing anime", http.StatusInternalServerError) + return + } + + templates.DiscoverItems(res.Animes, "airing", page+1, res.HasNextPage).Render(r.Context(), w) +} + +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 + } + + res, err := h.svc.GetUpcomingAnime(page) + if err != nil { + log.Printf("upcoming anime error: %v", err) + http.Error(w, "Failed to fetch upcoming anime", http.StatusInternalServerError) + return + } + + templates.DiscoverItems(res.Animes, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w) +} diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go index e53d997..7e60522 100644 --- a/internal/features/anime/service.go +++ b/internal/features/anime/service.go @@ -28,6 +28,14 @@ func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) { return s.jikanClient.GetTopAnime(page) } +func (s *Service) GetAiringAnime(page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsNow(page) +} + +func (s *Service) GetUpcomingAnime(page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsUpcoming(page) +} + func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) { anime, err := s.jikanClient.GetAnimeByID(id) if err != nil { diff --git a/internal/jikan/client.go b/internal/jikan/client.go index 41f9b69..24cbdd0 100644 --- a/internal/jikan/client.go +++ b/internal/jikan/client.go @@ -14,6 +14,8 @@ type Client struct { baseURL string cache *expirable.LRU[string, SearchResult] topCache *expirable.LRU[int, TopAnimeResult] + airingCache *expirable.LRU[int, TopAnimeResult] + upcomingCache *expirable.LRU[int, TopAnimeResult] animeCache *expirable.LRU[int, Anime] relationsCache *expirable.LRU[int, JikanRelationsResponse] } @@ -21,6 +23,8 @@ type Client struct { func NewClient() *Client { cache := expirable.NewLRU[string, SearchResult](500, nil, time.Hour*1) topCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) + airingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) + 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) @@ -29,6 +33,8 @@ func NewClient() *Client { baseURL: "https://api.jikan.moe/v4", cache: cache, topCache: topCache, + airingCache: airingCache, + upcomingCache: upcomingCache, animeCache: animeCache, relationsCache: relationsCache, } diff --git a/internal/jikan/seasons.go b/internal/jikan/seasons.go new file mode 100644 index 0000000..ffed9c6 --- /dev/null +++ b/internal/jikan/seasons.go @@ -0,0 +1,51 @@ +package jikan + +import "fmt" + +// GetSeasonsNow fetches currently airing anime +func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) { + if page < 1 { + page = 1 + } + if cached, ok := c.airingCache.Get(page); ok { + return cached, nil + } + + var result TopAnimeResponse + reqURL := fmt.Sprintf("%s/seasons/now?page=%d", c.baseURL, page) + if err := c.fetchWithRetry(reqURL, &result); err != nil { + return TopAnimeResult{}, err + } + + res := TopAnimeResult{ + Animes: result.Data, + HasNextPage: result.Pagination.HasNextPage, + } + + c.airingCache.Add(page, res) + return res, nil +} + +// GetSeasonsUpcoming fetches upcoming anime +func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) { + if page < 1 { + page = 1 + } + if cached, ok := c.upcomingCache.Get(page); ok { + return cached, nil + } + + var result TopAnimeResponse + reqURL := fmt.Sprintf("%s/seasons/upcoming?page=%d", c.baseURL, page) + if err := c.fetchWithRetry(reqURL, &result); err != nil { + return TopAnimeResult{}, err + } + + res := TopAnimeResult{ + Animes: result.Data, + HasNextPage: result.Pagination.HasNextPage, + } + + c.upcomingCache.Add(page, res) + return res, nil +} diff --git a/internal/server/routes.go b/internal/server/routes.go index d7adfa1..6960da0 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -34,6 +34,9 @@ func NewRouter(cfg Config) http.Handler { // Anime / Search / Catalog mux.HandleFunc("/", animeHandler.HandleCatalog) + mux.HandleFunc("/discover", animeHandler.HandleDiscover) + mux.HandleFunc("/api/discover/airing", animeHandler.HandleAPIDiscoverAiring) + mux.HandleFunc("/api/discover/upcoming", animeHandler.HandleAPIDiscoverUpcoming) mux.HandleFunc("/search", animeHandler.HandleSearch) mux.HandleFunc("/api/search", animeHandler.HandleAPISearch) mux.HandleFunc("/api/search-quick", animeHandler.HandleQuickSearch) diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ index 36c6057..9cc63d0 100644 --- a/internal/templates/anime.templ +++ b/internal/templates/anime.templ @@ -11,7 +11,7 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {