- 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.
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. Seedocs/superpowers/specs/2026-08-12-typescript-oop-rewrite-design.mdanddocs/superpowers/plans/2026-08-12-typescript-oop-rewrite.mdfor 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_KEYLANGFUSE_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 ifLLM_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.jsonlangfuse/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,previouslySuggestedmovie-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>.