How to Scrape Netflix and Streaming Sites (Python Guide)
- Netflix public /title/ pages return HTTP 200 and embed a clean JSON-LD block with name, type, description, and content rating. I parsed it with plain requests, output pasted below.
- Catalog and recommendation data sit behind a login wall: /browse redirected me straight to /login. You cannot scrape personalized feeds without an account.
- IMDb blocked me with an AWS WAF challenge (HTTP 202, x-amzn-waf-action: challenge). Spotify returned a 6 KB JS shell with no data in the HTML.
- For IMDb, Spotify, and logged-in Netflix data, a scraper API that renders JS and rotates IPs (ChocoData example below) is the realistic path.
Streaming sites split sharply into what they hand you and what they hide. I ran live Python fetches against Netflix, IMDb, and Spotify in June 2026 to map that line. Netflix public title pages returned a 200 and a parseable JSON-LD block. IMDb threw an AWS WAF challenge. Spotify served a JavaScript shell with no data in the HTML. This guide shows the real results, a working Netflix scraper with pasted output, and the scraper-API fallback for the targets that block plain requests.
Can you legally scrape Netflix and streaming sites?
You can scrape public, logged-out metadata in most jurisdictions, with limits. US case law (hiQ Labs v. LinkedIn, Ninth Circuit) held that scraping publicly accessible data does not by itself violate the Computer Fraud and Abuse Act. Netflix’s Terms of Use separately prohibit automated access and accessing content behind the login. Those are contract terms, not criminal law, but they govern your account and any commercial relationship.
The practical rules I follow:
- Stay on public
/title/{id}pages that load without a session. - Never log in and scrape the authenticated UI. That breaks the Terms of Use directly.
- Do not redistribute copyrighted descriptions, artwork, or any video stream.
- For your own watch history, use Netflix’s official data export, not a scraper.
For a deeper legal walkthrough, see my guide on whether web scraping is legal. The short version for streaming: metadata on a public page is defensible, anything behind a password is not.
What data can you actually scrape from streaming sites?
The split is per-site and depends on whether data renders in server-side HTML or loads later via JavaScript and login. Here is what my June 2026 fetches returned for each target:
| Site | Public page | What I got | Blocker |
|---|---|---|---|
| Netflix | /title/{id} | HTTP 200, JSON-LD (name, type, rating, description) | Geo-redirect; /browse needs login |
| Netflix | /browse, My List | HTTP 200 then 302 to /login | Login wall, no public HTML |
| IMDb | /title/{id} | HTTP 202, empty body | AWS WAF bot challenge |
| Spotify | open.spotify.com/artist/{id} | HTTP 200, 6 KB shell | Client-rendered, no data in HTML |
So the only one of the three that gave up clean data to plain requests was a Netflix standalone title page. Netflix embeds a single application/ld+json block per title with the fields below:
| Field | Example value (title 80100172) |
|---|---|
| name | Dark |
| @type | TVSeries |
| contentRating | 16+ |
| genre | Thrillers |
| description | A missing child sets four families on a frantic hunt for answers… |
That is enough to build a public catalog reference. Cast lists, episode-level data, per-region availability, and personalized rows are not in that block and require either login or a heavier rendering setup.
How do streaming sites block scrapers?
Each of the three uses a different defense, which is why one method does not cover all of them. My fetches surfaced three distinct mechanisms:
| Mechanism | Site | Signal I saw | Effect |
|---|---|---|---|
| Login wall | Netflix | 302 to /lt/login?nextpage=... on /browse | Catalog/personalized data unreachable logged out |
| Geo-routing | Netflix | /title redirected to /lt/ (my IP’s country) | Region decides catalog; wrong IP = wrong data |
| WAF bot challenge | IMDb | HTTP 202, x-amzn-waf-action: challenge, 0 bytes | No HTML returned to plain requests |
| Client-side render | Spotify | HTTP 200 but 6 KB shell, no og:title | Data loads via JS/API after page load |
The Netflix geo-redirect is worth flagging: my request to a US-style title URL came back from /lt/ (Lithuania, my exit IP’s country). Catalog availability is region-specific, so to get a given country’s data you need an exit IP in that country. That is a job for a proxy network or a scraper API with geo-targeting.
For the general playbook on headers, IP rotation, and challenge handling, see how to scrape without getting blocked.
Method 1: Scrape a Netflix title page with Python (DIY)
A plain requests + BeautifulSoup script pulls the JSON-LD block off any public Netflix title page. Netflix renders that block server-side, so no browser automation is needed for this specific data. Here is the exact script I ran:
import requests, json
from bs4 import BeautifulSoup
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
def scrape_netflix_title(title_id):
url = f"https://www.netflix.com/title/{title_id}"
headers = {"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}
r = requests.get(url, headers=headers, timeout=20)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
block = soup.find("script", type="application/ld+json")
data = json.loads(block.string) if block else {}
return {
"id": title_id,
"name": data.get("name"),
"type": data.get("@type"),
"description": data.get("description"),
"contentRating": data.get("contentRating"),
"genre": data.get("genre"),
}
print(json.dumps(scrape_netflix_title("80100172"), indent=2, ensure_ascii=False))
Running it against title 80100172 returned this, verbatim:
{
"id": "80100172",
"name": "Dark",
"type": "TVSeries",
"description": "A missing child sets four families on a frantic hunt for answers as they unearth a mind-bending mystery that spans three generations.",
"contentRating": "16+",
"genre": "Thrillers"
}
New to the libraries here? Start with web scraping with BeautifulSoup and the broader Python web scraping guide.
Three limits to know before you build on this:
- Geo-locked catalog. My request resolved to
/lt/. To target a specific country’s availability, route through an exit IP in that country. - You need title IDs. This script reads one title at a time. Netflix gives logged-out users no public, scrapable index of all IDs, so discovery is the hard part. Sitemaps and external ID datasets help; the
/browsegrid does not (it needs login). - Public fields only. Cast, episode lists, and per-region flags are not in the JSON-LD block.
For Spotify and IMDb, this exact approach fails. Spotify’s HTML is a 6 KB shell, so there is nothing to parse; you would need Playwright to render the page or call Spotify’s official Web API with an OAuth token. IMDb returned a WAF challenge with an empty body, so r.raise_for_status() would pass on the 202 but block would be None. Both need the rendering-plus-rotation setup in Method 2.
Method 2: Use a scraper API for blocked targets
A scraper API handles the parts that broke Method 1: it renders JavaScript (Spotify), solves or absorbs the WAF challenge (IMDb), and routes through a geo-targeted residential IP (Netflix region data). You send a target URL, the API returns the final rendered HTML, and you parse it with the same BeautifulSoup code.
Here is the pattern using ChocoData’s universal endpoint. It takes the target URL plus flags for JavaScript rendering and country, and hands back rendered HTML:
import requests, json
from bs4 import BeautifulSoup
API_KEY = "YOUR_CHOCODATA_KEY"
def fetch_rendered(target_url, country="us"):
resp = requests.get(
"https://api.chocodata.com/v1",
params={
"api_key": API_KEY,
"url": target_url,
"render_js": "true", # for Spotify's client-rendered pages
"country": country, # for Netflix region-specific catalog
},
timeout=90,
)
resp.raise_for_status()
return resp.text
# Spotify: render JS, then parse the populated DOM
html = fetch_rendered("https://open.spotify.com/artist/0OdUWJ0sBjDrqHygGUXeCF")
soup = BeautifulSoup(html, "html.parser")
og = soup.find("meta", property="og:title")
print("Artist:", og["content"] if og else "not found")
# Netflix: same title page, but force a specific country's catalog
html = fetch_rendered("https://www.netflix.com/title/80100172", country="gb")
print("UK fetch bytes:", len(html))
ChocoData exposes a universal endpoint plus 453 dedicated endpoints for specific targets, so for high-volume work you can call a purpose-built endpoint instead of parsing HTML yourself. I have not pasted output for this snippet because it needs a paid key; the Netflix output I did paste above came from the free requests method I actually ran.
When the API earns its cost over DIY:
| Situation | DIY requests | Scraper API |
|---|---|---|
| Public Netflix title metadata | Works, free | Overkill |
| Spotify artist/playlist data | Fails (JS shell) | Renders JS |
| IMDb pages | Fails (WAF 202) | Absorbs challenge |
| Country-specific Netflix catalog | Needs your own proxies | Built-in geo-targeting |
| Thousands of titles in parallel | You manage IP rotation | Handled for you |
Should you use the official APIs instead?
For Spotify and IMDb-adjacent data, yes, the official route is cleaner and avoids the blocks entirely. Two of the three targets have legitimate data sources that beat scraping:
- Spotify Web API. Free with an OAuth token, returns artist, album, track, and playlist data as JSON. No rendering, no blocks. Docs at developer.spotify.com. This is the correct way to get the data the 6 KB HTML shell hides.
- Movie/TV metadata. For IMDb-style data (titles, ratings, cast), licensed catalog APIs and open datasets exist that are built for automated access, unlike IMDb’s own scraping-hostile pages.
- Netflix. No public catalog API. Logged-out title metadata via the JSON-LD method above is the realistic public route; anything broader needs a partner agreement or a scraper API.
The decision order I would follow: official API first (Spotify), then public JSON-LD scraping where it works (Netflix titles), then a scraper API for the JS-heavy or WAF-protected targets (Spotify at scale without a token, IMDb, geo-locked Netflix). Scrape raw HTML only when no API exposes the field you need.
Key takeaways
Netflix public title pages are the one streaming target that gives up clean data to a plain Python request: HTTP 200 and a JSON-LD block with name, type, rating, and description, which I parsed and pasted above. Everything else on Netflix sits behind a login wall and a country-specific geo-redirect. IMDb blocks plain requests with an AWS WAF challenge (HTTP 202, empty body). Spotify ships a 6 KB JavaScript shell with no data in the HTML, so use its official Web API. For the blocked targets and for country-specific Netflix catalog data, a scraper API that renders JavaScript and rotates geo-targeted IPs (the ChocoData example above) is the practical path. Keep your scraping on public, logged-out pages, and pull personal data only through official exports.
FAQ
Scraping public metadata (titles, descriptions, ratings shown to logged-out visitors) sits in the same gray area as most public-web scraping: courts in the US (hiQ v. LinkedIn) have allowed scraping of public data, but Netflix's Terms of Use prohibit automated access and ban scraping content behind a login. Stay on public /title/ pages, do not log in, do not redistribute copyrighted artwork or video, and consult a lawyer for commercial use.
Yes, but not by scraping the site. Netflix lets account holders download their own viewing activity and a full data archive from Account, Get a copy of your data. That is the compliant route for personal watch history; scraping the logged-in UI violates the Terms of Use.
Pages like /browse, My List, and personalized rows require an authenticated session, so a logged-out request 302-redirects to /login. Only standalone /title/{id} pages render public metadata without a session. If you need catalog-wide data, use a scraper API with session handling or an official partner feed.