Releases: 2scraper/youtube-scraper
Release list
v0.2.0 — four silent successes from the audit, and an HTTP transport
Written after a third-party audit. Four of its findings were correctness
defects that reported SUCCESS, which is this codebase's most expensive bug
class — no test failed, no run crashed, and the output looked right.
If you branch on
statusor on the exit code, read this. A run that
lost reply threads, or whose--mode videosecond call failed, used to
reportstatus: completeand exit0. It now reportspartialand
exit6. Pipelines that treatedcompleteas "everything arrived" were
being told something untrue; pipelines that treat6as a hard error
will now see runs they previously saw as clean.
Fixed
- A run with failed pages called itself complete.
finish_rundecided
completeness fromstop_reasonalone, and a named list of reasons
cannot cover a failure recorded anywhere else. Reproduced directly:
stop_reason="page_cap_reached", pages_failed=[3, 7]returned exit 0
andstatus: completewith both failures listed in the same sidecar.
Completeness now consults the evidence as well as the reason. Measured
across the family by CALLING each repo'sfinish_runrather than
grepping: 28 of 32 behaved this way. - Lost reply threads were invisible. A failed reply fetch put a thread
INDEX intopages_failed, a field holding top-level page NUMBERS — so
[3, 7]could mean either and nothing said which. Replies are now
accounted for separately:reply_threads_requested,…_completed,
…_failed, and areply_failureslist naming each thread by its parent
comment id, depth and state. Any failure makes the run partial;
pagination_stop_reasonkeeps the loop's own reason beside it, because
"we reached the page cap" and "three threads failed" are two facts. - A half-built video row reported success. When
/playerdid not
answer,--mode videologged a warning and returned a successful
outcome withpublished_at,duration_seconds,categoryand
keywordsall null — indistinguishable from a video that genuinely has
none. The row now records which sources built it (innertube.watch
againstinnertube.watch+player), the sidecar names the missing
columns, and the run is partial. --proxy-rotate per-pagedid not rotate per page.pool.advance()
was reached only from a dead exit or a refusal, so a run whose pages all
succeeded stayed on one address for its entire life. It now takes a new
exit between pages, in all three modes — and exactly N-1 times for N
pages, not N: the first version rotated after the last page too and
built a browser for a request that never came.- The Scraper API client wrote no sidecar, while the README promised
one beside every run that wrote output. It now callsfinish_runlike
the engines, and recordsplayer_fields_unreachable— those columns are
missing by ROUTE there (the service issues a GET;/playeris a POST),
which is a different fact from the engines' failure and should not read
as one. - Output files are written atomically. A kill or a full disk during a
write used to leave a truncated file where a complete one had been, with
a sidecar beside it still describing the old run. Writes now go to a
temporary file in the same directory, are flushed and fsynced, and are
renamed over the destination — so a reader sees the whole previous file
or the whole new one. - The cross-engine surface check never ran. It compared engines that
IMPORTED, and a supported virtualenv holds exactly one (§6 says install
one), so it compared one engine against nothing and reported itself
passed — 0 pairs, suite green. The audit found real divergence only by
installing all three, a configuration the README tells people not to
create. The comparison now reads the source, so it runs everywhere
including with no engine installed, and it immediately found the drift:
one method named_apply_client_hintsin one engine and
_apply_fingerprintin the others.
Added
--transport auto|http|browser, defaulting toauto. The endpoint
this repo reads answers plain HTTPS, which the README has said since
0.1.0 while every run started Chromium anyway. Measured end to end, two
pages, median of three: 2.0 s over HTTP against 3.4 s through a
browser, identical rows.autostarts a browser the first time a
response classifies as a challenge and stays on it for the rest of the
run. A browser is no longer needed to install or to use.- Fault-injection checks for all four correctness fixes above, each
driving the engine with the transport stubbed out, and each verified by
planting the fault back and watching the suite go red.
Notes
- One
--transport httprun in nine returned exit 1 during testing, once,
and did not reproduce in eight further attempts. Recorded rather than
explained away: it is either a transient network fault or something not
yet understood.
v0.1.0 — comments, video metadata and search
First release. Three modes, three browser engines, and a Scraper API
client, reading YouTube through the endpoint its own front end calls.
Added
--mode comments— a video's comment threads and, with--replies,
their replies. 20 top-level comments per page, 10 replies per page.
--sort top|newestselects between the site's two orderings.--mode video— one video's metadata, from two calls:nextfor
the rendered figures andplayerfor the exact upload date, the
duration in seconds, the category and the keyword list. The only mode
that can use--concurrency, because a video has its own address and a
comment page does not.--mode search— a query to videos, to feed--mode comments.playwright_scraper.py,puppeteer_scraper.pyand
selenium_scraper.py: one file with three driver layers, so the shared
half cannot drift. Verified live on 2026-09-21 — the same video and
ordering through all three gave 40 rows each, 40 ids in common and zero
disagreeing columns.scraper_api_client.pyfor--mode videoonly. It cannot read comments
on this site, and the measurement is in its docstring and the README.diff_runs.py, which refuses to compare two runs whose ordering differs
— on this site the ordering decides which comments a capped run holds,
not merely their order.- An offline suite of 639 checks that passes with no engine library
installed, andmake_fixtures.py, which proves every trimmed fixture
parses identically to its untrimmed original before writing it.
Measured, and worth knowing before choosing this
- The comment endpoint is not gated. 2026-09-21, from a bare
datacentre address in Finland with no key, no proxy, no cookies and no
account: 60 consecutive pages, 1,200 comments, zero refusals, ~0.25 s
per page. - YouTube publishes no exact like count for a comment, and no absolute
timestamp, to any client — checked under WEB, MWEB and TVHTML5 over 70
comments in four captures. Solike_countis a magnitude beside
like_count_text, andpublished_at_approxis derived, labelled with
published_at_precision, and null in any locale this parser does not
read. A video row has both exactly, which is why--mode videoexists. - The site serves two payload shapes. The WEB client returns the
entity form, where the comment text lives in
frameworkUpdates.entityBatchUpdate.mutationsand the renderer tree
holds only keys; the MWEB client still returns the legacy
commentRendererform. Both are parsed, both are pinned to a real
capture, anddata_sourcesays which was read. - The Scraper API cannot return comments here, because YouTube renders
none into its HTML and fetches them over a POST the service does not
make. AwaitForon the comment section times out (HTTP 408); a
networkidlewait returns 1.39 MB with zero comments. Control: a
deliberately wrong key answered 401 in 0.1 s against the real key's 200
in 6.7 s. - The Scraping Browser path is live-verified, on Playwright and
pyppeteer (Selenium cannot reach an authenticated CDP endpoint and
refuses it by name). A US profile returned comments, video metadata and
search normally on 2026-09-21. Its WebSocket upgrade answeredHTTP 500
on one of three attempts seconds apart, so both engines now retry it —
and its auto-solve extension injectscf-turnstileand fifteen other
captcha markers into pages the site served normally, which is why none
of them is in this repo's block-marker set and why a fixture cut from
such a page is now in the suite. - No challenge has ever been rendered to this scraper, and the served
page's own greps mislead:recaptchaappears once, in a CSS rule that
hides a badge, andbotguardthirteen times, all of them configuration
flags. The site's actual defence is its own attestation (bgChallenge),
which is not a solvable widget. The captcha machinery is carried as
readiness and says so.
Notes on what is not verbatim
fixtures_generated.json, sample_output.json and sample_output.csv are
cut from real runs and then anonymised: a commenter's display name,
channel id, avatar, comment id and own words are replaced with
placeholders. Everything the site generates around them — counts,
timestamps, badges, the video and its publisher — is untouched. A comment
is a person's writing, and republishing it is a separate act from YouTube
showing it on its own page.