Skip to content

YouTube imports fail: JS challenge solver (EJS remote component) is never enabled #432

Closed

Description

@diegoarusso

What happens

Every YouTube import fails on a fresh Windows portable install. The UI shows
Audio processing failed. Please try another video. and the job's
error_detail is unknown, so the cause is only visible in data/logs/.

There are actually two independent blockers stacked on top of each other.
This issue is about the second one; I'll open a PR for the first separately.

1. Bot check (cookies)

HTTP Error 429: Too Many Requests
ERROR: [youtube] <id>: Sign in to confirm you're not a bot.

yt-dlp's documented remedy is to send the user's cookies, but there is no way
to configure that today — pipeline/download.py never sets cookiefile or
cookiesfrombrowser. I have a change for this ready and will open it as a
draft PR.

2. JS challenge solver — the part I'd like guidance on

With cookies configured, the bot check clears and the failure moves further
down the pipeline:

WARNING: [youtube] [jsc] Remote components challenge solver script (deno) and
NPM package (deno) were skipped. These may be required to solve JS challenges.
You can enable these downloads with --remote-components ejs:github
WARNING: [youtube] <id>: n challenge solving failed: Some formats may be missing.
WARNING: Only images are available for download.
ERROR: [youtube] <id>: Requested format is not available.

The metadata probe succeeds, then every media format is filtered out, so
format: "bestaudio/best" matches nothing.

Recent yt-dlp moved YouTube's signature/n-challenge solver ("EJS") out of the
package into a component fetched at runtime, disabled by default. StemDeck
never enables it, so on a current yt-dlp the download cannot resolve a format.

Confirmed with the bundled interpreter, same URL, only the flag differing:

# fails
python -m yt_dlp --cookies-from-browser firefox -f bestaudio --simulate <url>
  -> ERROR: Requested format is not available

# works
python -m yt_dlp --cookies-from-browser firefox -f bestaudio --simulate \
  --remote-components ejs:github <url>
  -> format 251 webm 137k

Not a PATH problem: it fails identically with a JS runtime (deno 2.9.1) on
PATH. The missing piece is the solver script, not the runtime.

Worth noting the version direction is counter-intuitive: an older
standalone yt-dlp (2026.06.09) resolves the format fine, while the newer
bundled one (2026.08.19) does not — because the newer one expects the
component that is off by default.

Why I'm opening an issue instead of a PR

