Repository navigation
fix: bundle a JS engine so Made-for-Kids videos download at full quality - #32
Merged
Merged
Conversation
Reported from the church PC (feedback #17): a pasted link failed with "This video is not available" while the same video plays fine in a browser. The video is not unavailable. yt-dlp asks YouTube through one of several InnerTube clients, and the whole `Mount Moriah Sabbath School` channel is refused by the ones yt-dlp reaches for by default. Measured 2026-09-07 on yt-dlp 2026.08.19, the version v1.6.0 bundles: visionos, android_vr -> playabilityStatus UNPLAYABLE, no streamingData web, ios, mweb, tv -> player response, but stream URLs withheld (SABR-only / a GVS PO Token demanded) android -> serves it, as muxed 360p format 18 yt-dlp surfaces the first client's own reason verbatim, so the app repeated YouTube's misleading wording as if the video were gone. So a failure is now treated as inconclusive: download_video retries once with player_client=default,android,ios,web_safari,tv. `default` stays first, or the retry would trade a good 1080p stream for whatever the older clients offer. The retry is deliberately not gated on the error text - YouTube's messages churn, and matching them is a fix that quietly stops working - and the error handed back stays the *first* attempt's, since the retry ran against clients the user never chose. A retry starts from zero bytes, so it emits a new PROGRESS_RESET_STATUS event before its plan. DownloadProgress only moves forward; without it the bar would report the abandoned attempt's high-water mark. Verified end to end on the reported link: fails on the first attempt, succeeds on the retry, and the file has both a video and an audio stream. A normal video still downloads on the first attempt with no extra round-trips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The real cause behind the report the previous commit patched over.
yt-dlp solves YouTube's signature / "n" challenge by running YouTube's own
JavaScript, and that needs a JS engine on the machine. The app shipped none,
and yt-dlp auto-enables only `deno` — so the church PC (no deno, node, bun or
qjs anywhere) had no engine at all.
Videos that demand the challenge release no adaptive-format URL until it is
solved. Adaptive formats are the only route above 360p, so every one of them
gets dropped, yt-dlp falls through its client list, `visionos` refuses, and
YouTube's flatly wrong "This video is not available" is what the user sees.
The discriminator is YouTube's "Made for Kids" flag. Swept 22 videos, no engine
vs quickjs: 8/8 kids videos failed, 0/10 non-kids failed, and every failure
returned 1080p with an engine. The kids set spanned five unrelated channels plus
Cocomelon and Baby Shark, 2021 and 2026 uploads, all `People & Blogs`, with
`isFamilySafe: true` in both groups. That is why nothing had ever failed before:
an ordinary video is served by `visionos` with clean URLs and never needs
JavaScript.
quickjs-ng publishes static single-file builds of ~2 MB, fetched per platform in
release.yml (pinned by QUICKJS_TAG) into `app/tools/quickjs_<platform>/`, which
the spec already bundles wholesale. `config.py` resolves it into
`js_runtime_path`, recomputed on every load like mpv/ffmpeg.
A missing engine stays silent rather than adding a user-facing warning: it is
not exposed in Settings, a source checkout has none, and an empty path just
means "let yt-dlp auto-detect". `download_video` logs which engine it used, so
a build that shipped without the binary — or lost its executable bit in
packaging — shows up in the log instead of quietly reverting.
The player-client retry from the previous commit stays as a safety net behind
this: it rescues such a video at 360p where the engine gets 1080p, which still
matters if the binary is missing or broken.
Verified through the app's own download_video with the binary placed exactly
where a build puts it: the reported link now succeeds on the FIRST attempt at
1920x1080 (av1 + opus, 7.27 MB) with no retry, where before the retry rescued
it at 640x360. Both weekly channel videos pick format 399+251 with and without
the engine, so nothing about the Sabbath downloads changes.
Note: js_runtimes must be a dict of {runtime: {config}} — a list raises a bare
ValueError from YoutubeDL.__init__ that names no option.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
Feedback #17, from the church PC (v1.6.0): a link pasted into Others
failed with
ERROR: [youtube] Mg1Aa34cHjI: This video is not available— whilethe same video plays fine in a browser. The church PC's own log matches a Pi
reproduction line for line.
Root cause
The app shipped no JavaScript engine.
yt-dlp solves YouTube's signature /
nchallenge by running YouTube's ownJavaScript, which needs a JS engine on the machine — and yt-dlp auto-enables
only
deno. The church PC has no deno, node, bun or qjs anywhere (wherefinds none).
Videos that demand the challenge release no adaptive-format URL until it is
solved. I parsed the player response directly: all 24 adaptive formats up to
1080p60 are listed, every one with
url=no. Adaptive formats are the only routeabove 360p (the sole surviving muxed format is 360p), so all of them get
dropped, yt-dlp falls through its client list,
visionosanswersUNPLAYABLE,and YouTube's flatly wrong reason string is passed through verbatim.
The discriminator is YouTube's "Made for Kids" flag
Swept 22 videos, no engine vs
--js-runtimes quickjs:8/8 kids fail, 0/10 non-kids fail. Not channel-, age- or category-bound:
failures span five unrelated channels and 2021/2026 uploads, all
People & Blogs,isFamilySafe: truein both groups. Confirmed by commentsbeing force-disabled (a hard COPPA consequence creators cannot override) on 4/4
failing and 0/3 working.
isMadeForKidsis absent from both the web and androidplayer responses, so the flag cannot be read directly.
That is why nothing had ever failed before: an ordinary video is served by
visionoswith clean URLs and never needs JavaScript at all.The fix
quickjs-ng publishes static single-file builds of ~2 MB, fetched per
platform in
release.yml(pinned byQUICKJS_TAG) intoapp/tools/quickjs_<platform>/— which the spec already bundles wholesale, sono spec change.
config.pyresolves it intojs_runtime_path, recomputed onevery
load_settings()likempv_path/ffmpeg_path.A missing engine stays silent rather than adding a user-facing warning: it
is not exposed in Settings, a source checkout legitimately has none, and an
empty path just means "let yt-dlp auto-detect" — the old behaviour.
download_videologs which engine it used instead, so a build that shippedwithout the binary, or lost its executable bit in packaging, shows up in the log
rather than quietly reverting to 360p-or-nothing.
The player-client retry (first commit) stays as a safety net behind the
engine: it rescues such a video at 360p where the engine gets 1080p, which still
matters if the binary is missing or broken, and for whatever YouTube does next.
Verification
Through the app's own
download_video, binary placed exactly where a build putsit:
Both weekly channel videos pick
399+251with and without the engine, sonothing about the Sabbath downloads changes.
303 passed. The new config testswere mutation-checked (breaking the Windows path, and adding a warning on a
missing engine, each fail the intended test).
Notes
js_runtimesmust be a dict of{runtime: {config}}. A list raises abare
ValueErrorfromYoutubeDL.__init__that names no option.deno. Node can sit onPATHcompletely unused,which is what makes "no JS runtime" so easy to misdiagnose — it briefly fooled
me into thinking the yt-dlp version mattered.
minted, nothing changed); forcing
player_client=webis SABR-onlyregardless, which sends you chasing yt-dlp #12482; upgrading yt-dlp
changes nothing (stable 2026.08.19 == nightly 2026.08.30); the
androidclient alone "works" but serves only muxed 360p — a trap that looks like a fix.
release (same as fix: ship the app icon at every size Windows asks for #31). The second commit's subject has none either; the first
one's does.
🤖 Generated with Claude Code