diff --git a/internal/handlers/anime.go b/internal/features/anime/handler.go similarity index 62% rename from internal/handlers/anime.go rename to internal/features/anime/handler.go index a7b42a8..0b85a6d 100644 --- a/internal/handlers/anime.go +++ b/internal/features/anime/handler.go @@ -1,4 +1,4 @@ -package handlers +package anime import ( "log" @@ -6,21 +6,19 @@ import ( "strconv" "malago/internal/database" - "malago/internal/jikan" "malago/internal/middleware" "malago/internal/templates" ) -type AnimeHandler struct { - jikan *jikan.Client - db *database.Queries +type Handler struct { + svc *Service } -func NewAnimeHandler(jikan *jikan.Client, db *database.Queries) *AnimeHandler { - return &AnimeHandler{jikan: jikan, db: db} +func NewHandler(svc *Service) *Handler { + return &Handler{svc: svc} } -func (h *AnimeHandler) HandleCatalog(w http.ResponseWriter, r *http.Request) { +func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return @@ -28,16 +26,15 @@ func (h *AnimeHandler) HandleCatalog(w http.ResponseWriter, r *http.Request) { templates.Catalog().Render(r.Context(), w) } -func (h *AnimeHandler) HandleSearch(w http.ResponseWriter, r *http.Request) { +func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { templates.Search("").Render(r.Context(), w) return } - // Check if HTMX request for results only if r.Header.Get("HX-Request") == "true" { - res, err := h.jikan.Search(query, 1) + res, err := h.svc.Search(query, 1) if err != nil { log.Printf("search error: %v", err) http.Error(w, "Failed to search anime", http.StatusInternalServerError) @@ -47,11 +44,10 @@ func (h *AnimeHandler) HandleSearch(w http.ResponseWriter, r *http.Request) { return } - // Full page with query templates.Search(query).Render(r.Context(), w) } -func (h *AnimeHandler) HandleAPISearch(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) @@ -59,7 +55,7 @@ func (h *AnimeHandler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { page = 1 } - res, err := h.jikan.Search(query, page) + res, err := h.svc.Search(query, page) if err != nil { log.Printf("search pagination error: %v", err) http.Error(w, "Failed to fetch search page", http.StatusInternalServerError) @@ -69,14 +65,14 @@ func (h *AnimeHandler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { templates.SearchItems(query, res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) } -func (h *AnimeHandler) HandleAPICatalog(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 } - res, err := h.jikan.GetTopAnime(page) + res, err := h.svc.GetTopAnime(page) if err != nil { log.Printf("top anime error: %v", err) http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError) @@ -86,7 +82,7 @@ func (h *AnimeHandler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) templates.CatalogItems(res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) } -func (h *AnimeHandler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { +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 { @@ -94,29 +90,22 @@ func (h *AnimeHandler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request return } - anime, err := h.jikan.GetAnimeByID(id) + userID := "" + if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil { + userID = user.ID + } + + anime, currentStatus, err := h.svc.GetAnimeDetails(r.Context(), id, userID) if err != nil { log.Printf("anime fetch error for %d: %v", id, err) http.Error(w, "Failed to fetch anime details", http.StatusInternalServerError) return } - // Get current watchlist status if user is logged in - currentStatus := "" - if user := middleware.GetUser(r.Context()); user != nil { - entry, err := h.db.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{ - UserID: user.ID, - AnimeID: int64(id), - }) - if err == nil { - currentStatus = entry.Status - } - } - templates.AnimeDetails(anime, currentStatus).Render(r.Context(), w) } -func (h *AnimeHandler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request) { +func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request) { path := r.URL.Path[len("/api/anime/"):] idStr := "" for i, c := range path { @@ -132,6 +121,6 @@ func (h *AnimeHandler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Re return } - relations := h.jikan.GetFullRelations(id) + relations := h.svc.GetRelations(id) templates.AnimeRelationsList(relations).Render(r.Context(), w) } diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go new file mode 100644 index 0000000..183cb35 --- /dev/null +++ b/internal/features/anime/service.go @@ -0,0 +1,53 @@ +package anime + +import ( + "context" + "fmt" + + "malago/internal/database" + "malago/internal/jikan" +) + +type Service struct { + jikanClient *jikan.Client + db database.Querier +} + +func NewService(jikanClient *jikan.Client, db database.Querier) *Service { + return &Service{ + jikanClient: jikanClient, + db: db, + } +} + +func (s *Service) Search(query string, page int) (jikan.SearchResult, error) { + return s.jikanClient.Search(query, page) +} + +func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetTopAnime(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 { + return jikan.Anime{}, "", fmt.Errorf("failed to fetch anime details: %w", err) + } + + currentStatus := "" + if userID != "" { + entry, err := s.db.GetWatchListEntry(ctx, database.GetWatchListEntryParams{ + UserID: userID, + AnimeID: int64(id), + }) + if err == nil { + currentStatus = entry.Status + } + } + + return anime, currentStatus, nil +} + +func (s *Service) GetRelations(id int) []jikan.RelationEntry { + return s.jikanClient.GetFullRelations(id) +} diff --git a/internal/jikan/types.go b/internal/jikan/types.go index a683f57..bc3a533 100644 --- a/internal/jikan/types.go +++ b/internal/jikan/types.go @@ -2,6 +2,16 @@ package jikan import "fmt" +type SearchResult struct { + Animes []Anime + HasNextPage bool +} + +type TopAnimeResult struct { + Animes []Anime + HasNextPage bool +} + // NamedEntity represents genres, studios, producers, etc. type NamedEntity struct { MalID int `json:"mal_id"` @@ -145,13 +155,3 @@ func (a Anime) DisplayTitle() string { } return a.Title } - -type SearchResult struct { - Animes []Anime - HasNextPage bool -} - -type TopAnimeResult struct { - Animes []Anime - HasNextPage bool -} diff --git a/internal/server/routes.go b/internal/server/routes.go index 3a98b77..c9e11dd 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -4,9 +4,9 @@ import ( "net/http" "malago/internal/database" + "malago/internal/features/anime" "malago/internal/features/auth" "malago/internal/features/watchlist" - "malago/internal/handlers" "malago/internal/jikan" "malago/internal/middleware" ) @@ -21,9 +21,12 @@ func NewRouter(cfg Config) http.Handler { mux := http.NewServeMux() authHandler := auth.NewHandler(cfg.AuthService) + watchlistSvc := watchlist.NewService(cfg.DB) watchlistHandler := watchlist.NewHandler(watchlistSvc) - animeHandler := handlers.NewAnimeHandler(cfg.JikanClient, cfg.DB) + + animeSvc := anime.NewService(cfg.JikanClient, cfg.DB) + animeHandler := anime.NewHandler(animeSvc) // Serve static files fs := http.FileServer(http.Dir("./static"))