package anime import ( "encoding/json" "html" "log" "net/http" "strconv" "strings" "mal/integrations/jikan" ctxpkg "mal/internal/context" "mal/internal/db" "mal/templates" ) type Handler struct { jikanClient *jikan.Client db database.Querier } 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) if err := templates.GetRenderer().ExecuteTemplate(w, "not_found.gohtml", map[string]any{ "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func writeInlineLoadError(w http.ResponseWriter, message string) { w.Header().Set("Content-Type", "text/html") _, _ = w.Write([]byte(`
` + html.EscapeString(message) + `
`)) } func parsePageParam(r *http.Request) int { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { return 1 } return page } func NewHandler(jikanClient *jikan.Client, db database.Querier) *Handler { return &Handler{jikanClient: jikanClient, db: db} } func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { renderNotFoundPage(r, w) return } animes, err := h.jikanClient.GetTopAnime(r.Context(), 1) if err != nil { log.Printf("top anime error: %v", err) http.Error(w, "Failed to fetch anime", http.StatusInternalServerError) return } currentlyAiring, err := h.jikanClient.GetSeasonsNow(r.Context(), 1) if err != nil { log.Printf("seasons now error: %v", err) // non-fatal } if len(animes.Animes) > 6 { animes.Animes = animes.Animes[:6] } if len(currentlyAiring.Animes) > 6 { currentlyAiring.Animes = currentlyAiring.Animes[:6] } // Fetch continue watching if logged in (user ID in context, handle this safely) // We'll skip DB fetch for continue watching for now if it requires complex session parsing // Actually we should try to fetch it if we can. var cw []database.GetContinueWatchingEntriesRow user, userOk := r.Context().Value(ctxpkg.UserKey).(*database.User) if userOk && user != nil { cw, _ = h.db.GetContinueWatchingEntries(r.Context(), user.ID) } if err := templates.GetRenderer().ExecuteTemplate(w, "index.gohtml", map[string]any{ "MostPopular": animes.Animes, "CurrentlyAiring": currentlyAiring.Animes, "ContinueWatching": cw, "User": user, "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) { user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User) q := r.URL.Query().Get("q") animeType := r.URL.Query().Get("type") status := r.URL.Query().Get("status") orderBy := r.URL.Query().Get("order_by") sort := r.URL.Query().Get("sort") res, err := h.jikanClient.SearchAdvanced(r.Context(), q, animeType, status, orderBy, sort, 1, 24) if err != nil { log.Printf("browse error: %v", err) } if err := templates.GetRenderer().ExecuteTemplate(w, "browse.gohtml", map[string]any{ "User": user, "CurrentPath": r.URL.Path, "Query": q, "Type": animeType, "Status": status, "OrderBy": orderBy, "Sort": sort, "Animes": res.Animes, }); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { renderNotFoundPage(r, w) } func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not implemented yet", http.StatusNotImplemented) } func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not implemented yet", http.StatusNotImplemented) } func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { idStr := strings.TrimPrefix(r.URL.Path, "/anime/") idStr = strings.TrimSuffix(idStr, "/") id, err := strconv.Atoi(idStr) if err != nil { renderNotFoundPage(r, w) return } anime, err := h.jikanClient.GetAnimeByID(r.Context(), id) if err != nil { renderNotFoundPage(r, w) return } user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User) var status string if user != nil { entry, err := h.db.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{ UserID: user.ID, AnimeID: int64(id), }) if err == nil { status = entry.Status } } if err := templates.GetRenderer().ExecuteTemplate(w, "anime.gohtml", map[string]any{ "Anime": anime, "User": user, "Status": status, "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func (h *Handler) HandleHTMLWatchOrder(w http.ResponseWriter, r *http.Request) { animeIdStr := r.URL.Query().Get("animeId") id, err := strconv.Atoi(animeIdStr) if err != nil { http.Error(w, `