refactor: centralize jikan list constants
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"mal/internal/database"
|
||||
"mal/internal/jikan"
|
||||
@@ -29,6 +30,23 @@ type Handler struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
type quickSearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
func renderNotFoundPage(r *http.Request, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates.NotFoundPage().Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func writeInlineLoadError(w http.ResponseWriter, message string) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = w.Write([]byte(`<p style="color: var(--text-muted); font-size: var(--text-sm);">` + message + `</p>`))
|
||||
}
|
||||
|
||||
func parsePageParam(r *http.Request) int {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
@@ -53,8 +71,7 @@ func NewHandler(svc *Service) *Handler {
|
||||
|
||||
func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates.NotFoundPage().Render(r.Context(), w)
|
||||
renderNotFoundPage(r, w)
|
||||
return
|
||||
}
|
||||
templates.Catalog().Render(r.Context(), w)
|
||||
@@ -110,7 +127,7 @@ func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if fallbackPlaceholder {
|
||||
templates.CatalogPlaceholderItems(24).Render(r.Context(), w)
|
||||
templates.CatalogPlaceholderItems(jikan.ListPageSize).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,8 +140,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.URL.Path[len("/anime/"):]
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates.NotFoundPage().Render(r.Context(), w)
|
||||
renderNotFoundPage(r, w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,8 +154,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if jikan.IsNotFoundError(err) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates.NotFoundPage().Render(r.Context(), w)
|
||||
renderNotFoundPage(r, w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,55 +166,27 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
templates.AnimeDetails(anime, currentStatus).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len("/api/anime/"):]
|
||||
idStr := ""
|
||||
for i, c := range path {
|
||||
if c == '/' {
|
||||
idStr = path[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
if id <= 0 {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
templates.AnimeRelationsList(relations).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
// HandleAPIAnime routes anime API requests
|
||||
func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len("/api/anime/"):]
|
||||
|
||||
// Parse: {id}/relations or {id}/recommendations
|
||||
parts := splitPath(path)
|
||||
if len(parts) < 2 {
|
||||
idPart, section, ok := strings.Cut(path, "/")
|
||||
if !ok || section == "" {
|
||||
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(parts[0])
|
||||
id, err := strconv.Atoi(idPart)
|
||||
if err != nil || id <= 0 {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
switch section {
|
||||
case "relations":
|
||||
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")
|
||||
w.Write([]byte(`<p style="color: var(--text-muted); font-size: var(--text-sm);">Failed to load relations.</p>`))
|
||||
writeInlineLoadError(w, "Failed to load relations.")
|
||||
return
|
||||
}
|
||||
templates.AnimeRelationsList(relations).Render(r.Context(), w)
|
||||
@@ -207,69 +194,40 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
w.Write([]byte(`<p style="color: var(--text-muted); font-size: var(--text-sm);">Failed to load recommendations.</p>`))
|
||||
writeInlineLoadError(w, "Failed to load recommendations.")
|
||||
return
|
||||
}
|
||||
templates.AnimeRecommendations(recs).Render(r.Context(), w)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates.NotFoundPage().Render(r.Context(), w)
|
||||
renderNotFoundPage(r, w)
|
||||
}
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
var parts []string
|
||||
var current string
|
||||
for _, c := range path {
|
||||
if c == '/' {
|
||||
if current != "" {
|
||||
parts = append(parts, current)
|
||||
current = ""
|
||||
}
|
||||
} else {
|
||||
current += string(c)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
parts = append(parts, current)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode([]any{})
|
||||
json.NewEncoder(w).Encode([]quickSearchResult{})
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit to 5 results
|
||||
results := res.Animes
|
||||
if len(results) > 5 {
|
||||
results = results[:5]
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
output := make([]SearchResult, len(results))
|
||||
output := make([]quickSearchResult, len(results))
|
||||
for i, anime := range results {
|
||||
output[i] = SearchResult{
|
||||
output[i] = quickSearchResult{
|
||||
ID: anime.MalID,
|
||||
Title: anime.DisplayTitle(),
|
||||
Type: anime.Type,
|
||||
@@ -277,7 +235,6 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(output)
|
||||
}
|
||||
|
||||
7
internal/jikan/constants.go
Normal file
7
internal/jikan/constants.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package jikan
|
||||
|
||||
import "time"
|
||||
|
||||
const ListPageSize = 24
|
||||
|
||||
const shortCacheTTL = time.Hour
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) Search(ctx context.Context, query string, page int) (SearchResult, error) {
|
||||
@@ -15,7 +14,7 @@ func (c *Client) Search(ctx context.Context, query string, page int) (SearchResu
|
||||
page = 1
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("search:limit24:%s:%d", query, page)
|
||||
cacheKey := fmt.Sprintf("search:limit%d:%s:%d", ListPageSize, query, page)
|
||||
var cached SearchResult
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
@@ -25,7 +24,7 @@ func (c *Client) Search(ctx context.Context, query string, page int) (SearchResu
|
||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||
|
||||
var result SearchResponse
|
||||
reqURL := fmt.Sprintf("%s/anime?q=%s&limit=24&page=%d", c.baseURL, url.QueryEscape(query), page)
|
||||
reqURL := fmt.Sprintf("%s/anime?q=%s&limit=%d&page=%d", c.baseURL, url.QueryEscape(query), ListPageSize, page)
|
||||
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
if hasStale {
|
||||
@@ -40,7 +39,7 @@ func (c *Client) Search(ctx context.Context, query string, page int) (SearchResu
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@ func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, err
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("top:limit24:%d", page)
|
||||
cacheKey := fmt.Sprintf("top:limit%d:%d", ListPageSize, page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
@@ -58,7 +57,7 @@ func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, err
|
||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/top/anime?filter=bypopularity&limit=24&page=%d", c.baseURL, page)
|
||||
reqURL := fmt.Sprintf("%s/top/anime?filter=bypopularity&limit=%d&page=%d", c.baseURL, ListPageSize, page)
|
||||
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
if hasStale {
|
||||
@@ -73,6 +72,6 @@ func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, err
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ScheduleResult struct {
|
||||
@@ -14,7 +13,7 @@ type ScheduleResult struct {
|
||||
|
||||
func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, error) {
|
||||
day = strings.ToLower(day)
|
||||
cacheKey := fmt.Sprintf("schedule_limit24_%s", day)
|
||||
cacheKey := fmt.Sprintf("schedule_limit%d_%s", ListPageSize, day)
|
||||
|
||||
var cached ScheduleResult
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
@@ -25,7 +24,7 @@ func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, e
|
||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true&limit=24", c.baseURL, day)
|
||||
reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true&limit=%d", c.baseURL, day, ListPageSize)
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
if hasStale {
|
||||
return stale, nil
|
||||
@@ -39,7 +38,7 @@ func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, e
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, e
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("seasons_now_limit24:%d", page)
|
||||
cacheKey := fmt.Sprintf("seasons_now_limit%d:%d", ListPageSize, page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
@@ -72,7 +71,7 @@ func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, e
|
||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/seasons/now?limit=24&page=%d", c.baseURL, page)
|
||||
reqURL := fmt.Sprintf("%s/seasons/now?limit=%d&page=%d", c.baseURL, ListPageSize, page)
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
if hasStale {
|
||||
return stale, nil
|
||||
@@ -86,7 +85,7 @@ func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, e
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
cacheKey := fmt.Sprintf("seasons_upcoming_limit24:%d", page)
|
||||
cacheKey := fmt.Sprintf("seasons_upcoming_limit%d:%d", ListPageSize, page)
|
||||
var cached TopAnimeResult
|
||||
if c.getCache(ctx, cacheKey, &cached) {
|
||||
return cached, nil
|
||||
@@ -104,7 +103,7 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
|
||||
hasStale := c.getStaleCache(ctx, cacheKey, &stale)
|
||||
|
||||
var result TopAnimeResponse
|
||||
reqURL := fmt.Sprintf("%s/seasons/upcoming?limit=24&page=%d", c.baseURL, page)
|
||||
reqURL := fmt.Sprintf("%s/seasons/upcoming?limit=%d&page=%d", c.baseURL, ListPageSize, page)
|
||||
if err := c.fetchWithRetry(ctx, reqURL, &result); err != nil {
|
||||
if hasStale {
|
||||
return stale, nil
|
||||
@@ -118,6 +117,6 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
|
||||
HasNextPage: result.Pagination.HasNextPage,
|
||||
}
|
||||
|
||||
c.setCache(ctx, cacheKey, res, time.Hour*1)
|
||||
c.setCache(ctx, cacheKey, res, shortCacheTTL)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user