尚未偵測到更新。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) |