# sugarr Movie-recommendation companion for Jellyfin: pulls your favorite/recently-watched movies, gets suggestions from an LLM (via Langfuse-managed prompts) or a TMDb similarity/frequency fallback, resolves them against TMDb, excludes anything already in your library or Radarr, and can auto-request the rest via Radarr. Runs on a cron schedule and/or as an HTTP API. Built with a TypeScript/OOP architecture (Node + Fastify + Langfuse): a reusable `src/core/` framework (HTTP client base classes, an LLM tool/pipeline abstraction with Langfuse-cached prompts, Fastify server scaffolding) and an `src/app/` layer with Sugarr's concrete clients/tools/pipeline/controllers. See `docs/superpowers/specs/2026-08-12-typescript-oop-rewrite-design.md` and `docs/superpowers/plans/2026-08-12-typescript-oop-rewrite.md` for the design and implementation plan (this repo previously also had a Go implementation, now removed in favor of this one). ## Running the app ```bash pnpm install cp .env.example .env # fill in the values below pnpm dev # or: pnpm build && pnpm start ``` ### Required environment variables See `.env.example` for the full list with defaults. At minimum you need: - `JELLYFIN_URL`, `JELLYFIN_API_KEY`, `JELLYFIN_USER_ID`, `TMDB_API_KEY` - `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_BASE_URL` — the app fetches its LLM prompts from Langfuse at runtime (see below), so these are required even if `LLM_ENABLED=false` (only the recommend/search endpoints actually call Langfuse; if you never enable the LLM path you can point these at any reachable Langfuse instance). ### Docker ```bash docker compose up -d --build ``` ## Seeding Langfuse prompts The app fetches two chat prompts from Langfuse by name — `movie-recommender` and `movie-search` — compiles them with per-request variables, and reads model/temperature/etc. from each prompt version's `config` field. Ready-to-import definitions are in `langfuse/`: - `langfuse/movie-recommender.prompt.json` - `langfuse/movie-search.prompt.json` Create each one in your Langfuse project (UI: **Prompts → New prompt**, type **Chat**; or via the Langfuse API/SDK) using the `prompt` array as the chat messages and the `config` object as the prompt's config. Variables use `{{name}}` interpolation: - `movie-recommender`: `count`, `favorites`, `watched`, `library`, `previouslySuggested` - `movie-search`: `query`, `count` ## Prompt caching and invalidation Compiled prompts are cached in memory per process (`LANGFUSE_PROMPT_CACHE_ENABLED`, default `true`; set to `false` to always fetch fresh from Langfuse — useful while iterating on a prompt). To invalidate the cache after updating a prompt in Langfuse without restarting the app, call: ``` POST /webhooks/langfuse/invalidate Content-Type: application/json Authorization: Bearer # if SECRET_KEY is set {"promptName": "movie-recommender"} # or {} / omit body to clear the whole cache ``` This isn't wired to a Langfuse-native push webhook (self-hosted Langfuse has no built-in "prompt updated" event) — call it manually, from CI after a prompt change, or from Langfuse Cloud's webhook feature if you have one configured. ## HTTP API | Method | Path | Notes | |---|---|---| | GET | `/health` | exempt from auth | | GET | `/recommendations` | `?count=`, `?force_llm=true`, `?force_tmdb=true` | | POST | `/recommendations/search` | body `{"query": "..."}`, requires `LLM_ENABLED=true` | | POST | `/recommendations/trigger` | runs the pipeline + auto-requests via Radarr if enabled | | POST | `/recommendations/request` | body `{"tmdb_id": 12345}`, requires `RADARR_ENABLED=true` | | POST | `/webhooks/langfuse/invalidate` | body `{"promptName"?: string}` | If `SECRET_KEY` is set, all routes except `/health` require `Authorization: Bearer `.