Skip to content

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ceca

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.

License: MIT

Por qué existe

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.

Qué hace, exactamente

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.

Uso con yt-dlp

eval "$(ceca | python -c 'import sys,json; d=json.load(sys.stdin); print("VD=%s; PT=%s" % (d["visitorData"], 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.

Lo que se aprendió construyéndolo

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

Construir

deno task test                     # sin red
deno task mint                     # acuña uno de verdad
deno task compile                  # dist/ceca, un ejecutable autocontenido

El binario compilado pesa ~110 MB (Deno mete su runtime adentro) y no necesita Node, npm ni Deno instalados en la máquina donde corre.

Detrás de un proxy corporativo

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.

Tiempos límite

Cada pedido se corta a los 20 s, y ninguno sigue después de los 60 s de empezada la corrida. Los dos quedan por debajo de los 120 s que suele esperar quien lo llama. Así el motivo llega antes de que el otro se canse. El error dice en qué paso se colgó y cuál límite lo cortó:

att/get (desafio de BotGuard): sin respuesta en 20000 ms (CECA_REQUEST_TIMEOUT_MS)
GenerateIT (integrity token): se agoto el techo total de 60000 ms (CECA_TIMEOUT_MS)
Variable Qué limita Por defecto
CECA_REQUEST_TIMEOUT_MS cada pedido, incluida la lectura de la respuesta 20000
CECA_TIMEOUT_MS la corrida entera: cada pedido recibe como mucho lo que queda 60000

Tienen que ser enteros positivos. Cualquier otro valor corta la corrida de entrada, con un error que nombra la variable. Las dos se leen antes de soltar el entorno (ver Seguridad).

Seguridad

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 corre con los permisos de Deno acotados:

  • Red: solo www.youtube.com, www.google.com (de ahí baja el intérprete) y jnn-pa.googleapis.com (donde se canjea el integrity token). Cualquier otro host da NotCapable, también para un import() dinámico: deno run permite importar de esm.sh, jsr.io, deno.land y otros sin flag, así que ese permiso también se revoca antes de acuñar (ver el punto siguiente).
  • Disco y entorno: se sueltan antes de acuñar. dropLocalAccess() revoca lectura, escritura, entorno, sistema, procesos, FFI e importaciones remotas, y revocar no tiene vuelta atrás: el JS de YouTube recibe NotCapable si intenta leer un archivo o una variable. Los flags no alcanzan solos porque la carga de las dependencias los necesita: debug enumera todas las variables de entorno al cargarse (de ahí --allow-env sin lista), y desde el código fuente jsdom lee su hoja de estilos del caché de npm, en una ruta que depende de DENO_DIR (de ahí --allow-read en deno task mint). El binario compilado no lleva --allow-read: jsdom la lee de los archivos que van adentro del ejecutable.
  • --no-prompt: sin él, un permiso revocado que ese código pidiera se convertiría en una pregunta en la terminal.

Quien lance src/main.ts con sus propios flags igual pierde disco y entorno antes de acuñar, pero la lista de hosts depende de los flags: hay que copiar los de deno task mint.

No usa cuentas, cookies ni credenciales de nadie. La sesión es anónima y nueva en cada corrida.

Créditos y licencias

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.

About

Acuña el PO Token que YouTube exige para entregar audio y video. Un binario, sin cuentas ni cookies de nadie.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages