mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 07:41:08 +00:00
128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
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)
|
|
mux.HandleFunc("POST /recommendations/trigger", c.handleTrigger)
|
|
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)
|
|
}
|
|
|
|
// handleTrigger runs the same pipeline as the cron job: recommend + auto-request if Radarr is enabled.
|
|
func (c *RecommendationsController) handleTrigger(w http.ResponseWriter, r *http.Request) {
|
|
log.Info().Msg("Trigger: running recommendation pipeline")
|
|
recs, err := c.rec.Run()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Trigger: pipeline failed")
|
|
httputil.WriteError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if c.cfg.RadarrEnabled {
|
|
c.rec.AutoRequest(recs)
|
|
}
|
|
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)
|
|
}
|