Node.js service for:
- Retrieving FilmAffinity ratings (Puppeteer-based scraping).
- Exposing ratings through a REST API.
- Persisting cache in SQLite.
- Providing a stable HTTP contract for external integrations such as Jellyfin sync.
- Serving as API-only provider for external worker consumers.
This repository is API-only. Worker execution is handled externally in https://github.com/Cruzadera/fa-jellyfin-sync
- HTTP API for movie lookup by title/year.
- HTTP batch API contract for external integrations.
- In-memory cache (node-cache) + persistent cache (data/ratings.db).
- Scraper powered by puppeteer-extra + stealth plugin.
- npm start: starts the API (src/index.js).
- POST /ratings/batch is the main integration contract for an external Jellyfin sync worker.
- Node.js 20+
- Access/network to this API from your external worker deployment
git clone https://github.com/Cruzadera/filmaffinity-scores.git
cd filmaffinity-scores
npm install
cp .env.example .envUpdate .env with real values before running sync tasks.
Start:
npm startDefault base URL: http://localhost:8085
Endpoints:
- GET /movie?title=...&year=...
- title is required
- year is required (4 digits)
- Returns 400/404/502 depending on validation, no result, or scraper errors
- GET /rating?title=...&year=...
- title is required
- year is optional
- GET /health
- POST /ratings/batch
- Content-Type: application/json
- body: { "items": [{ "title": "Alien", "year": "1979" }, ...] }
- title is required per item
- year is optional per item (if present, must be 4 digits)
- returns per-item status and data/error (partial success supported)
Batch example:
curl -X POST "http://localhost:8085/ratings/batch" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "title": "Alien", "year": "1979" },
{ "title": "Blade Runner", "year": "1982" },
{ "title": "Unknown Movie" }
]
}'Typical batch response:
{
"count": 3,
"ok": 2,
"failed": 1,
"results": [
{
"ok": true,
"status": 200,
"source": "db",
"input": { "title": "alien", "year": "1979" },
"data": {
"title": "Alien",
"year": "1979",
"rating": 8.1,
"votes": "123456",
"url": "https://www.filmaffinity.com/es/film123456.html"
}
},
{
"ok": false,
"status": 404,
"error": "No result found",
"input": { "title": "unknown movie", "year": null }
}
]
}Example:
curl "http://localhost:8085/movie?title=Alien&year=1979"Typical response:
{
"title": "Alien",
"year": "1979",
"rating": 8.1,
"votes": "123456",
"url": "https://www.filmaffinity.com/es/film123456.html"
}Worker/scheduler logic is out of scope for this repository and lives in:
See .env.example for full details.
- General:
-
PORT (default 8085)
-
LOG_LEVEL (debug|info|warn|error, default info)
-
DB_PATH (default data/ratings.db)
-
BATCH_MAX_ITEMS (default 100) — max items accepted by POST /ratings/batch
-
CACHE_TTL in seconds (default 86400) — canonical value for the in-memory cache (node-cache). This takes precedence if set. The updater derives its day-based defaults from this value and from the
RECENT_*settings. -
RECENT_TTL_DAYS (default 7) — TTL in days applied to recent releases (they are refreshed more often).
-
RECENT_YEARS (default 2) — how many years are considered "recent".
-
Quick notes:
-
The API process configures NodeCache with a global
CACHE_TTL(seconds), but entries are set with per-entry TTLs based on movie year so recent movies are refreshed more often while older movies are cached longer. -
Jellyfin:
- Not used by this API-only repository.
-
Scraping/cache:
- REQUEST_DELAY_MS
- RECENT_TTL_DAYS
- RECENT_YEARS
- DEBUG_SCREENSHOTS
One service is defined in docker-compose.yml:
- app: REST API and canonical ratings provider
Start full stack:
docker compose up -d --buildThe ./data:/app/data volume persists SQLite data.
npm test- Scraping can break if FilmAffinity changes HTML or anti-bot behavior.
- The main contract to preserve is
POST /ratings/batch; it is intended to survive the Jellyfin split. - Jellyfin updates depend on API key permissions.
- In production, validate with dry-run before enabling write mode.
This project is intended for personal/self-hosted and educational use. Please respect third-party terms of use.
See LICENSE.