Skip to content

Commit 9fb7f0d

Browse files
authored
Release 0.5.0 (#46)
* Add incremental scheduler (#43) * feat(update-cache): incremental updates — add last_updated, TTL, and isStale; update only stale/missing entries; add tests and utils * docs(env): unify cache TTL env var to CACHE_TTL (seconds), remove duplicate vars * feat: replace JSON file cache with SQLite persistent storage (#44) * Add filmaffinity rating badges to posters image overlay (#45) * feat(ui): add poster badge in films and series when you starts service schedule in docker * fix: now download badge of series correctly and add schedule script for compose
2 parents 8a03933 + 3b01fc9 commit 9fb7f0d

20 files changed

Lines changed: 2085 additions & 302 deletions

.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,27 @@ SYNC_JELLYFIN_FORCE=false
4646
SYNC_JELLYFIN_PAGE_SIZE=100
4747
# Comma-separated include types for iteration: Movie,Series,Episode
4848
SYNC_JELLYFIN_INCLUDE_ITEM_TYPES=Movie
49+
50+
# Poster badge rendering (works for web, TV and mobile clients because poster is replaced server-side)
51+
# Enable poster processing and upload back to Jellyfin.
52+
ENABLE_POSTER_BADGES=false
53+
# Badge position: top-right, top-left, bottom-right, bottom-left
54+
POSTER_BADGE_POSITION=top-right
55+
# Badge width ratio relative to poster width (recommended 0.12 - 0.35)
56+
POSTER_BADGE_SIZE=0.2
57+
58+
# Incremental cache updater tuning
59+
# Default TTL for cache entries that are not recent. The canonical variable is
60+
# `CACHE_TTL` (seconds, existing above). If you prefer days, set `CACHE_TTL` to
61+
# the equivalent number of seconds (e.g. 30 days = 2592000).
62+
# RECENT_TTL_DAYS controls the TTL (days) for recent releases (more frequent refresh)
63+
RECENT_TTL_DAYS=7
64+
# Number of years considered "recent" (used to apply RECENT_TTL_DAYS)
65+
RECENT_YEARS=2
66+
# Milliseconds to wait between individual FilmAffinity requests (scraping).
67+
# Increase to lower scraping load and reduce rate-limit risk.
68+
REQUEST_DELAY_MS=5000
69+
70+
# Path to SQLite DB file used for persistent ratings storage
71+
# Relative to project root, e.g. `data/ratings.db`
72+
DB_PATH=data/ratings.db

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
.env
3-
.cache
3+
.cache
4+
data/ratings.json
5+
data/ratings.db

DOCKER.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Environment variables reference
2727
- `JELLYFIN_BASE_URL`: your Jellyfin base URL (e.g. `http://192.168.1.31:8096`).
2828
- `JELLYFIN_API_KEY`: your Jellyfin API key.
2929
- `JELLYFIN_AUTH_MODE`: auth mode for Jellyfin client (`auto`|`header`|`query`).
30+
- `ENABLE_POSTER_BADGES`: enable FilmAffinity badge overlay and poster upload (`true`/`false`, default `false`).
31+
- `POSTER_BADGE_POSITION`: badge position (`top-right` default, also `top-left`, `bottom-right`, `bottom-left`).
32+
- `POSTER_BADGE_SIZE`: badge width ratio relative to poster width (default `0.2`).
3033
- `PUPPETEER_EXECUTABLE_PATH` : optional — set only if you want to use a system-installed Chromium.
3134
- `DEBUG_SCREENSHOTS` : set to `true` to persist debug screenshots under the `data/` folder inside the container (disabled by default).
3235

README.md

Lines changed: 46 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
## 🚀 Features
1313

1414
* 🎯 Reliable scraping using **Puppeteer + Stealth Plugin**
15-
* 💾 Daily-generated local cache (`data/ratings.json`)
15+
* 💾 Persistent SQLite storage (`data/ratings.db`) with in-memory NodeCache layer
1616
* ⚡ Fast in-memory caching with NodeCache
1717
* 🔌 Simple REST API endpoint: `/movie?title=...&year=...`
1818
* 🔗 Direct integration support with Jellyfin API
@@ -85,33 +85,31 @@ curl "http://localhost:3000/movie?title=Inception&year=2010"
8585

8686
## 🐳 Docker
8787

88-
See [DOCKER.md](DOCKER.md) for build and run instructions using Docker and Docker Compose.
88+
See [DOCKER.md](DOCKER.md) for build and runtime details.
8989

9090
**Published image**
9191

92-
Official images are published to GitHub Container Registry at `ghcr.io/cruzadera/filmaffinity-scores` by CI. To pull:
92+
Official images are published to GitHub Container Registry at `ghcr.io/cruzadera/filmaffinity-scores` by CI.
9393

9494
```bash
9595
docker pull ghcr.io/cruzadera/filmaffinity-scores:latest
9696
```
9797

98-
## ⏰ Scheduler service
99-
100-
The `docker-compose.yml` includes a `scheduler` service that performs an initial full library cache population by running `node src/scripts/updateCache.js` on startup, then runs the same update once per day. The scheduler shares the `./data` volume so the generated `data/ratings.json` is persisted and available to the API container.
101-
102-
You can control the interval with the `SLEEP_SECONDS` environment variable (default `86400` seconds = 24h). To run the scheduler with Docker Compose:
98+
Run scheduler only:
10399

104100
```bash
105101
docker compose up -d scheduler
106102
```
107103

108-
Or start the full stack (API + scheduler):
104+
The scheduler runs `updateCache` and then `sync-jellyfin` on startup and each cycle, so poster badges are updated automatically when enabled.
105+
It is implemented in `scripts/scheduler.js` and can force the first sync pass on startup to fill missing poster badges.
106+
107+
Run full stack:
109108

110109
```bash
111110
docker compose up -d
112111
```
113112

114-
115113
---
116114

117115
## 🔗 Integrations
@@ -121,168 +119,71 @@ This service is designed to work with:
121119
* Jellyfin
122120
* Jellyseerr
123121

124-
Future integrations may include:
125-
126-
* Kodi
127-
* Plex
128-
129122
---
130123

131-
## 🔌 Jellyfin integration (client)
132-
133-
This project includes a reusable Jellyfin API client at [src/services/jellyfinClient.js](src/services/jellyfinClient.js). The client is configurable via constructor options or environment variables and supports both API key header auth and query-based `ApiKey` authentication.
124+
## 🔌 Jellyfin Sync + Poster Badges
134125

135-
- File: [src/services/jellyfinClient.js](src/services/jellyfinClient.js)
126+
Main scripts:
136127

137-
Environment variables (or pass as options):
128+
* `scripts/scheduler.js` (scheduler loop: cache + sync)
129+
* `scripts/sync-jellyfin.js` (recommended)
130+
* `src/scripts/updateJellyfin.js` (legacy updater)
138131

139-
- `JELLYFIN_BASE_URL`: base URL of your Jellyfin server (e.g. `http://192.168.1.31:8096`)
140-
- `JELLYFIN_API_KEY`: Jellyfin API key
141-
- `JELLYFIN_TIMEOUT`: request timeout in milliseconds (optional)
142-
- `JELLYFIN_AUTH_MODE`: authentication mode (`auto`|`header`|`query`) — default is `auto`
132+
Required env vars:
143133

144-
### Jellyfin configuration via environment variables
145-
146-
You can configure the Jellyfin integration purely via environment variables. The application validates required values and will fail with a clear message if they are missing when strict validation is used (for example by scripts).
147-
148-
- `JELLYFIN_BASE_URL` (required): base URL of your Jellyfin server (e.g. `http://192.168.1.31:8096`).
149-
- `JELLYFIN_API_KEY` (required): Jellyfin API key.
150-
- `JELLYFIN_USER_ID` (optional): a Jellyfin user id to scope queries.
151-
- `JELLYFIN_AUTH_MODE` (optional): `auto`|`header`|`query` (default `auto`).
152-
- `JELLYFIN_TIMEOUT` (optional): request timeout in milliseconds.
153-
154-
Examples:
155-
156-
Set values in a `.env` file or export them before running scripts:
157-
158-
```bash
159-
JELLYFIN_BASE_URL=http://192.168.1.31:8096
160-
JELLYFIN_API_KEY=your_api_key_here
161-
```
134+
* `JELLYFIN_BASE_URL`
135+
* `JELLYFIN_API_KEY`
162136

163-
The sync and updater scripts use a centralized config helper which will throw a helpful error if `JELLYFIN_BASE_URL` or `JELLYFIN_API_KEY` are not set when strict validation is requested.
137+
Optional env vars:
164138

165-
### Setup Jellyfin API key
139+
* `JELLYFIN_AUTH_MODE`: `auto` | `header` | `query`
140+
* `SYNC_JELLYFIN_DRY_RUN`: `true`/`false`
141+
* `SYNC_JELLYFIN_FORCE_ON_STARTUP`: `true`/`false` (default `true`, used by `scripts/scheduler.js`)
142+
* `SYNC_JELLYFIN_BATCH_SIZE`, `SYNC_JELLYFIN_DELAY_MS`, `SYNC_JELLYFIN_RETRIES`, `SYNC_JELLYFIN_RETRY_DELAY`
143+
* `SYNC_JELLYFIN_PAGE_SIZE`, `SYNC_JELLYFIN_LIMIT`, `SYNC_JELLYFIN_INCLUDE_ITEM_TYPES`
166144

167-
To allow this service to update your Jellyfin metadata you need an API key (token) from your Jellyfin server:
145+
Notes:
168146

169-
1. Log in to Jellyfin web UI with an account that can create API keys (your personal user is fine).
170-
2. Open the user menu (top-right) and click your username, or go to **Dashboard → Users** and select your user.
171-
3. Find the **API Keys** or **API Keys / Manage API Keys** section and create a new key (give it a name like `filmaffinity-sync`).
172-
4. Copy the generated token and store it securely. Use it as the value for `JELLYFIN_API_KEY`.
147+
* Leave `SYNC_JELLYFIN_LIMIT` empty to process without limit.
173148

174-
If you prefer to use query-based auth, set `JELLYFIN_AUTH_MODE=query` and the library will send the token as `ApiKey` query parameter.
149+
Poster badge env vars:
175150

176-
### Example workflows
151+
* `ENABLE_POSTER_BADGES`: `true`/`false` (default `false`)
152+
* `POSTER_BADGE_POSITION`: `top-right` (default), `top-left`, `bottom-right`, `bottom-left`
153+
* `POSTER_BADGE_SIZE`: badge width ratio relative to poster width (default `0.2`)
177154

178-
1) Dry-run to preview changes (recommended first):
155+
Example dry-run:
179156

180157
```bash
181-
JELLYFIN_BASE_URL=http://192.168.1.31:8096 \
182-
JELLYFIN_API_KEY=xxxxxx \
183158
LOG_LEVEL=debug \
184159
SYNC_JELLYFIN_DRY_RUN=true \
185-
node scripts/sync-jellyfin.js --limit=5 --batch-size=2
160+
ENABLE_POSTER_BADGES=true \
161+
node scripts/sync-jellyfin.js --limit=5 --batch-size=2
186162
```
187163

188-
2) Apply a small real run (careful):
164+
Apply updates:
189165

