mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 07:41:08 +00:00
init
This commit is contained in:
26
internal/controller/health.go
Normal file
26
internal/controller/health.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/fjurmanovic/sugarr/internal/utl/configs"
|
||||
"github.com/fjurmanovic/sugarr/internal/utl/httputil"
|
||||
)
|
||||
|
||||
type HealthController struct {
|
||||
cfg *configs.Config
|
||||
}
|
||||
|
||||
func NewHealthController(mux *http.ServeMux, cfg *configs.Config) *HealthController {
|
||||
c := &HealthController{cfg: cfg}
|
||||
mux.HandleFunc("GET /health", c.handle)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *HealthController) handle(w http.ResponseWriter, _ *http.Request) {
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "ok",
|
||||
"llm": c.cfg.LLMEnabled,
|
||||
"radarr": c.cfg.RadarrEnabled,
|
||||
})
|
||||
}
|
||||
111
internal/controller/recommendations.go
Normal file
111
internal/controller/recommendations.go
Normal file
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
}
|
||||
59
internal/controller/request.go
Normal file
59
internal/controller/request.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/fjurmanovic/sugarr/internal/service"
|
||||
"github.com/fjurmanovic/sugarr/internal/utl/configs"
|
||||
"github.com/fjurmanovic/sugarr/internal/utl/httputil"
|
||||
)
|
||||
|
||||
type RequestController struct {
|
||||
cfg *configs.Config
|
||||
rec *service.Recommender
|
||||
}
|
||||
|
||||
func NewRequestController(mux *http.ServeMux, cfg *configs.Config, rec *service.Recommender) *RequestController {
|
||||
c := &RequestController{cfg: cfg, rec: rec}
|
||||
mux.HandleFunc("POST /recommendations/request", c.handle)
|
||||
return c
|
||||
}
|
||||
|
||||
// handle adds a movie to Radarr by TMDb ID.
|
||||
// Body: {"tmdb_id": 12345}
|
||||
func (c *RequestController) handle(w http.ResponseWriter, r *http.Request) {
|
||||
if !c.cfg.RadarrEnabled || c.rec.Radarr() == nil {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "Radarr is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
TMDbID int `json:"tmdb_id"`
|
||||
}
|
||||
if err := decodeJSON(r, &body); err != nil || body.TMDbID == 0 {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "body must be {\"tmdb_id\": 12345}")
|
||||
return
|
||||
}
|
||||
|
||||
movie, err := c.rec.Radarr().Lookup(body.TMDbID)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusInternalServerError, fmt.Sprintf("radarr lookup failed: %s", err))
|
||||
return
|
||||
}
|
||||
if err := c.rec.Radarr().AddMovie(*movie); err != nil {
|
||||
httputil.WriteError(w, http.StatusInternalServerError, fmt.Sprintf("radarr add failed: %s", err))
|
||||
return
|
||||
}
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"requested": true, "title": movie.Title})
|
||||
}
|
||||
|
||||
func decodeJSON(r *http.Request, v any) error {
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
Reference in New Issue
Block a user