Aggiunge a Seerr due filtri che l'upstream non offre: voto IMDb e numero di voti IMDb, come slider nella pagina Scopri, sopra i filtri TMDB esistenti.
Il mod è un wrapper Docker: parte dall'immagine ufficiale di Seerr, ricostruisce il frontend con gli slider aggiunti e applica una patch al backend compilato all'avvio del container.
Stato: verificato su Seerr v3.4.1, 12 route di discover filtrate.
Browser: slider IMDb → query params imdbRatingGte / imdbRatingLte /
imdbVoteCountGte / imdbVoteCountLte
│
▼
seerr-api.yml i 4 parametri sono dichiarati su 12 endpoint, altrimenti
express-openapi-validator risponde 400 prima di tutto
│
▼
imdb-route-wrapper.js middleware Express agganciato alle 12 route
│ ├─ se nessun filtro IMDb è attivo → passa oltre, costo zero
│ ├─ spinge a TMDB un pre-filtro (vote_count >= 100, anno >= 1965)
│ ├─ rimappa la pagina frontend su una finestra di 10 pagine TMDB
│ └─ se i risultati non bastano, chiede altre pagine via HTTP loopback
│
▼
imdb-filter.js risoluzione TMDB → IMDb e lookup del voto
│ ├─ cache locale tmdb_cache.db (SQLite, read-write)
│ ├─ mediaInfo di Seerr, quando presente
│ └─ TMDB /external_ids, in parallelo e con concorrenza limitata
│
▼
imdb_ratings.db dataset pubblico IMDb (read-only, ~1,7 M titoli)
1. Pre-filtro su TMDB (economico). Quando un filtro IMDb è attivo, il
middleware aggiunge alla query TMDB voteCountGte=100 e una data minima
(1965-01-01). Sono titoli che non possono realisticamente soddisfare una soglia
IMDb, e scartarli a monte evita di risolvere centinaia di id per nulla. È una
scelta deliberata di prestazioni: restringe il bacino prima che qualsiasi lookup
IMDb avvenga. Entrambi i valori sono configurabili.
Un caso merita una nota, perché la calibrazione è controintuitiva. Con
sortBy=vote_average.asc TMDB restituisce intere pagine di titoli esattamente
al valore del floor richiesto. Un floor molto sotto la soglia IMDb produce
quindi pagine che non possono contenere nulla: misurato sul catalogo reale, con
soglia IMDb 7.5 un floor TMDB di 6,5 mette 0 candidati su 20 sopra soglia, 7,0 ne
mette 2, 7,5 ne mette 14. In quel caso il floor TMDB viene quindi allineato alla
soglia IMDb invece di seguirla a distanza.
2. Filtro IMDb (autorevole). Per ogni candidato si risolve l'id IMDb e si
legge averageRating / numVotes dal dataset. Un titolo viene scartato se:
| Motivo | Significato |
|---|---|
no_imdb_id |
TMDB non espone un id IMDb per quel titolo |
no_rating |
il titolo non ha una riga nel dataset IMDb |
rating |
voto fuori dall'intervallo richiesto |
votes |
numero di voti fuori dall'intervallo richiesto |
no_rating merita una nota. Il dataset IMDb contiene ogni titolo con più di
qualche voto, quindi una riga mancante significa davvero che il titolo sta
sotto il minimo dello slider (che parte da 100 voti), non che i nostri dati
siano incompleti. Questo è vero solo se il dataset è fresco: è esattamente il
punto in cui la versione precedente falliva, escludendo film con 300.000 voti
IMDb perché il database locale era fermo da mesi.
Una pagina di 20 risultati TMDB, dopo il filtro, può ridursi a 2. Il middleware recupera quindi altre pagine TMDB, in parallelo a gruppi di 3, risolvendo gli id IMDb di tutto il gruppo in un unico batch, finché non ha di nuovo 20 risultati o esaurisce la finestra.
Due dettagli che valgono più di quanto sembri:
Le pagine extra si chiedono alla stessa route, via HTTP su loopback, con un
header x-imdb-bypass, invece di ricostruire a mano i parametri TMDB. Così le
pagine compensate usano esattamente la stessa semantica della prima, incluse le
opzioni che una lista scritta a mano dimenticherebbe (era il caso di
certification, certificationCountry e certificationMode). Il ramo bypass
del middleware riapplica lo stesso pre-filtro dall'altro lato, quindi le pagine
compensate restano coerenti con la prima.
L'allargamento si ferma presto quando non rende. Alcune combinazioni sono intrinsecamente sparse — i titoli "upcoming" non hanno ancora voti IMDb — e continuare a scandagliare la finestra non aggiunge nulla. Dopo due blocchi consecutivi che non producono risultati utili la scansione si interrompe:
| Route | Prima | Dopo |
|---|---|---|
/movies/studio/2 |
8,5 s | 0,7 s |
/movies/upcoming |
13,0 s | 0,3 s |
/tv/upcoming |
14,5 s | 0,6 s |
Stessi risultati restituiti, solo senza il lavoro inutile.
La pagina frontend N copre le pagine TMDB [10(N-1)+1 .. 10N], quindi pagine
consecutive non si sovrappongono. TMDB rifiuta page oltre 500: il numero di
pagine esposto è quindi limitato a 50 e le pagine successive rispondono vuote
invece che con un errore 500.
totalResults resta la dimensione del bacino di candidati: nessuno può
sapere quanti titoli soddisfino una soglia IMDb senza percorrere l'intero
catalogo.
Tre decisioni non ovvie, ognuna conseguenza di un bug reale.
imdb_ratings.db contiene solo i rating. L'app lo apre in sola lettura;
imdb-sync.sh ne costruisce uno nuovo e lo pubblica con una mv atomica. Il
file è in modalità delete, quindi non ha file -wal/-shm che possano
sopravvivere allo swap.
La versione precedente teneva rating e cache nello stesso file, in modalità WAL,
e lo sostituiva con mv: il -wal della generazione precedente restava sotto lo
stesso nome e veniva riapplicato al file nuovo. SQLite risponde
database disk image is malformed. È successo davvero, il 2026-09-16.
Sostituire invece la tabella in place non è una alternativa valida: SQLite
richiede un lock esclusivo per i cambi di schema, quindi i lettori concorrenti
ricevono SQLITE_BUSY e lo swap stesso può fallire con database is locked.
tmdb_cache.db è scritto dall'app e non viene mai sostituito. È questa
separazione che rende sicuro lo swap descritto sopra.
La cache ha chiave primaria composita (tmdbId, mediaType): gli id TMDB di
film e serie sono namespace distinti, quindi un film e una serie possono
condividere lo stesso id numerico. Con la chiave singola di prima si
sovrascrivevano a vicenda, producendo voti IMDb sbagliati. Verificato: per
l'id 155 la cache distingue correttamente tt0468569 (film) da tt0115082
(serie).
La cache viene popolata in anticipo dal warmer, perché l'API discover di TMDB
non restituisce mai imdb_id: senza warm-up ogni titolo mai visto costa una
chiamata extra.
patches/patch-seerr.js verifica che tutte e 12 le route attese esistano e
siano patchate. Un patch parziale è un fallimento, non un avviso. I backup sono
creati una sola volta, quindi rieseguire la patch non può sovrascrivere
l'originale con un file già patchato.
La versione precedente, a ogni riavvio, contava zero route da patchare su un container già patchato, usciva con codice 1, e l'entrypoint annunciava "starting Seerr without IMDB mod" — su un'app perfettamente funzionante. Il suo rollback aveva lo stesso difetto e non poteva ripristinare nulla.
.
├── Dockerfile wrapper: pin della release Seerr + build frontend
├── docker-compose.yml servizio seerr + servizio tools/warmup
├── entrypoint.sh installa i moduli, applica la patch, avvia Seerr
├── imdb-sync.sh scarica il dataset IMDb e pubblica imdb_ratings.db
├── imdb-warmup.js pre-popola la cache TMDB → IMDb
├── update.sh promozione verificata a una nuova release Seerr
├── patches/
│ └── patch-seerr.js patcha dist/routes/discover.js e seerr-api.yml
├── server-modules/
│ ├── imdb-filter.js accesso ai DB, risoluzione id, semantica filtro
│ └── imdb-route-wrapper.js middleware Express + compensazione pagine
├── overrides/ sorgenti frontend (upstream + slider IMDb)
│ ├── constants.ts
│ └── FilterSlideover/index.tsx
├── docs/ design e note di architettura
└── imdb-data/ volumi persistenti (gitignored)
├── imdb_ratings.db ~48 MB, read-only per l'app
└── tmdb_cache.db cache TMDB → IMDb, read-write
I file in overrides/ sono il contenuto upstream del tag più le sole aggiunte
IMDb: update.sh verifica a ogni aggiornamento che restino un superset pulito,
perché un override non più allineato reverebbe silenziosamente il lavoro upstream.
docker exec seerr /imdb-sync.shNessun riavvio necessario: lo swap è atomico e i lettori non vengono mai
bloccati. Il cron notturno (0 3 * * *) lo esegue già.
cd /mnt/user/appdata/seerr-imdb-mod
docker compose --profile tools run --rm warmup
docker compose --profile tools run --rm warmup --budget=5000 # più corto
docker compose --profile tools run --rm warmup --budget=40000 --genresIl warmer è cache-first: le voci già note non costano richieste, quindi le esecuzioni successive convergono e diventano rapide. ~18 richieste/s, quindi 20.000 lookup ≈ 18 minuti.
Cron settimanale: 0 5 * * 0.
docker logs seerr | head -20 # versione, patch, freschezza dataset
docker exec seerr sqlite3 "file:/imdb-data/imdb_ratings.db?mode=ro" \
"SELECT key, value FROM meta;"
docker exec seerr sqlite3 /imdb-data/tmdb_cache.db \
"SELECT mediaType, COUNT(*) FROM tmdb_to_imdb GROUP BY mediaType;"Variabili d'ambiente del servizio seerr:
| Variabile | Default | Significato |
|---|---|---|
IMDB_DEBUG |
0 |
1 logga una riga per pagina filtrata: tempi, candidati, motivi di scarto |
IMDB_WINDOW |
10 |
pagine TMDB per pagina frontend (determina il massimo di 50 pagine) |
IMDB_CHUNK |
3 |
pagine TMDB richieste in parallelo per riempire una pagina |
IMDB_MAX_EMPTY_CHUNKS |
2 |
blocchi consecutivi a resa zero prima di fermare l'allargamento |
IMDB_MIN_VOTE_COUNT |
100 |
pre-filtro TMDB sul numero di voti |
IMDB_MIN_YEAR |
1965 |
pre-filtro TMDB sull'anno |
IMDB_TMDB_CONCURRENCY |
12 |
chiamate /external_ids TMDB in parallelo |
IMDB_DB_PATH |
/imdb-data/imdb_ratings.db |
dataset rating |
IMDB_CACHE_PATH |
/imdb-data/tmdb_cache.db |
cache TMDB → IMDb |
IMDB_DEBUG=1 è lo strumento principale per capire un filtro che sembra
sbagliato:
[imdb-filter] /api/v1/discover/movies page=1 tmdb=1-4 in=80 kept=20
reasons={"no_imdb_id":0,"no_rating":0,"rating":34,"votes":0} 27ms
80 candidati esaminati, 20 tenuti, 34 scartati per voto, 0 per dato mancante.
/mnt/user/appdata/seerr-imdb-mod/update.shLo script è una promozione verificata, non un rebuild cieco:
- cerca l'ultima release upstream;
- verifica che gli override frontend siano ancora superset puliti dei sorgenti di quel tag;
- costruisce l'immagine candidata;
- verifica che la patch trovi e patchi ancora tutte e 12 le route;
- solo allora promuove, aggiorna il pin e ricrea il container.
Ogni fallimento lascia il container corrente in esecuzione. Cron giornaliero:
30 4 * * *.
Bump manuale: cambiare ARG SEERR_VERSION in Dockerfile e SEERR_VERSION in
docker-compose.yml, poi docker compose build && docker compose up -d.
Gli slider non filtrano nulla. Controllare il log di avvio: se la patch è
fallita, discover.js viene ripristinato e Seerr parte senza mod.
docker logs seerr | head -15. Verificare anche che il dataset esista:
docker exec seerr ls -la /imdb-data/.
Il filtro scarta troppi titoli. Confrontare syncedAt con la data odierna.
Un dataset vecchio fa sembrare "senza voto" film che hanno centinaia di migliaia
di voti.
?imdbRatingGte= risponde 400 "Unknown query parameter". La patch di
seerr-api.yml non ha coperto quella route: eseguire
docker exec seerr node /patches/patch-seerr.js e leggere l'output.
Prima richiesta lenta su una pagina profonda. Normale a cache fredda. Il
warmer è la risposta; IMDB_DEBUG=1 mostra quanti candidati sono stati risolti.
database disk image is malformed sul DB IMDb. Non deve più accadere: il
file è read-only per l'app e viene pubblicato con mv in modalità delete. Se
succede, eliminare imdb-data/imdb_ratings.db* e rieseguire /imdb-sync.sh.