尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Daily movie, TV, and book recommendations — movies and TV from your Plex library, books from your Goodreads want-to-read shelf — chosen by Gemini (on Vertex AI). This is an experiment in building an app with generative AI.
Stack: Go, Chi (routing), GORM (ORM), Postgres, Gemini on Vertex AI (google.golang.org/genai), zap + gutil/logging (JSON logs), OpenTelemetry (HTTP metrics on /metrics).
The home page shows one set of recommendations per calendar day:
ViewCount == 0).Each card shows poster, title, year, rating, genre, and runtime (movies), season count (TV), or author and page count (books). A tier's heading is omitted when it has no picks, so books simply don't appear unless Goodreads is configured.
Past days are listed at /dates (one row per distinct day, paginated).
rating and fall back to audienceRating, which is the only score Plex sets on TV shows/review/list_rss/{userID}), since the official API was retired in December 2020 and issues no new keysmetacritic.com/tv/<show>/season-N/| Method | Path | Description |
|---|---|---|
| GET | / | Today’s recommendations (UTC date) |
| GET | /date/YYYY-MM-DD | Recommendations for that day |
| GET | /dates | Paginated list of days (?page, ?size) |
| GET | /cron/recommend | Start recommendation generation (async; file lock) |
| GET | /cron/cache | Refresh Plex → Postgres cache (async; file lock) |
| GET | /stats | DB statistics |
| GET | /health | JSON health including DB ping |
| GET | /metrics | Prometheus exposition (otelhttp HTTP server metrics) |
| GET | /static/* | Embedded static files (e.g. favicon) |
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | yes | Postgres connection string, e.g. postgres://user:pass@host:5432/recommender?sslmode=disable |
PLEX_URL | yes | Plex server base URL |
PLEX_TOKEN | yes | Plex token |
GOOGLE_CLOUD_PROJECT | yes | GCP project ID (Vertex AI API enabled) |
GOOGLE_CLOUD_LOCATION | yes | Vertex AI region, e.g. us-central1 |
GOOGLE_GENAI_USE_VERTEXAI | no | true to use Vertex AI (recommended); the SDK also supports the Gemini Developer API |
GEMINI_MODEL | no | Model ID (default gemini-2.5-flash) |
GEMINI_API_KEY | no | When set, call the Gemini API with this key instead of Vertex AI, e.g. through a LiteLLM proxy set by GOOGLE_GEMINI_BASE_URL |
GOOGLE_APPLICATION_CREDENTIALS | no | Path to a service-account key for local dev; production uses ambient ADC (workload identity) |
TRAKT_CLIENT_ID | no | Trakt API app client id; enables Trakt signals |
TRAKT_CLIENT_SECRET | no | Trakt API app client secret |
TRAKT_CONNECT_TOKEN | no | Shared secret required to call GET /trakt/connect?token=…; the endpoint is disabled when unset |
ANILIST_USERNAME | no | AniList username (public list); enables AniList signals |
OMDB_API_KEY | no | OMDb key; enables Metacritic Metascores |
OMDB_BATCH_SIZE | no | OMDb lookups per cache run (default 40, sized for OMDb's free 1000/day quota) |
GOODREADS_USER_ID | no |
Authentication to Vertex AI uses Application Default Credentials — no API key. Locally, run gcloud auth application-default login or set GOOGLE_APPLICATION_CREDENTIALS.
External sources only re-rank titles you already own in Plex — they never add new titles. Each is off unless configured, and all are synced during /cron/cache.
TRAKT_CLIENT_ID/TRAKT_CLIENT_SECRET and a TRAKT_CONNECT_TOKEN, then authorize once — curl "http://localhost:8080/trakt/connect?token=$TRAKT_CONNECT_TOKEN" and enter the returned code at the Trakt URL. Tokens persist in the DB and auto-refresh.ANILIST_USERNAME (public list; no auth). Matched to owned anime by title + year.GOODREADS_USER_ID to the number in your profile URL — goodreads.com/user/show/12680-nat → 12680. This is a different namespace from the author id on /author/show/<id>, which will silently return someone else's shelves. The profile must be public; no auth or key is needed. Unlike the other sources, Goodreads doesn't just re-rank — the want-to-read shelf is the book candidate pool.OMDB_API_KEY. Metacritic has no public API, so scores come from OMDb, joined on the IMDb ID Plex already provides. Enrichment is incremental: each cache run looks up OMDB_BATCH_SIZE titles that have never been checked, so the library backfills over a few days inside the free tier. A title that comes back with a score is never re-fetched — critic scores are final once a title is released. Titles Metacritic doesn't cover are stamped too and retried only after 90 days, to catch a late score without re-spending the quota every run. A run is also time-boxed, so a slow OMDb defers titles to the next run rather than holding the shared cron lock past the point where /cron/recommend can acquire it.Signals feed genre affinity, a watchlist score boost, watched-elsewhere handling, and a short "recently loved" line in the prompt.
The Metascore is used three ways: it adds a modest term to candidate scoring (centered on 55, so an unscored title ranks the same as an average one rather than being penalized), it appears on the shortlist lines sent to Gemini, and it renders on each card as a colored badge linking to metacritic.com. That link is built from the title slug — Metacritic exposes no ID to join on — so it is only published for titles that actually have a Metascore.
recommender/
├── handlers/ # HTTP handlers and HTML templates (embedded)
├── lib/
│ ├── db/ # Migrations and GORM logger
│ ├── goodreads/ # Goodreads shelf client (RSS)
│ ├── health/ # Health check
│ ├── lock/ # File locks for cron endpoints
│ ├── metacritic/ # metacritic.com link building
│ ├── omdb/ # OMDb client (Metacritic Metascores)
│ ├── plex/ # Plex client and cache update
│ ├── recommend/ # Gemini generation, candidate scoring, and queries
│ └── validation/ # Request and response validation helpers
├── models/ # GORM models
├── static/ # Assets embedded into the binary (e.g. favicon)
└── data/ # Docker volume mount target for the DB (optional locally)
Package docs: pkg.go.dev/go.icco.me/recommender.
gcloud auth application-default login # or set GOOGLE_APPLICATION_CREDENTIALS
export DATABASE_URL=postgres://recommender:recommender@localhost:5432/recommender?sslmode=disable
export PLEX_URL=... PLEX_TOKEN=...
export GOOGLE_GENAI_USE_VERTEXAI=true GOOGLE_CLOUD_PROJECT=... GOOGLE_CLOUD_LOCATION=us-central1
go run .
Optional: POSTER_DIR=/path/to/posters. Need a local Postgres? docker compose up -d postgres.
docker compose up -d
Open http://localhost:8080. Trigger cache then recommendations:
curl -sS "http://localhost:8080/cron/cache"
curl -sS "http://localhost:8080/cron/recommend"
Logs: docker compose logs -f. Stop: docker compose down.
The compose file runs a bundled postgres:17 service (data in the pgdata volume) and mounts ./data at /data for cached posters (POSTER_DIR=/data/posters).
/cron/cache — Reads Plex libraries and stores all movies and TV shows in Postgres, including view_count, GUIDs (imdb/tmdb/tvdb), and the full genre list. Poster thumbs are stored as absolute URLs when Plex returns relative paths. External signals sync afterward, including a capped batch of OMDb Metascore lookups./cron/recommend — Skips if a successful run already exists for the UTC day. Otherwise: loads cached titles (minus anything recommended in the last 30 days), scores them (rating + novelty + Plex-derived taste affinity + Metacritic reception), takes a date-seeded diverse shortlist, asks Gemini to pick the best fits by ID with a one-line reason, slots them deterministically (comedy / action-drama / rewatch / wildcard movies + unwatched TV), caches the finalists' Plex posters locally, and replaces that day's rows in one transaction. Every attempt records a GenerationRun.A day is "done" when a GenerationRun with status ok exists for it — tracked explicitly rather than inferred from row counts, so cron never re-runs a completed day.
/cron/cache, /cron/recommend, and /metrics are unauthenticated. The cron endpoints trigger paid Gemini calls and full Plex/Trakt/AniList syncs, so anyone who can reach them can drive cost and load. Restrict /cron/* and /metrics to trusted callers at the ingress/reverse proxy (source-IP allow-list or an internal-only route); the app does not gate them itself.X-Plex-Token to the configured Plex host, so an absolute thumb URL pointing off-host cannot exfiltrate the token or be used for SSRF with credentials.gorm.ParamsFilter), so SQL is logged with placeholders and secrets like Trakt tokens never land in logs.| Goodreads numeric user id; enables the books tier |
PORT | no | HTTP port (default 8080) |
POSTER_DIR | no | Directory for locally cached Plex posters (default posters; Docker Compose uses /data/posters) |