package controller import ( "fmt" "net/http" "strconv" "github.com/fjurmanovic/sugarr/internal/model" "github.com/fjurmanovic/sugarr/internal/service" "github.com/fjurmanovic/sugarr/internal/utl/configs" "github.com/fjurmanovic/sugarr/internal/utl/httputil" "github.com/rs/zerolog/log" ) type RecommendationsController struct { cfg *configs.Config rec *service.Recommender } func NewRecommendationsController(mux *http.ServeMux, cfg *configs.Config, rec *service.Recommender) *RecommendationsController { c := &RecommendationsController{cfg: cfg, rec: rec} mux.HandleFunc("GET /recommendations", c.handleList) mux.HandleFunc("POST /recommendations/search", c.handleSearch) return c } // handleList runs the recommendation pipeline and returns results as JSON. // Query params: ?count=10&force_llm=true&force_tmdb=true func (c *RecommendationsController) handleList(w http.ResponseWriter, r *http.Request) { count := c.cfg.RecommendCount if v := r.URL.Query().Get("count"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { count = n } } // Temporarily override LLM flag for force_llm / force_tmdb params origLLM := c.cfg.LLMEnabled if r.URL.Query().Get("force_llm") == "true" { c.cfg.LLMEnabled = true } else if r.URL.Query().Get("force_tmdb") == "true" { c.cfg.LLMEnabled = false } recs, err := c.rec.RunWithCount(count) c.cfg.LLMEnabled = origLLM if err != nil { log.Error().Err(err).Msg("Recommendation pipeline failed") httputil.WriteError(w, http.StatusInternalServerError, err.Error()) return } httputil.WriteJSON(w, http.StatusOK, recs) } // handleSearch performs a natural-language LLM search and resolves results via TMDb. // Body: {"query": "sci-fi movies like Blade Runner but more recent"} func (c *RecommendationsController) handleSearch(w http.ResponseWriter, r *http.Request) { if !c.cfg.LLMEnabled || c.rec.LLM() == nil { httputil.WriteError(w, http.StatusBadRequest, "LLM is not enabled") return } var body struct { Query string `json:"query"` } if err := decodeJSON(r, &body); err != nil || body.Query == "" { httputil.WriteError(w, http.StatusBadRequest, "body must be {\"query\": \"...\"}") return } prompt := fmt.Sprintf( "You are a movie recommendation engine. The user is looking for: %s\n\n"+ "Return ONLY a JSON array of %d movies matching this request. No other text.\n"+ "Each entry: {\"title\": \"Movie Title\", \"year\": 2024, \"reason\": \"brief reason\"}", body.Query, c.cfg.RecommendCount) suggestions, err := c.rec.LLM().Recommend(prompt) if err != nil { log.Error().Err(err).Msg("LLM search failed") httputil.WriteError(w, http.StatusInternalServerError, err.Error()) return } library, _ := c.rec.Jellyfin().GetAllMovies() exclude := map[int]bool{} for _, m := range library { if id, err := strconv.Atoi(m.TMDbID); err == nil && id != 0 { exclude[id] = true } } var recs []model.Recommendation for _, sug := range suggestions { if len(recs) >= c.cfg.RecommendCount { break } tm, err := c.rec.TMDb().SearchMovie(sug.Title, sug.Year) if err != nil || tm == nil || exclude[tm.ID] { continue } exclude[tm.ID] = true recs = append(recs, model.Recommendation{ Title: tm.Title, Year: tm.Year, TMDbID: tm.ID, Reason: sug.Reason, }) } httputil.WriteJSON(w, http.StatusOK, recs) }