Enabling remote_components makes yt-dlp download and execute a solver script
from the yt-dlp GitHub org at runtime. That is a trust/threat-model decision
for the project, not something a drive-by PR should decide, and it interacts
with the CSP/SSRF care already visible in download.py. Options as I see them:

  1. An opt-in setting, defaulting off (mirrors yt-dlp's own default). This is
    what I prototyped locally to confirm the diagnosis.
  2. Enabled by default, on the grounds that YouTube import is a headline feature
    and is otherwise broken out of the box.
  3. Ship the EJS component inside the portable build so nothing is fetched at
    runtime.
  4. Pin the bundled yt-dlp to a version that does not need it (stopgap only).

Happy to send a PR for whichever direction you prefer.

Unrelated but adjacent

error_detail is unknown for these failures, so diagnosing anything here
means reading data/logs/. Propagating yt-dlp's message would have made both
of the above self-evident. Can split that into its own issue if useful.

Environment

  • StemDeck v0.13.0, Windows x64 portable (CPU-only)
  • Windows 11, bundled yt-dlp 2026.08.19
  • Reproduced on multiple videos

Activity

  1. self-assigned this
    on Aug 25, 2026
  2. thcp commented on Aug 25, 2026

    @thcp
    Collaborator

    Thanks for this. The diagnosis is right, and opening it as an issue rather than a PR was the right call. Enabling a runtime code fetch is not something a drive-by change should decide.

    There is a fifth option none of us listed, and it removes the trust question instead of answering it. yt-dlp-ejs is on PyPI. It is a 53 KB pure-Python wheel with no dependencies, and yt-dlp checks for it as an installed package before it considers any remote source. So the solver ships with the app, nothing is fetched at runtime, and --remote-components stays off.

    I reproduced your failure exactly. Forcing a client that needs the challenge, with a runtime present and no solver:

    WARNING: Remote component challenge solver script (node) was skipped
    WARNING: n challenge solving failed
    WARNING: Only images are available for download
    ERROR: Requested format is not available
    

    Installing the package clears it. The solver runs and the format resolves. I also measured quickjs-ng against node, since a 2 MB engine against deno's 110 MB matters a lot for a portable build. quickjs solves it in about 10 seconds versus node's 4. That sits in front of a separation that takes minutes, so it is affordable, but a second unsigned binary in the bundle and three build scripts to fetch it is a packaging decision I want to look at properly before committing to it.

    One thing your report helped me find that I would not have looked for otherwise. The two blockers are not independent. Supplying cookies makes yt-dlp drop every client that does not support them, and those are exactly the clients resolving formats today without any solver. So cookies alone would have made imports worse for people who currently work. That is why the cookie setting I just landed is a fallback rather than a default: the first attempt never carries cookies, and they are only used after YouTube has actually turned us away. By then the path they would have displaced has already failed.

    Landed now:

    • A cookies.txt path in Settings, empty by default, applied only on a bot check
    • Failure classification for source-blocked and source-unavailable, so error_detail carries the real yt-dlp message instead of unknown. You were right that this was the thing making everything else hard to see, and it was two separate gaps, not one

    The solver itself is not settled yet. I am holding it for further investigation and tracking it separately in #438, which records everything above so none of it has to be worked out again. Extraction still works today through the fallback clients, so this is not urgent, but yt-dlp has deprecated that path and it will not last forever.

    One caveat worth setting now. Even with cookies and the solver, a flagged IP may still be short a PO token, which yt-dlp cannot generate on its own. My test of the web client failed for that reason with the solver working fine. So this may improve your case without fully fixing it, and I would rather say that up front than have you find out after the release.

    If you still have the setup that reproduced this, I would be glad to know whether the cookies option alone moves the needle for you, and later how it behaves once the solver lands. Let me know how it goes.

  3. diegoarusso commented on Aug 25, 2026

    @diegoarusso
    Author

    Thanks — the yt-dlp-ejs pointer was the missing piece, and it does exactly
    what you said. Confirmed on the Windows x64 portable build: pip install yt-dlp-ejs into the bundled interpreter, remote_components left off, and a
    YouTube import now runs end to end. Nothing fetched at runtime.

    Did cookies alone move the needle?

    No — and that lines up with your fallback design.

    With cookies configured and no solver, the bot check cleared but the import
    still failed, just further down:

    n challenge solving failed: Some formats may be missing
    Only images are available for download
    ERROR: Requested format is not available
    

    So on this setup cookies alone were necessary but not sufficient. The import
    only worked once the solver was present. Worth knowing if you were expecting
    the cookies release alone to unblock people.

    Your point about cookies displacing the clients that resolve formats without a
    solver also explains something I had written off as noise: the older
    standalone yt-dlp on this machine (2026.06.09) resolves formats fine with no
    solver at all, while the bundled 2026.08.19 does not. I had put that down to a
    version difference. Making cookies a fallback rather than a default is clearly
    the right call, and my patch applied them unconditionally on every request, so
    it would have hit exactly the regression you describe. Not pursuing it.

    One data point for #438: node is not detected, deno is

    Since you were weighing quickjs-ng against node, this may be worth checking
    before you commit to a runtime. With yt-dlp-ejs 0.8.0 installed and only the
    PATH differing:

    # node v24.15.0 on PATH
    [debug] JS runtimes: none
    [debug] [youtube] [jsc] JS Challenge Providers: bun (unavailable),
            deno (unavailable), node (unavailable), quickjs (unavailable)
    ERROR: Requested format is not available
    
    # deno 2.9.1 on PATH
    [debug] JS runtimes: deno-2.9.1
    [debug] [youtube] [jsc] JS Challenge Providers: bun (unavailable), deno,
            node (unavailable), quickjs (unavailable)
    -> format 251 resolved
    

    Note JS runtimes: none in the first case: node is present and executable but
    never registers as a runtime at all, so this looks like detection rather than a
    solving failure. I have not dug into why. One guess is that the PyPI
    distribution carries the bundle deno can consume and that node wants the npm
    one — the original warning did name the script and the NPM package separately —
    but that is speculation on my part and I have not verified it.

    Environment: Windows 11, StemDeck v0.13.0 portable, bundled yt-dlp 2026.08.19,
    Python 3.12.7, yt-dlp-ejs 0.8.0. Single video, so treat it as one data point
    rather than a general claim. Happy to run whatever else is useful — I still
    have the setup.

    PO token

    No sign of it here so far. With cookies plus the solver, format 251 resolves
    and the download completes, so whatever client that path lands on is not
    short a token on this IP. If that changes I will report back.

    On the diagnostics

    The failure classification is going to save the next person a lot of time.
    error_detail: unknown meant every one of these had to be diagnosed by reading
    data/logs/ — I only found the second blocker because the first one's message
    disappeared and something different showed up underneath it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions