jdepoix

Activity

Stars and mentions · Last 30 days, Oct 6 – Nov 4

GitHub stars107
Mentions8
0481216Oct 6Oct 20Nov 4
UTC days · repositories jdepoix owns1 repository

Top repositories

jdepoix’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside jdepoix’s account where jdepoix opened or reviewed pull requests in the past 365 days.

Who noticed jdepoix

Everyone who starred or forked jdepoix’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

8posts
Nov 50 posts
  1. Why youtube-transcript-api gets blocked on servers, and what works Originally published at lurkapi.com !A white envelope sliding out of a server rack, stopped by a lowered jade-and-white barrier arm under an amber lamp. When youtube-transcript-api works on your laptop and raises RequestBlocked on AWS, a VPS, Colab or a CI runner, your code is fine. YouTube is refusing your server’s IP: it answers requests from cloud and datacenter addresses with “Sign in to confirm you’re not a bot” instead of captions, and the library gives up in under a second. From a fresh cloud server, 1 of 10 popular videos comes back; the other nine raise RequestBlocked. Two fixes still work: send the library through rotating residential proxies, or let a hosted transcript API make the request so your server never talks to YouTube. The third fix the error message offers, cookies, is switched off in the current release. The block hides inside an HTTP 200 Each fetch is three requests. The library loads the video’s watch page to read an API key, posts to YouTube’s internal player endpoint as the Android app, then downloads the caption track the player lists. From a datacenter IP the first request goes through: about 1.3 MB of HTML, key included, no captcha. The second also returns 200, but its body says LOGIN_REQUIRED, with the bot check as the reason. The library raises RequestBlocked and never sends the third. ![Both requests return 200; the second one’s body is the bot check.](media.lurkapi.com) Both requests return 200; the second one’s body is the bot check. So curling youtube.com from your server proves nothing: the page loads and every status code is 200. Without a proxy config the library doesn’t retry, and retrying yourself from the same IP gets the same answer. IpBlocked is the other flavour: an HTTP 429, or a reCAPTCHA page where the watch page should be. It subclasses RequestBlocked, so one except RequestBlocked catches both. Versions before 1.0.0 reported the same block as TranscriptsDisabled (“Subtitles are disabled for this video”), so if an old pinned version swears a popular video has no subtitles, it’s probably being blocked too. A green smoke test can hide a blocked server On that blocked server, one video still worked: Rick Astley’s Never Gonna Give You Up, dQw4w9WgXcQ, the go-to test video. It came back all three times. Nine other popular videos raised RequestBlocked. A deploy check that fetches only that id goes green while the jobs behind it fail. Check a server with the videos your job needs. If most of them raise RequestBlocked, it’s the IP. Check your server · Python from youtube_transcript_api import YouTubeTranscriptApi api = YouTubeTranscriptApi() ids = [ # use your own "dQw4w9WgXcQ", "kJQP7kiw5Fk", "arj7oStGLkU", ] for vid in ids: try: t = api.fetch(vid) print(vid, "OK", len(t)) except Exception as e: print(vid, type(e).__name__) # From a cloud server: # dQw4w9WgXcQ OK 61 # kJQP7kiw5Fk RequestBlocked # arj7oStGLkU RequestBlocked Cookies are off in 1.2.4, and proxies need upkeep The error message offers cookies as its second fix, with a warning that YouTube will eventually ban the account you log in with. In 1.2.4, the current release, cookie auth is switched off (“Cookie auth has been temporarily disabled”), CLI flag included. Its first fix is a proxy. The README recommends rotating residential proxies, because YouTube bans static ones after extended use. There’s a built-in config for one residential provider, and GenericProxyConfig for any other: Any proxy · adapted from the README from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api.proxies import GenericProxyConfig proxy = GenericProxyConfig( http_url="http://user:pass@my-custom-proxy.org:port", https_url="https://user:pass@my-custom-proxy.org:port", ) ytt_api = YouTubeTranscriptApi(proxy_config=proxy) ytt_api.fetch(video_id) Know how retries behave before you buy traffic. The built-in provider config retries a blocked video up to 10 times; GenericProxyConfig tries once unless you subclass it and raise retries_when_blocked. Every attempt starts over at the watch page, so each refusal still costs a full page of traffic, and residential traffic is billed by the gigabyte. If the videos are your own, there’s an official route. The YouTube Data API’s captions.download needs OAuth and permission to edit the video, and each call costs 200 of your 10,000 daily quota units, so 50 downloads a day. It can’t read anyone else’s videos. Or take YouTube off your server: one GET With a hosted API, your server makes one HTTPS call and the YouTube side happens elsewhere, so it stops mattering where your code runs. LurkAPI’s transcript endpoint fetches the captions for you, and a cached repeat comes back in 20 ms. ![Same ten videos: 1 from the library, 10 from LurkAPI.](media.lurkapi.com) Same ten videos: 1 from the library, 10 from LurkAPI. Video transcript: What's said in a video or Short: its captions as timed segments and as one block of text. GET https://api.lurkapi.com/v1/youtube/video/transcript · 1 credit per call · Read the docs Get a transcript · 1 credit curl "https://api.lurkapi.com/v1/youtube/video/transcript?url=https://www.youtube.com/watch?v=arj7oStGLkU" \ -H "x-api-key: $LURK_KEY" The JSON, trimmed to 2 segments Response { "success": true, "credits_charged": 1, "videoId": "arj7oStGLkU", "url": "https://www.youtube.com/watch?v=arj7oStGLkU", "language": "English", "languageCode": "en", "isGenerated": false, "transcript": [ { "text": "So in college,", "startMs": 12645, "endMs": 14015, "startTimeText": "0:12" }, { "text": "I was a government major,", "startMs": 15349, "endMs": 16913, "startTimeText": "0:15" } ], "transcript_only_text": "So in college, I was a government major, …", "availableLanguages": [ { "code": "ar", "name": "Arabic", "isGenerated": false }, { "code": "hy", "name": "Armenian", "isGenerated": false }, { "code": "bg", "name": "Bulgarian", "isGenerated": false } ] } Each call is 1 credit: $0.45 to $0.90 per 1,000 transcripts depending on the pack, and a new account starts with 100 free credits. Switching from the library takes a few lines. Segments come back as text, startMs and endMs; to_snippet converts one to the library’s text, start and duration: Python · 1 credit import requests API = ("https://api.lurkapi.com" "/v1/youtube/video/transcript") KEY = "lk_live_..." def transcript(video_id): r = requests.get( API, params={"url": video_id}, headers={"x-api-key": KEY}, timeout=30, ) r.raise_for_status() return r.json()["transcript"] def to_snippet(seg): s = seg["startMs"] e = seg["endMs"] return { "text": seg["text"], "start": s / 1000, "duration": (e - s) / 1000, } segs = transcript("arj7oStGLkU") print(len(segs), to_snippet(segs[0])) # 315 {'text': 'So in college,', # 'start': 12.645, 'duration': 1.37} One default differs. The library asks for English only and raises NoTranscriptFound otherwise; the endpoint returns English when the video has it and falls back to the video’s main language when it doesn’t. Pass language=es (or pt-BR, and so on) for a specific track, and check languageCode if you need English. A video with no captions returns 404 not_found. For a whole channel, list its uploads with channel videos and make one transcript call per video. For one transcript without code, the free YouTube transcript tool runs the same endpoint in your browser, or try it here: Run GET /v1/youtube/video/transcript in the live playground Questions What does RequestBlocked mean in youtube-transcript-api? YouTube answered the library’s player request with “Sign in to confirm you’re not a bot” instead of captions, almost always because the request came from a cloud or datacenter IP. IpBlocked, a subclass, means an HTTP 429 or a reCAPTCHA page; except RequestBlocked catches both. Why does youtube-transcript-api work locally but not on a server? Same code, different IP. YouTube blocks most IPs that belong to cloud providers such as AWS, Google Cloud and Azure, and a home IP can be blocked too after too many requests. Do cookies work with youtube-transcript-api? Not in 1.2.4. Cookie auth is switched off in the code, CLI flag included, even though the RequestBlocked message still suggests it. How do I use a proxy with youtube-transcript-api? Pass a proxy_config to YouTubeTranscriptApi: the built-in config for one residential provider, or GenericProxyConfig(http_url=..., https_url=...) for any other. The README recommends rotating residential proxies, since static ones get banned after extended use. Is there an official YouTube transcript API? Only for videos you can edit. The YouTube Data API’s captions.download needs OAuth and edit permission, and costs 200 of the default 10,000 daily quota units per call. For anyone else’s videos, LurkAPI’s transcript endpoint returns the captions for 1 credit a call.

  2. How to get a YouTube transcript in 2026: libraries, yt-dlp, Whisper, hosted APIs and MCP Getting the transcript of a YouTube video sounds like one function call. In practice the right tool depends on where your code runs, whether the video has captions at all, and whether a person or an AI agent is asking. This is a short guide to the options, with commands you can run today. It is a companion to awesome-youtube-transcripts, a list we maintain of libraries, hosted APIs, MCP servers, agent skills and speech-to-text tools for YouTube transcripts. Quick answer | You want to | Start with | | --- | --- | | Fetch captions from a script on your own machine | youtube-transcript-api (Python) or YouTube.js (JavaScript) | | Download subtitle files from the command line | yt-dlp | | Transcribe a video that has no captions | faster-whisper or whisper.cpp on the audio | | Fetch transcripts from a server or cloud function | A hosted API, or a library behind your own proxies | | Let an AI assistant read YouTube videos | An MCP server or an agent skill | 1. A library, on your own machine If you run code on a laptop or home connection, an open-source library is the simplest route. Python: youtube-transcript-api. It reads manual and auto-generated captions, lists the languages a video offers, and can translate. Version 1.x uses an instance: from youtube_transcript_api import YouTubeTranscriptApi transcript = YouTubeTranscriptApi().fetch("dQw4w9WgXcQ", languages=["en"]) text = " ".join(snippet.text for snippet in transcript) print(text[:200]) Each snippet also has start and duration in seconds if you need timestamps. JavaScript: YouTube.js. A client for YouTube's internal InnerTube API; getTranscript() returns timed segments. There is a transcript example in the repo. If you want one function and nothing else, the youtube-transcript npm package has a single fetchTranscript() call, but it uses an unofficial endpoint and can break when YouTube changes. 2. yt-dlp, for subtitle files yt-dlp can save captions without downloading the video: yt-dlp --write-subs --write-auto-subs --sub-langs en --sub-format vtt \ --skip-download -o "%(id)s" "https://www.youtube.com/watch?v=dQw4w9WgXcQ" That writes dQw4w9WgXcQ.en.vtt. --write-subs takes uploaded captions, --write-auto-subs falls back to auto-generated ones, and the subtitle options section lists every format and language flag. It also handles whole playlists and channels. 3. Videos with no captions: speech-to-text Some videos have captions turned off, or none were ever generated. Then you need the audio and a speech model: - faster-whisper: Whisper on CTranslate2, several times faster than the original with lower memory use. - whisper.cpp: Whisper in C/C++, runs on CPU and Apple Silicon. - WhisperX: adds word-level timestamps and speaker diarization. Download the audio with yt-dlp -x --audio-format m4a <url> and pass the file to the model. Expect this to take far longer than reading captions, and to cost GPU time at scale. 4. The cloud problem Everything above works well from a home connection. Run the same code on AWS, GCP, Azure or most VPS providers and requests often fail with RequestBlocked or IpBlocked, because YouTube blocks many cloud IP ranges. The youtube-transcript-api README has a good explanation and proxy setup. You have two choices: 1. Keep the library and route it through residential or rotating proxies you manage. 2. Use a hosted API that handles it for you. Hosted options (all commercial; check each site for current pricing and limits): - GetYouTubeTranscript: REST API and remote MCP server for transcripts, YouTube search, channels and playlists, with Python and Node SDKs. (Ours; see the disclosure below.) - SearchAPI: a transcripts endpoint inside a general search API. - Supadata: transcripts and metadata for YouTube, TikTok, Instagram, X and web pages. - TranscriptAPI: YouTube transcripts, search, channels and playlists, with an MCP server. - TranscriptOut: transcript API with agent skills and an MCP server. - youtube-transcript.io: web tool and API. A hosted call is one HTTP request. With ours, for example: curl "https://getyoutubetranscript.com/api/v1/transcript?v=dQw4w9WgXcQ" \ -H "Authorization: Bearer $YOUTUBE_TRANSCRIPT_API_KEY" When comparing services, test with your own videos: success rate on long videos, videos with only auto-generated captions, and non-English videos matters more than the headline price. We publish an open benchmark you can fork and point at any provider. 5. For AI assistants: MCP servers and agent skills If the goal is "let Claude, Cursor or ChatGPT read this video", you don't need to write code at all. MCP servers (Model Context Protocol) give an assistant a transcript tool: - jkawamoto/mcp-youtube-transcript: runs locally, paginates long videos, supports proxy settings. - anaisbetts/mcp-youtube: uses a local yt-dlp install. - tubeagentkit/youtube-mcp: our hosted server (transcripts, search, channels, playlists), remote HTTP with OAuth sign-in or local stdio. Local servers inherit the cloud problem only if your machine is in a data centre; on a laptop they work fine. Agent skills are instruction folders that Claude Code, Cursor and Codex load when a task matches. tapestry-skills pulls transcripts as research sources, and youtube-to-ebook turns a channel's videos into an EPUB. The full list has more. 6. Framework integrations If you already use a framework, there is probably a loader: - LlamaIndex has a built-in YoutubeTranscriptReader. - n8n, Haystack and Dify have community nodes and components, listed in the Framework integrations section. Common errors - Transcripts disabled: the uploader turned captions off. Only speech-to-text will help. - No transcript in that language: ask for the languages the video offers first, then pick one. - RequestBlocked / IpBlocked: you are on a cloud IP; see section 4. - Works locally, fails in CI: same cause. CI runners are cloud machines. We keep a longer error reference with what triggers each one. Disclosure We build GetYouTubeTranscript, so some tools above are ours. Competing tools are listed on the same terms, and if one is missing or described unfairly, open a PR on awesome-youtube-transcripts.

  3. How to get YouTube transcripts in bulk with Python (no API key, works on cloud servers) Disclosure: I built the Apify actor used in this post. It is a paid tool (pay per transcript), and I say below where it doesn't fit. If you have tried to get YouTube transcripts from code, you have probably hit one of these walls: 1. The official YouTube Data API doesn't help. Its captions.download endpoint only works for videos you own. For anyone else's video you get a 403. 2. youtube-transcript-api works on your laptop and then dies on a server. YouTube answers requests from cloud IP ranges (AWS, GCP, Hetzner, most VPSes) with a bot check, so the same code that ran fine locally starts throwing RequestBlocked / IpBlocked in production. Developers on the library's issue tracker discuss exactly this. 3. There is no "whole channel" button. You need to list the channel's videos first, then fetch each transcript, then handle the videos that have no captions. This post shows a way around all three from Python, in about 20 lines. The short version: one HTTP call YouTube Transcript Scraper runs on Apify's infrastructure. It tries a datacenter proxy first and automatically retries through a residential IP when YouTube shows a bot check, so you don't manage proxies yourself. You send video, playlist or channel URLs and get one JSON row per video. You need a free Apify account and your API token (Console → Settings → Integrations). Then: import requests TOKEN = "YOUR_APIFY_TOKEN" url = ("https://api.apify.com/v2/acts/groupoject~youtube-transcript-scraper" f"/run-sync-get-dataset-items?token={TOKEN}") rows = requests.post(url, json={ "urls": ["https://www.youtube.com/watch?v=iG9CE55wbtY"], "outputFormats": ["text"], }, timeout=320).json() for row in rows: print(row["title"], "-", row["wordCount"], "words") print(row["text"][:300]) run-sync-get-dataset-items starts the run, waits for it to finish and returns the rows in one response. Apify caps synchronous calls at about 5 minutes, so use it for a handful of videos. For a whole channel, use the client library below. Bulk: a whole channel or playlist pip install apify-client, then: from pathlib import Path from apify_client import ApifyClient client = ApifyClient("YOUR_APIFY_TOKEN") run = client.actor("groupoject/youtube-transcript-scraper").call(run_input={ "urls": [ "@lexfridman", # a channel handle "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxx", # a playlist ], "maxVideosPerSource": 50, # newest 50 per channel / playlist "channelContentTypes": ["videos"], # or add "shorts", "streams" "languages": ["en"], # preference order, e.g. ["es", "en"] "outputFormats": ["text", "segments", "srt"], }) out = Path("transcripts") out.mkdir(exist_ok=True) for item in client.dataset(run["defaultDatasetId"]).iterate_items(): (out / f"{item['videoId']}.txt").write_text(item["text"], encoding="utf-8") (out / f"{item['videoId']}.srt").write_text(item["srt"], encoding="utf-8") print(item["videoId"], item["languageCode"], "auto" if item["isAutoGenerated"] else "manual", item["title"]) A few input details worth knowing (these are the real field names from the actor's input schema): - urls accepts watch?v= links, youtu.be links, Shorts, /live/ URLs, bare 11-character IDs, playlist URLs, channel URLs and @handles, all mixed in one list. - maxVideosPerSource caps each playlist and each channel content type (newest first, up to 5,000). Single video URLs are not affected. - captionType is any (default: prefer creator-uploaded, fall back to auto-generated), manual or auto. - fallbackToAnyLanguage (default true) returns the original-language captions when none of your languages exist. Set it to false to skip those videos instead. - includeVideoMetadata (default true) adds title, channel, publish date, views, likes, duration, description and tags to each row. Which videos had no transcript? Videos without captions are not pushed to the dataset and are not charged. They are listed in the run's key-value store: store = client.key_value_store(run["defaultKeyValueStoreId"]) missing = (store.get_record("NO_TRANSCRIPT") or {}).get("value") or [] for m in missing: print(m["videoId"], m["reason"], m.get("availableLanguages")) reason is NO_CAPTIONS, LANGUAGE_NOT_AVAILABLE or EMPTY_CAPTIONS. Private, deleted or region-blocked videos go to the ERRORS record instead. Feeding transcripts to an LLM or a RAG pipeline For summarising, the text field is all you need. For retrieval with citations, use segments: each segment has start, duration and text in seconds, so you can chunk by time and keep a deep link back to the moment in the video. def chunks(item, seconds=90): buf, t0 = [], None for seg in item["transcript"]: t0 = seg["start"] if t0 is None else t0 buf.append(seg["text"]) if seg["start"] - t0 >= seconds: yield {"text": " ".join(buf), "source": f"{item['url']}&t={int(t0)}s", "title": item["title"]} buf, t0 = [], None if buf: yield {"text": " ".join(buf), "source": f"{item['url']}&t={int(t0)}s", "title": item["title"]} Each chunk can go straight into your vector store with source as metadata, so answers can link to the exact timestamp. No-code: n8n, Make, Zapier If you'd rather not run Python, the actor works with Apify's integrations. In n8n, add the Apify node, choose Run an Actor and Get Dataset, set the actor to groupoject~youtube-transcript-scraper and paste the JSON input above. Connect a Google Sheets or Notion node after it. In Make and Zapier the Apify app has the same "run actor" and "get dataset items" steps. You can also call it from Claude, Cursor or another MCP client through https://mcp.apify.com?tools=groupoject/youtube-transcript-scraper. Monitor a channel for new transcripts Turn on onlyNewVideos and give the monitor a stateKey (any name, e.g. ai-podcasts). Each run then saves only videos it hasn't saved before for that key, so you're never charged twice for the same video. Schedule it daily in Apify (Schedules → Create) and connect a Slack, email or Google Sheets integration to get each new transcript as it lands: run = client.actor("groupoject/youtube-transcript-scraper").call(run_input={ "urls": ["https://www.youtube.com/@lexfridman"], "onlyNewVideos": True, "stateKey": "lex-podcast", "publishedAfter": "7 days", # optional: ignore older uploads on the first run }) Brand-new uploads sometimes get auto-captions a few hours late; the actor re-checks those for 24 hours instead of skipping them. What it costs $2.50 per 1,000 transcripts on the free plan ($0.0025 per video), with proxy traffic included. Paid Apify plans get a lower per-result rate automatically. Videos with no captions, and failed, private or deleted videos, are not charged. A 100-video playlist where every video has captions costs $0.25. You can set a maximum charge per run, and the actor stops cleanly before it goes over. Honest limits - It returns YouTube's own captions. It does not transcribe audio. If a video has no captions at all (music clips, some Shorts), you get nothing for it. For those you need Whisper or a speech-to-text API. - Auto-generated captions are only as good as YouTube's speech recognition. Names and jargon are often wrong. isAutoGenerated tells you which kind you got. - Translation (translateTo) is best effort. YouTube rate-limits its auto-translate endpoint heavily. When it refuses, you still get the original-language transcript and translationError explains why. - No login, by design. Age-restricted, members-only and private videos are reported in ERRORS, not scraped. - If you need a few videos a day from your laptop, you don't need this. youtube-transcript-api or yt-dlp --write-auto-subs is free and works fine from a home connection. This tool is for when you need volume, channels, or a server. - Respect copyright. Transcripts are the creators' work. Summarising for research is one thing. Republishing them is another. Links - Actor: apify.com If something breaks on a video that should work, open an issue on the actor's Issues tab with the video ID. I read them daily.

  4. youtube-transcript-api works locally but gets blocked on your server: why, and 3 fixes You build something with youtube-transcript-api. It works perfectly on your laptop. You deploy it to AWS, Render, Railway, Vercel or a VPS, and every call now fails with this: youtube_transcript_api._errors.RequestBlocked: Could not retrieve a transcript for the video ... Sometimes it shows up as IpBlocked instead. Your code didn't change and neither did the video, so what happened? Why it happens YouTube blocks most IP addresses that belong to cloud providers. The library's own README says so: requests from AWS, Google Cloud, Azure and similar networks are very likely to be refused. A home IP can get blocked too if it sends a lot of requests in a short time. So the problem is where your request comes from, not your code. That's also why switching libraries rarely helps. yt-dlp on the same server usually hits the same wall, as a "Sign in to confirm you're not a bot" error. There are three ways around it. Fix 1: Rotating residential proxies This is the fix the library supports itself. Residential proxies send your request through ordinary home connections, which YouTube treats like normal viewers. from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api.proxies import WebshareProxyConfig ytt_api = YouTubeTranscriptApi( proxy_config=WebshareProxyConfig( proxy_username="<proxy-username>", proxy_password="<proxy-password>", ) ) transcript = ytt_api.fetch("dQw4w9WgXcQ") print(" ".join(s.text for s in transcript)) Any provider works through GenericProxyConfig: from youtube_transcript_api.proxies import GenericProxyConfig ytt_api = YouTubeTranscriptApi( proxy_config=GenericProxyConfig( http_url="http://user:pass@proxy.example.com:8000", https_url="http://user:pass@proxy.example.com:8000", ) ) Things to know: - Buy rotating residential proxies. Static or datacenter proxies get banned after a while. - You pay per GB, and the price varies a lot between providers. Transcripts are small, but YouTube pages around them are not. - Some requests will still fail, so add retries with backoff. - Cache every transcript you fetch. Captions rarely change, and a cache hit costs no proxy traffic. import json, pathlib, time from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api._errors import RequestBlocked, IpBlocked CACHE = pathlib.Path("transcripts") CACHE.mkdir(exist_ok=True) def get_transcript(api: YouTubeTranscriptApi, video_id: str, tries: int = 4): path = CACHE / f"{video_id}.json" if path.exists(): return json.loads(path.read_text()) for attempt in range(tries): try: snippets = [ {"text": s.text, "start": s.start, "duration": s.duration} for s in api.fetch(video_id) ] path.write_text(json.dumps(snippets)) return snippets except (RequestBlocked, IpBlocked): time.sleep(2 ** attempt) # rotating proxies give a new IP on retry raise RuntimeError(f"still blocked after {tries} tries: {video_id}") Good fit: you want full control, you fetch a lot every day, and you're fine maintaining it. Fix 2: Fetch from a machine that isn't in a data center If your volume is small, run the fetching part somewhere with a normal home IP: an old laptop, a Raspberry Pi, or your dev machine. Your cloud app puts video IDs in a queue (a database table is enough), and the home worker fetches the transcripts and writes them back. It's free, but it's another machine to keep running. If you send too many requests, your home IP can get blocked too. Keep it slow, a few requests a minute, and cache everything. Good fit: side projects, a few dozen videos a day. Fix 3: Use a hosted transcript API The third option is to make the IP problem somebody else's. Several hosted APIs do this: Supadata, a few Apify actors, and the one I build, BulkTranscripts. They run the proxies and retries, and your server makes a plain HTTPS call that YouTube never sees. Disclosure: I build BulkTranscripts, so here's what it looks like: import requests r = requests.get( "https://bulktranscripts.co/api/v1/transcript", params={"video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, headers={"Authorization": "Bearer bt_ak_YOUR_KEY"}, timeout=60, ) r.raise_for_status() data = r.json() print(data["title"], data["word_count"]) print(data["text"][:500]) # full transcript as one string print(data["segments"][0]) # {"text": ..., "start": ..., "duration": ...} Some details, since you'd want them before choosing: - 30 free credits to try it, no card. After that, packs start at $4.99 for 200 transcripts, and credits don't expire. - A failed fetch isn't charged, and re-reading a transcript you already fetched is free. - There are endpoints for a whole channel or playlist (/api/v1/channel/videos, /api/v1/playlist/videos), plus an MCP server if you want Claude or ChatGPT to fetch transcripts directly. - Docs here. Whichever hosted API you pick, check three things: what a failed request costs, whether re-fetches are billed, and whether it can do channels and playlists in bulk. Good fit: you want to ship your actual product and stop thinking about proxies. Which one to pick | Your situation | Pick | |---|---| | Hobby project, under ~50 videos a day | Fix 2: a home worker | | High volume, you want control and can maintain it | Fix 1: rotating residential proxies | | You want it working today and don't want to maintain it | Fix 3: a hosted API | Whatever you choose, cache transcripts. It's the one thing that makes all three cheaper and more reliable. If you've found another way around RequestBlocked, I'd like to hear it in the comments.

  5. Why youtube-transcript-api gets blocked on your server (and 3 ways to fix it) If you've used youtube-transcript-api, you know the feeling: it works perfectly on your laptop, you deploy it to a server, and every request fails with RequestBlocked or IpBlocked. I hit this while building a transcript tool, so I measured it instead of guessing. Here's what happened, why, and what you can do about it. The test I took 10 popular YouTube videos that have captions (I checked each one from my home connection first) and fetched their transcripts from a cloud server three ways, using youtube-transcript-api 1.2.4: | How the request went out | Transcripts fetched | |---|---| | Directly from the cloud server | 10 / 10 | | Through datacenter proxies | 9 / 10 | | Through residential proxies | 10 / 10 | This is one run with 10 videos in October 2026, so treat it as a snapshot, not a benchmark. The pattern is still clear: YouTube blocks cloud IP ranges outright. Your code isn't broken; your server's IP is. One more data point: my home IP also got blocked after roughly 20 requests in a single afternoon of testing. So "just run it locally" works for a handful of videos, not for a batch. Why it happens The library doesn't use an official API. It loads the video page like a browser would and then fetches the caption track. IP addresses of cloud and hosting providers are easy to recognize, and traffic from them looks like bots, because it usually is. YouTube refuses those requests, and the library surfaces that as RequestBlocked or IpBlocked. Fix 1: run it from a home connection (small volumes only) For a few videos, the plain library is all you need: from youtube_transcript_api import YouTubeTranscriptApi api = YouTubeTranscriptApi() transcript = api.fetch("aircAruvnKk", languages=["en"]) text = " ".join(snippet.text for snippet in transcript) print(text[:200]) Keep the volume low and space out your requests, or your home IP ends up blocked too. Fix 2: bring your own proxies The library has proxy support built in. With residential proxies from Webshare: from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api.proxies import WebshareProxyConfig api = YouTubeTranscriptApi( proxy_config=WebshareProxyConfig( proxy_username="your-username", proxy_password="your-password", ) ) transcript = api.fetch("aircAruvnKk") Or with any other provider: from youtube_transcript_api.proxies import GenericProxyConfig api = YouTubeTranscriptApi( proxy_config=GenericProxyConfig( http_url="http://user:pass@proxy.example.com:8000", https_url="http://user:pass@proxy.example.com:8000", ) ) Watch the bandwidth bill. Residential proxies are usually priced per GB. In my test, each video moved about 0.4 MB through the proxy (the library downloads the whole watch page, not just the captions). At around $8/GB, that's roughly $3 per 1,000 transcripts if every request goes residential. The cheaper pattern is what I ended up doing: try a datacenter proxy first, and fall back to residential only when it's blocked. Datacenter got 9 out of 10 through, so only the leftovers pay the residential price. That brought my measured cost down to about $0.50 per 1,000 transcripts. from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled, VideoUnavailable from youtube_transcript_api.proxies import GenericProxyConfig ROUTES = [DATACENTER_URL, DATACENTER_URL, RESIDENTIAL_URL, RESIDENTIAL_URL] # your proxy URLs FINAL = (NoTranscriptFound, TranscriptsDisabled, VideoUnavailable) # another IP won't fix these def fetch(video_id): last_error = None for url in ROUTES: api = YouTubeTranscriptApi(proxy_config=GenericProxyConfig(http_url=url, https_url=url)) try: return api.fetch(video_id) except FINAL: raise except Exception as exc: # blocked or flaky: try the next route last_error = exc raise last_error Two details that matter in practice: - Don't retry errors another IP can't fix. A video with captions disabled fails the same way from every IP; retrying it only burns proxy bandwidth. - Rotate the IP on every attempt. Most providers give you a new IP per connection or per session; make sure your retries actually change it. Fix 3: use a hosted scraper If you don't want to manage proxies at all, there are hosted tools that do this for you. Disclosure: I built one of them, YouTube Transcript Scraper on Apify. It runs the datacenter-then-residential routine above, takes URLs or IDs in bulk (Shorts included), lets you choose languages in order of preference, and returns plain text plus timestamped segments. It charges $3 per 1,000 transcripts delivered; failed videos are free. Calling it from Python with the official client: from apify_client import ApifyClient client = ApifyClient("YOUR_APIFY_TOKEN") run = client.actor("sturdyscrape/youtube-transcript-scraper").call( run_input={ "videos": [ "https://www.youtube.com/watch?v=aircAruvnKk", "https://youtu.be/dQw4w9WgXcQ", ], "languages": ["en"], "includeSegments": False, } ) for item in client.dataset(run.default_dataset_id).iterate_items(): print(item["videoId"], item["wordCount"], item["text"][:80]) There are other transcript tools on the Apify Store and elsewhere, at different prices; compare them for your volume. Which one should you pick? - A handful of videos, once: Fix 1. Free. - Regular batches and you're comfortable running infrastructure: Fix 2, with the datacenter-first fallback to keep the proxy bill down. - You just need the text inside a pipeline, an AI agent or a no-code workflow: Fix 3. Whatever you choose, the main lesson from the test holds: if your transcript code works locally and fails on the server, it's the IP, not the code. Have you found another way around the blocking? Tell me in the comments; I'm curious what's working for other people.

    1 reaction

Achievements

Awards earned by the repositories jdepoix owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.