JustWatch New Content with IMDb Ratings (ES)
Purpose
Return JustWatch's daily "Nuevo" page for Spain (/es/nuevo) grouped by streaming platform, with each title's IMDb score and vote count (plus TMDB score and Rotten Tomatoes meter, all already in the same payload). The page lists what was added in the last day on each provider in Spain (Netflix, Filmin, Atresplayer, RTVE Play, Plex, Amazon Prime, Disney+, Hayu Amazon Channel, …). Read-only — never logs in, never clicks watchlist/like buttons, never follows the e.justwatch.com outbound click-out links.
When to Use
- Daily monitoring of new movies / seasons / episodes added to streaming platforms in Spain.
- Building a "what's new and worth watching" feed where IMDb rating gates inclusion (e.g. only surface ≥ 7.0 with ≥ 1000 votes).
- Anywhere you'd otherwise scrape
/es/nuevoHTML — the Apollo state embedded in the SSR HTML is fully structured JSON and skips DOM parsing entirely.
Workflow
The page is a Vue/Apollo SPA that ships its full GraphQL cache inline as window.__APOLLO_STATE__.defaultClient. Every title visible on the page already has imdbScore, imdbVotes, tmdbScore, tomatoMeter in that cache — no per-title page visit needed. The optimal path is: load the page once, walk the Apollo cache, return structured JSON. There is no public REST endpoint; the underlying GraphQL (https://apis.justwatch.com/graphql) is operational but introspection is disabled and persisted-operation hashes change between webapp builds, so calling it directly is more brittle than reading the SSR-hydrated cache.
-
Create a session (a bare Browserbase session is fine — JustWatch does not gate
/es/nuevobehind anti-bot). Stealth/proxies are not required.sid=$(browse cloud sessions create --keep-alive \ | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))") export BROWSE_SESSION="$sid" -
Open the page and wait for hydration. The Apollo cache is populated by the time
loadfires.browse open "https://www.justwatch.com/es/nuevo" --remote -
Walk
window.__APOLLO_STATE__.defaultClientviabrowse eval. The cache is keyed by GraphQL field-with-arguments strings. The relevant root keys are everything underROOT_QUERYstarting withnewTitles(that contains"packages":["<3-letter pkg>"]— each one is a single (date, package) bucket. The 8 SSR-prefilled buckets share the most-recent date in the cache (one bucket per platform).// Run inside browse eval --remote (() => { const apollo = window.__APOLLO_STATE__.defaultClient; const root = apollo['ROOT_QUERY']; // Index packages by short-name (e.g. "nfx" -> "Netflix") const packages = {}; Object.keys(apollo).forEach(k => { if (k.startsWith('Package:')) { const p = apollo[k]; if (p.shortName) packages[p.shortName] = p.clearName || p.technicalName; } }); // Collect buckets — keys look like: // newTitles({"country":"ES","date":"YYYY-MM-DD","filter":{... "packages":["nfx"] ...},"first":10,"pageType":"NEW",...}) const bucketKeys = Object.keys(root) .filter(k => k.startsWith('newTitles(') && k.includes('packages')); const result = []; for (const bkey of bucketKeys) { const date = (bkey.match(/"date":"([0-9-]+)"/) || [])[1]; const pkg = (bkey.match(/"packages":\["([a-z]+)"\]/) || [])[1]; const ref = root[bkey]; // {type:"id", generated:true, id:"$ROOT_QUERY..."} const conn = apollo[ref.id]; // NewTitlesConnection const items = []; for (const edgeRef of conn.edges) { const edge = apollo[edgeRef.id]; // NewTitlesEdge const node = apollo[edge.node.id]; // Movie | Show | Season const cKey = Object.keys(node).find(k => k.startsWith('content(')); const content = apollo[node[cKey].id]; // MovieContent | ShowContent | SeasonContent const scoring = content.scoring && content.scoring.id ? apollo[content.scoring.id] : null; items.push({ id: node.id, // "tm…" movie, "ts…" show, "tss…" season type: node.__typename, // "Movie" | "Show" | "Season" title: content.title, url: 'https://www.justwatch.com' + content.fullPath, imdb_score: scoring && scoring.imdbScore, imdb_votes: scoring && scoring.imdbVotes, tmdb_score: scoring && scoring.tmdbScore, tomato_meter: scoring && scoring.tomatoMeter, }); } result.push({ date, package: pkg, platform: packages[pkg] || pkg, items }); } return result; })()Returns an array of
{ date, package, platform, items[] }.dateis aYYYY-MM-DDstring in JustWatch's bucket-date convention (this is what the site labels under headers like "Ayer" / "Hace dos días").imdb_score/imdb_votesmay both benullfor titles IMDb does not list (Spanish daytime TV in particular often hasimdb_score:nullwithtmdb_scoreonly). -
(Optional) Paginate for older days or more platforms. The initial SSR cache contains only 8 buckets — one per top platform for the latest day. To get more (older dates, smaller providers), scroll/click the day-navigation in the UI; each user-triggered fetch lands as another
newTitleBuckets({"after":"<cursor>",…})andnewTitles({…})entry in the Apollo cache. After each interaction, re-run step 3 to pick up the newly hydrated buckets.Cursor source:
ROOT_QUERY['newTitleBuckets({…}).pageInfo'].endCursor(base64-encodedYYYY-MM-DD_<offset>).hasNextPage:truemeans more buckets exist; trigger the SPA's "scroll past last bucket" sentinel or click a deeper-day link. -
Release the session.
browse cloud sessions update "$sid" --status REQUEST_RELEASE
Browser fallback (DOM scrape — only if SSR state is missing)
If window.__APOLLO_STATE__.defaultClient is empty (rare — only seen if the page returns a soft-error skeleton), fall back to DOM parsing on /es/nuevo:
- Day section headers render as plain text like
"Ayer"/"Hace 2 días"/ a localized date — these have no stable selector, so prefer reading from the Apollodatefield above. - Per-platform rows: each platform header
<img title="<platform-name>">followed by an<a href="/es/{pelicula|serie}/...">per item. - DOM scrape gives you titles + platform grouping but no IMDb rating — you'd have to visit each title page and parse the
imdb-scorespan (<span class="imdb-score">7.5 (20k)</span>). That's 1 fetch per title vs. zero in the Apollo path, so only use as a last resort.
Site-Specific Gotchas
window.__APOLLO_STATE__has only one top-level key —defaultClient. All cache entries live underneath it. Don't expect__APOLLO_STATE__[<typename>:<id>]directly; it's always__APOLLO_STATE__.defaultClient[…].- No
window.__NUXT__— JustWatch is Vue 2 + Apollo, not Nuxt. The hint that misleads is the SSR script ID; trust the actualwindowkeys (__APOLLO_STATE__,__DATA__,__INITIAL_SSR_USER__, …). - Apollo cache uses Apollo Client v2 normalized-cache id-reference format, not a flat object graph. Every nested object that has a
__typenameis stored as a separate key and replaced inline with a{type:"id", generated:true, id:"…"}reference. The walker MUST dereference each.idlookup (seeapollo[edge.node.id],apollo[content.scoring.id]). Treating the references as inline objects gets you{type, generated, id}strings instead of titles. Seasonnodes use a season-level scoring, not the parent show's. For new-episode releases the IMDb score is usually present at the season level (e.g. "Ley y orden: UVE T23" →imdbScore:8.1, imdbVotes:143604). If you want the show-level average instead, follownode.show.idto the parentShow:ts…entry and read its content scoring.content.titleon aSeasonis often just"Temporada N"or"season-1"(debug-shaped) — to get the human-readable show name, look up the parent show via the season'sshowreference OR derive fromfullPath(/es/serie/<show-slug>/temporada-<n>). The first-level "title" shown on the page is built by concatenating show-display-name + season number; in the Apollo cache the show-display-name lives on the parentShow.content.title.imdb_scoreandimdb_votesare bothnullon titles IMDb does not track (e.g. RTVE-only Spanish productions). Don't drop them — pass through with explicitnulland rely ontmdb_scoreas a fallback signal.- The SSR cache is locale-locked.
content({"country":"ES","language":"es"})is the only content variant in the cache for/es/nuevo. Hitting/us/newor/de/neuyields a different(country, language)tuple. The walker pattern is the same; just don't hardcode the key. - Platform short-codes are an opaque 3-letter enum (
nfx=Netflix,fil=Filmin,atr=Atresplayer,rtv=RTVE Play,plx=Plex,azp=Amazon Prime Video,dnp=Disney+,ahy=Hayu Amazon Channel, …). Always resolve via thePackage:<id>entries in the cache (shortName→clearName) rather than hardcoding — the codes are consistent across countries but the displayedclearNameis localized ("Disney Plus" vs "Disney+"). - Date bucket label vs. system date can be off by one. JustWatch's bucket-date is "when the platform crawl detected the addition", which can be UTC-shifted vs. the user's local "today". The page labels them with relative Spanish strings ("Ayer" = yesterday) computed off the bucket date, not the system date. Always trust the
datefield from the cache key, never re-derive from the page label. newTitleBuckets.endCursoris base64(YYYY-MM-DD_<offset>). If you ever decode it, the trailing offset is the slot index within that day's platform list — not a global cursor. Thefirst:8request param is the number of buckets per page, not titles per bucket; titles per bucket isfirst:10on the innernewTitlesquery.- Apollo GraphQL endpoint at
apis.justwatch.com/graphqlis callable from the page context but introspection is disabled ({"errors":[{"message":"introspection disabled"}]}). The published webapp doesn't use persisted queries — operations go through as fullquery { … }strings — so you can call it directly if you reconstruct the operation from a captured request. We did not pursue this in iteration because the SSR-cache path is strictly cheaper (zero extra round-trips) and not version-coupled to JustWatch's GraphQL schema. - No anti-bot.
/es/nuevoloads cleanly from a bare session with no--verified/--proxies. Snapshot/eval/screenshot all work first-shot. There is a Cookiebot/Usercentrics consent banner overlay (__ucCmp) that does not block content rendering or the Apollo cache — ignore it. - The slug structure differs for movies vs. shows. Movies live at
/es/pelicula/<slug>, shows at/es/serie/<slug>, and seasons at/es/serie/<slug>/temporada-<n>. Usenode.__typename(Movie|Show|Season) instead of slug-parsing — theShowtypename surfaces only when an entire show was added new (rare); most "new content" buckets areSeason(a new episode of an ongoing show) orMovie. - Outbound clickout links (
e.justwatch.com/a?…) carry a base64-encoded analytics envelope. Don't click them — they fire conversion tracking and redirect to the platform's product page. The streamingmonetizationType(FLATRATE / FREE / RENT / BUY) and presentation (HD/4K) are already in the Apollo cache underMovie:<id>.offers(…)if you need them; never traverse the clickout URL.
Expected Output
A list of per-platform, per-day buckets with titles and IMDb ratings:
{
"country": "ES",
"source": "https://www.justwatch.com/es/nuevo",
"fetched_at": "2026-05-21T14:11:00Z",
"buckets": [
{
"date": "2026-05-21",
"package": "nfx",
"platform": "Netflix",
"items": [
{
"id": "tss420399",
"type": "Season",
"title": "Pop Culture Jeopardy! - Temporada 1",
"url": "https://www.justwatch.com/es/serie/pop-culture-jeopardy-2026/temporada-1",
"imdb_score": null,
"imdb_votes": null,
"tmdb_score": 7.4,
"tomato_meter": null
},
{
"id": "tss420398",
"type": "Season",
"title": "The Boroughs: Jubilación rebelde - Temporada 1",
"url": "https://www.justwatch.com/es/serie/the-boroughs/temporada-1",
"imdb_score": 7.8,
"imdb_votes": 412,
"tmdb_score": 7.5,
"tomato_meter": null
}
]
},
{
"date": "2026-05-21",
"package": "fil",
"platform": "Filmin",
"items": [
{
"id": "tm1477321",
"type": "Movie",
"title": "La Semilla del fruto sagrado",
"url": "https://www.justwatch.com/es/pelicula/the-seed-of-the-sacred-fig",
"imdb_score": 7.5,
"imdb_votes": 19621,
"tmdb_score": 7.5,
"tomato_meter": 97
}
]
},
{
"date": "2026-05-21",
"package": "atr",
"platform": "Atres Player",
"items": [
{
"id": "tss237674",
"type": "Season",
"title": "La Ley y el Orden: Unidad de Víctimas Especiales - Temporada 23",
"url": "https://www.justwatch.com/es/serie/ley-y-orden-unidad-de-victimas-especiales/temporada-23",
"imdb_score": 8.1,
"imdb_votes": 143604,
"tmdb_score": 7.939,
"tomato_meter": 78
}
]
}
]
}
Each item is guaranteed to carry id, type, title, url. imdb_score, imdb_votes, tmdb_score, tomato_meter are all nullable — only tmdb_score is reliably populated for niche Spanish-domestic titles.