Files
sugarr/README.md
Fran Jurmanović ae8bfaa2eb Fix final review findings: key leak, model fallback, error resilience, docs
- ApiClient: strip query string from ApiError messages so API keys
  (e.g. TMDb's api_key query param) never leak into 500 response
  bodies or logs.
- LlmTool: accept an optional defaultModel, wired from AppConfig's
  LLM_MODEL, so Langfuse prompts that omit a model config fall back
  to the configured LLM rather than silently requesting gpt-4o-mini.
- RecommendationPipeline: restore the Go original's graceful
  degradation — library/Radarr fetch failures, per-suggestion TMDb
  resolution failures, and per-signal TMDb similarity/recommendation
  failures are now logged and skipped instead of aborting the whole
  run (applies to both runInternal and searchAsync).
- RecommendationsController: validate the ?count= query param
  (must be a positive integer) instead of silently producing NaN.
- Commit the Langfuse prompt definitions (langfuse/*.prompt.json) and
  add a README covering setup, prompt seeding, cache invalidation,
  and the HTTP API surface — the branch was not otherwise runnable by
  anyone but the author.
2026-08-12 21:40:09 +02:00

3.7 KiB

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.

This repo currently has two implementations:

  • cmd/, internal/ — the original Go implementation.
  • src/ — a TypeScript/OOP rewrite (Node + Fastify + Langfuse), the one documented below. 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.

Running the TypeScript app

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

docker compose -f docker-compose.node.yml up -d --build

Uses Dockerfile.node / docker-compose.node.yml, kept separate from the Go app's Dockerfile / docker-compose.yml.

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 <SECRET_KEY>   # 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 <SECRET_KEY>.