No description
  • TypeScript 86.8%
  • Nunjucks 12.8%
  • CSS 0.1%
  • Dockerfile 0.1%
Find a file
Andrew Todd 9499668fd3
All checks were successful
Build & Push Docker Image / build (push) Successful in 51s
feat(rec): per-card member interest UI + audience staleness fixes (#19)
Each card now ships memberInterest with per-member score + rated sources;
multi-member audiences render tier-ringed avatars with hover tooltips and a
"View by member" sidebar that reorders cards client-side. Adds ?debug=recs
for inspecting raw source contributions. Fixes buildConsensusPicks to
enforce all-members-influence (was missing the COUNT(DISTINCT) HAVING) and
adds audience.last_refreshed_at so the TMDB worker re-scores after new
ratings even when source rec_links are already cached. Bumps the Ollama
timeout default from 60s to 180s.

Reviewed-on: #19
2026-05-27 05:46:42 +00:00
.claude feat: docker build, forgejo workflow 2026-05-20 19:52:00 -04:00
.forgejo/workflows ci: drop browser tests from CI pipeline (#3) 2026-05-25 14:43:49 +00:00
db feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
dev feature/ollama-fix (#13) 2026-05-26 04:20:25 +00:00
docs feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
example feat(ui): warm cinema theme — amber accent, espresso palette, editorial copy 2026-05-21 21:56:00 -04:00
scripts fix(docker): run tailwindcss after COPY src, not during npm ci (#9) 2026-05-25 15:23:24 +00:00
src feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
tests feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
.env.example feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
.gitignore feat/no-external-cdn (#6) 2026-05-25 15:06:03 +00:00
.npmrc feat(wip): initial implementation 2026-05-20 17:11:15 -04:00
.nvmrc feat(wip): initial implementation 2026-05-20 17:11:15 -04:00
AGENTS.md feat/rich-card-metadata (#11) 2026-05-25 19:55:59 +00:00
biome.json feat/no-external-cdn (#6) 2026-05-25 15:06:03 +00:00
CLAUDE.md feat(rec): per-card member interest UI + audience staleness fixes (#19) 2026-05-27 05:46:42 +00:00
Dockerfile fix(docker): copy tailwind.config.js and build CSS after COPY src/ (#10) 2026-05-25 15:42:01 +00:00
Dockerfile.ci fix(logging): demote per-iteration cron logs from info to debug (#1) 2026-05-25 02:58:39 +00:00
knexfile.ts fix(nav): narrow-viewport layout + parallel snapshot baselines 2026-05-24 00:46:03 -04:00
lefthook.yml feat(wip): initial implementation 2026-05-20 17:11:15 -04:00
LICENSE chore: add MIT license 2026-05-22 02:08:40 -04:00
package-lock.json chore: bump version to v0.3.0 2026-05-26 05:42:39 +00:00
package.json chore: bump version to v0.3.0 2026-05-26 05:42:39 +00:00
playwright.config.ts feat(tests): parallel browser matrix via snapshot-based dev stacks 2026-05-23 02:12:44 -04:00
playwright.parallel.config.ts wip: parallel test fixes — teardown, configurable workers, health url 2026-05-23 02:51:37 -04:00
README.md fix(tests): fix 5 desktop-safari failures + add 2 new Chromium device tiers 2026-05-22 20:13:35 -04:00
tailwind.config.js feat/no-external-cdn (#6) 2026-05-25 15:06:03 +00:00
tsconfig.json feat(wip): initial implementation 2026-05-20 17:11:15 -04:00
vitest.config.ts feature/ollama-fix (#13) 2026-05-26 04:20:25 +00:00
vitest.live.config.ts fix(tests): fix Vitest 4 poolOptions deprecation and loosen play-count assertion 2026-05-22 00:21:45 -04:00

watchcraft

Self-hosted media recommendation app for Jellyfin libraries. Rate titles from your library, discover similar content via TMDB, and optionally request media through Jellyseerr or get one-line AI explanations via Ollama.

Features

  • Star-rating UI for your Jellyfin library
  • Discovery feed powered by TMDB recommendations
  • Request buttons via Seerr / Jellyseerr (optional)
  • One-sentence AI explanations via Ollama (optional)
  • Multi-user support with shared audiences
  • Three auth modes: local accounts, SSO proxy header, or single implicit user
  • SQLite (zero-config), PostgreSQL, or MariaDB

Requirements

Quick start

cp docker-compose.yml docker-compose.local.yml  # or edit in place
# Fill in JELLYFIN_URL, JELLYFIN_API_KEY, TMDB_API_KEY, SESSION_SECRET
docker compose up -d
# Open http://localhost:3000 — setup wizard runs on first visit (local auth)

Generate a session secret:

openssl rand -hex 32

Configuration

Required

Variable Description
JELLYFIN_URL Base URL of your Jellyfin instance (e.g. http://jellyfin:8096)
JELLYFIN_API_KEY Jellyfin API key
TMDB_API_KEY TMDB v3 API key (free tier is sufficient)
SESSION_SECRET Random string for signing session cookies — use a long random value in production

Auth

Variable Default Description
AUTH_MODE local local | proxy | none
AUTH_HEADER X-authentik-username Header to read username from when AUTH_MODE=proxy

Auth modes:

  • local — bcrypt password auth with Express sessions. A setup wizard runs on first visit; the first account created is always admin.
  • proxy — reads the username from AUTH_HEADER. Use with Authentik, Authelia, or any other SSO proxy. Set AUTH_MODE=proxy and AUTH_HEADER to match your proxy's header name.
  • none — single implicit user named default. For trusted-network-only deployments with no login UI.

Database

Variable Default Description
DB_CLIENT better-sqlite3 better-sqlite3 | pg | mysql2
DB_SQLITE_PATH ./data/db.sqlite SQLite file path (SQLite only)
DB_HOST Hostname (PostgreSQL / MariaDB)
DB_PORT Port (PostgreSQL / MariaDB)
DB_NAME Database name
DB_USER Database user
DB_PASSWORD Database password

SQLite is zero-config and the default. PostgreSQL and MariaDB are useful if you want to run multiple watchcraft users off a shared database instance.

Optional integrations

Variable Description
SEERR_URL Seerr / Jellyseerr base URL — enables Request buttons on discovery cards
SEERR_API_KEY Seerr API key
OLLAMA_URL Ollama base URL — enables one-sentence recommendation explanations
OLLAMA_MODEL Ollama model to use (default: llama3.2:3b)

Cache TTLs

All intervals have sensible defaults. Override if needed.

Variable Default Description
CACHE_LIBRARY_HOURS 6 How often to re-sync the Jellyfin library
CACHE_HISTORY_HOURS 1 How often to re-sync play history
CACHE_TMDB_DAYS 30 How long TMDB recommendation data is cached
CACHE_METADATA_DAYS 90 How long TMDB metadata is cached
CACHE_SEERR_MINUTES 15 How often to poll Seerr for request status
CACHE_MAX_REC_LINKS 50000 Max recommendation graph edges before cache pruning

Misc

Variable Default Description
PORT 3000 HTTP port
TZ Etc/UTC Timezone (used for cron scheduling)
SESSION_STORE_PATH ./data/sessions.sqlite Path for the session SQLite store
LOG_LEVEL info trace | debug | info | warn | error | fatal
LOG_FORMAT pretty pretty (human-readable) | json (for log aggregators)

Data & persistence

The app writes two files to /app/data:

  • db.sqlite — application database
  • sessions.sqlite — session store

Mount /app/data to a persistent volume so data survives container restarts. The example docker-compose.yml uses a named Docker volume (watchcraft-data).

Database migrations run automatically on every startup — no manual migration step needed.

Upgrading

docker compose pull
docker compose up -d

Migrations run on startup; no other steps required.

Health check

GET /health returns 200 OK when the app is ready.

Development

Requirements

  • Node.js 24 LTS (use fnm or nvm)
  • Docker (for dev services: Jellyfin, Seerr, Postgres, MariaDB)

Setup

nvm use   # or: fnm use
npm install
cp .env.example .env
# Fill in TMDB_API_KEY (required), others optional for dev
npm run dev:up        # start Docker dev services
npm run dev:bootstrap # configure Jellyfin, write .env.dev
npm run dev           # start watchcraft in watch mode (uses .env.dev)

Tests

npm test                    # unit + integration (SQLite)
npm run test:all-dbs        # unit + integration (SQLite + Postgres + MariaDB)

Browser tests (Playwright)

One-time host setup (requires system package manager access for Chromium and Firefox):

npx playwright install chromium
npx playwright install-deps chromium
npx playwright install webkit
npx playwright install firefox
npx playwright install-deps firefox   # needs sudo on Linux/WSL

WebKit ships its own dependencies — no install-deps call needed for webkit.

npm run test:browser                   # all 9 device tiers (~18 min)
npm run test:browser:desktop           # Chromium: Desktop Chrome
npm run test:browser:desktop-safari    # WebKit: Desktop Safari
npm run test:browser:tablet            # WebKit: iPad Mini
npm run test:browser:mobile            # WebKit: iPhone 14
npm run test:browser:android-phone     # Chromium: Pixel 7
npm run test:browser:android-tablet    # Chromium: Galaxy Tab S9
npm run test:browser:firefox-desktop   # Firefox: desktop
npm run test:browser:firefox-tablet    # Firefox: tablet viewport
npm run test:browser:firefox-phone     # Firefox: phone viewport
npm run test:browser:update            # refresh visual-regression baselines

Each per-device command is self-contained: it resets the dev stack, starts the server, authenticates, runs tests, and stops the server (~2 min per tier).