La casa de moneda: acuña el PO Token que YouTube empezó a exigir para entregar audio y video. Un binario, sin instalar nada, sin tocar las cuentas de nadie.
Desde 2026 YouTube exige un PO Token (proof-of-origin) en todos sus clientes de
reproducción. Sin él, la extracción funciona —título, duración y calidades salen bien— pero
bajar el medio devuelve HTTP Error 403: Forbidden. Medido acá contra yt-dlp 2026.7.4:
falla con todos los clientes (android_vr, web, web_safari, ios, mweb,
tv_simply), con y sin runtime JS, y con y sin el solver de challenges. No es un problema
de versión: 2026.7.4 era la última publicada.
La alternativa habitual es pasarle a yt-dlp las cookies del navegador. Esto existe para no hacer eso: en un equipo donde la app la usan varias personas, leerles el navegador no es una opción.
sesión anónima nueva -> visitor_data
desafío de BotGuard -> intérprete JS -> integrity token -> PO Token
Sale una línea de JSON por stdout:
{"visitorData":"CgtLNVMz…","poToken":"MtYEKUO4B0oQ…"}Los dos viajan juntos y eso no es comodidad: el token que destraba las URLs del servidor
de video (gvs) se ata al visitor_data de la sesión. Usarlo con otro da un 403 idéntico al
de no tener token.
eval "$(ceca | python -c 'import sys,json; d=json.load(sys.stdin); print(f"VD={d[\"visitorData\"]}; PT={d[\"poToken\"]}")')"
yt-dlp --extractor-args "youtube:player_client=tv_simply;po_token=tv_simply.gvs+$PT;visitor_data=$VD" \
-f bestaudio "https://www.youtube.com/watch?v=…"tv_simply no es decorativo. Con el mismo token, web devuelve solo miniaturas, y mweb
e ios siguen dando 403. Es el único cliente que se verificó bajando de punta a punta.
Cuatro cosas que costaron tiempo y cuyo error no nombra su causa:
| Síntoma | Causa |
|---|---|
400 … JSPB Fava message don't accept top-level braces |
getHeaders() de bgutils-js manda content-type en minúscula. Pisarlo con Content-Type no reemplaza: viajan las dos y YouTube lee application/json+protobuf, application/json |
| Token válido que igual da 403 | Se ató al id del video (que es lo intuitivo) en vez de al visitor_data. El de id sirve para otro contexto |
Only images are available for download |
El cliente web necesita más que el token gvs. Usar tv_simply |
Not implemented: HTMLCanvasElement's getContext() |
Es un aviso, no un error: canvas no hace falta. Se verificó bajando video sin él, y evita un módulo nativo que hay que compilar por plataforma |
deno task test # sin red
deno task mint # acuña uno de verdad
deno task compile # dist/ceca, un ejecutable autocontenidoEl binario compilado pesa ~110 MB (Deno mete su runtime adentro) y no necesita Node, npm ni Deno instalados en la máquina donde corre.
Funciona sin tocar nada: Deno respeta HTTP_PROXY y HTTPS_PROXY del entorno, y un
proceso lanzado por otra aplicación hereda esas variables.
Verificado con un control, que es la única forma de saberlo: apuntando HTTPS_PROXY a un
puerto muerto la corrida falla con fetch failed, y sin esa variable la misma corrida
acuña normalmente. Si diera lo mismo en los dos casos, el proxy se estaría ignorando.
Este programa ejecuta JavaScript ofuscado que baja de YouTube. Es inevitable: el desafío de BotGuard es ese programa, y resolverlo es todo el punto. Por eso vive en un proceso aparte en vez de adentro de la app que lo usa, y por eso se compila con los permisos de Deno acotados: red sí, disco no. Aunque ese código quisiera hacer algo más, no puede.
No usa cuentas, cookies ni credenciales de nadie. La sesión es anónima y nueva en cada corrida.
El trabajo pesado —la reimplementación del cliente de BotGuard— es de
bgutils-js (MIT), y el visitor_data sale de
youtubei.js (MIT). Acá solo está el pegamento.
Existe bgutil-ytdlp-pot-provider, que hace esto y bastante más (sesiones múltiples, proxies, plugin de yt-dlp). Este repo no existe porque aquel esté mal: existe porque es GPL-3.0, y para poder distribuirlo dentro de una aplicación propia hacía falta algo MIT.