~ / guides / How to Scrape Netflix and Streaming Sites (Python Guide)

How to Scrape Netflix and Streaming Sites (Python Guide)

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • 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:

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:

SitePublic pageWhat I gotBlocker
Netflix/title/{id}HTTP 200, JSON-LD (name, type, rating, description)Geo-redirect; /browse needs login
Netflix/browse, My ListHTTP 200 then 302 to /loginLogin wall, no public HTML
IMDb/title/{id}HTTP 202, empty bodyAWS WAF bot challenge
Spotifyopen.spotify.com/artist/{id}HTTP 200, 6 KB shellClient-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:

FieldExample value (title 80100172)
nameDark
@typeTVSeries
contentRating16+
genreThrillers
descriptionA 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:

MechanismSiteSignal I sawEffect
Login wallNetflix302 to /lt/login?nextpage=... on /browseCatalog/personalized data unreachable logged out
Geo-routingNetflix/title redirected to /lt/ (my IP’s country)Region decides catalog; wrong IP = wrong data
WAF bot challengeIMDbHTTP 202, x-amzn-waf-action: challenge, 0 bytesNo HTML returned to plain requests
Client-side renderSpotifyHTTP 200 but 6 KB shell, no og:titleData 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:

  1. Geo-locked catalog. My request resolved to /lt/. To target a specific country’s availability, route through an exit IP in that country.
  2. 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 /browse grid does not (it needs login).
  3. 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:

SituationDIY requestsScraper API
Public Netflix title metadataWorks, freeOverkill
Spotify artist/playlist dataFails (JS shell)Renders JS
IMDb pagesFails (WAF 202)Absorbs challenge
Country-specific Netflix catalogNeeds your own proxiesBuilt-in geo-targeting
Thousands of titles in parallelYou manage IP rotationHandled 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:

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

Is it legal to scrape Netflix?

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.

Can I scrape my own Netflix viewing history?

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.

Why does my Netflix scraper get redirected to a login page?

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.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.