190166
```bash
191-
JELLYFIN_BASE_URL=http://192.168.1.31:8096 \
192-
JELLYFIN_API_KEY=xxxxxx \
167+
LOG_LEVEL=debug \
193168
SYNC_JELLYFIN_DRY_RUN=false \
194-
node scripts/sync-jellyfin.js --limit=10 --batch-size=3
169+
ENABLE_POSTER_BADGES=true \
170+
POSTER_BADGE_POSITION=top-right \
171+
POSTER_BADGE_SIZE=0.2 \
172+
node scripts/sync-jellyfin.js --limit=20 --batch-size=2
195173
```
196174

197-
3) Sync only Series and use a page-size tuning example:
198-
199-
```bash
200-
JELLYFIN_BASE_URL=http://192.168.1.31:8096 \
201-
JELLYFIN_API_KEY=xxxxxx \
202-
node scripts/sync-jellyfin.js --include-item-types=Series --page-size=50 --limit=50
203-
```
204-
205-
Notes:
206-
207-
- Start with a dry-run and small `--limit` to confirm payloads look correct.
208-
- Use `SYNC_JELLYFIN_BATCH_SIZE` and `SYNC_JELLYFIN_DELAY_MS` to tune concurrency and avoid overloading your Jellyfin server.
209-
- If you get `401` errors, try `JELLYFIN_AUTH_MODE=query`.
210-
211-
212-
Auth behaviour:
213-
214-
- `auto` (default): the client sends the recommended `Authorization: MediaBrowser Token="..."` header and will retry once using the `ApiKey` query parameter if the server responds `401`.
215-
- `header`: always use the `Authorization: MediaBrowser ...` header.
216-
- `query`: always use the `ApiKey` query parameter and do not send the header.
217-
218-
Basic usage example (prefer query mode for API key-only setups):
219-
220-
```javascript
221-
const JellyfinClient = require('./src/services/jellyfinClient');
175+
Poster processing module:
222176

223-
const client = new JellyfinClient({
224-
baseUrl: process.env.JELLYFIN_BASE_URL,
225-
apiKey: process.env.JELLYFIN_API_KEY,
226-
authMode: 'query', // 'auto' | 'header' | 'query'
227-
});
228-
229-
async function demo() {
230-
const users = await client.getUsers();
231-
console.log('Users:', users);
232-
233-
const items = await client.getItems({ Limit: 10 });
234-
console.log('Items:', items);
235-
}
236-
237-
demo().catch(console.error);
238-
```
239-
240-
## 🔁 Update Jellyfin metadata from FilmAffinity
241-
242-
This project includes a helper script to update Jellyfin movie metadata with ratings retrieved from FilmAffinity. The script iterates your Jellyfin movie library, queries FilmAffinity for ratings and updates the Jellyfin item fields when necessary.
243-
244-
Run the updater (by default it runs in dry-run mode):
245-
246-
```bash
247-
npm run update-jellyfin
248-
```
249-
250-
Environment variables used by the updater and the newer `sync-jellyfin` script:
251-
252-
- `UPDATE_JELLYFIN_DRY_RUN` / `SYNC_JELLYFIN_DRY_RUN`: `true`/`false` — when `true` the script will only show the payload it would send to Jellyfin and will not perform updates. Default: `true`.
253-
- `UPDATE_JELLYFIN_SET_CRITIC` / `SYNC_JELLYFIN_SET_CRITIC`: `true`/`false` — when `true` the updater will also set the `CriticRating` field (optional) if available from FilmAffinity. Default: `false`.
254-
- `UPDATE_JELLYFIN_FORCE` / `SYNC_JELLYFIN_FORCE`: `true`/`false` — when `true` the updater will skip fetching existing metadata and forcibly apply updates. Default: `false`.
255-
- `UPDATE_JELLYFIN_PAGE_SIZE` / `SYNC_JELLYFIN_PAGE_SIZE`: integer — page size used when iterating items (default `50` for updater, `100` for sync script).
256-
- `SYNC_JELLYFIN_LIMIT`: integer — maximum number of items to process (useful for testing). Default: no limit.
257-
- `SYNC_JELLYFIN_BATCH_SIZE`: integer — number of concurrent tasks per batch (default `5`).
258-
- `SYNC_JELLYFIN_DELAY_MS`: integer — milliseconds to wait between batches (default `500`).
259-
- `SYNC_JELLYFIN_RETRIES`: integer — number of retries for transient operations (default `3`).
260-
- `SYNC_JELLYFIN_RETRY_DELAY`: integer — base retry delay in milliseconds for exponential backoff (default `1000`).
261-
- `SYNC_JELLYFIN_INCLUDE_ITEM_TYPES`: comma-separated list — include item types to iterate, e.g. `Movie,Series,Episode` (default `Movie`).
262-
263-
Examples
264-
265-
Dry-run (show payloads, do not apply updates):
266-
267-
```bash
268-
LOG_LEVEL=debug SYNC_JELLYFIN_DRY_RUN=true node scripts/sync-jellyfin.js --limit=1 --batch-size=1 --page-size=1
269-
```
270-
271-
Apply real updates (be careful):
272-
273-
```bash
274-
LOG_LEVEL=debug SYNC_JELLYFIN_DRY_RUN=false node scripts/sync-jellyfin.js --limit=1 --batch-size=1 --page-size=1
275-
```
276-
277-
Sync only Series example:
278-
279-
```bash
280-
node scripts/sync-jellyfin.js --include-item-types=Series --limit=10
281-
```
177+
* `src/services/posterProcessor.js`
178+
* `downloadPoster(client, itemId)`
179+
* `generateBadge(rating, options)`
180+
* `applyOverlay(posterBuffer, badgeBuffer, options)`
181+
* `uploadPoster(client, itemId, posterBuffer)`
182+
* `processMoviePoster(client, movie, faData, cacheEntry, options)`
282183

283-
Testing
184+
The processor stores `last_rating` and `poster_processed` in SQLite and skips regeneration when unchanged.
284185

285-
Set `JELLYFIN_BASE_URL` and `JELLYFIN_API_KEY` in your `.env` and run:
186+
Testing:
286187

287188
```bash
288189
npm test

docker-compose.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: "3.8"
2-
31
services:
42
app:
53
build:
@@ -20,5 +18,4 @@ services:
2018
volumes:
2119
- ./data:/app/data
2220
restart: unless-stopped
23-
# Run an initial full cache update, then run update once per day
24-
command: sh -c "node src/scripts/updateCache.js || true; while true; do sleep ${SLEEP_SECONDS:-86400}; node src/scripts/updateCache.js || true; done"
21+
command: node scripts/scheduler.js

0 commit comments

Comments
 (0)