Files
yt-channel-scraper/docs/audits/CLIPPER-COMPARISON-AUDIT.md
urieljareth 450aee216d docs: documentacion completa para humanos y agentes IA
- README reescrito: ejemplos CLI corregidos (flags van en el subcomando scrape),
  webapp+doctor documentados, requisitos por OS, cookies, datos generados, migracion de maquina
- CLAUDE.md actualizado: conteo de tests, refs de linea reparadas, scripts nuevos,
  checklist de actualizacion de docs
- AGENTS.md nuevo: entrada estandar para cualquier agente de codigo (mapa de modulos,
  comandos, reglas de oro, punteros a docs)
- docs/GETTING-STARTED.md: de cero a webapp por OS con troubleshooting
- docs/CONFIG.md: referencia completa de las ~25 claves de config
- docs/COOKIES.md: adquisicion, import (web/navegador/CLI), rotacion y seguridad
- docs/backlog.md reemplaza OPPORTUNITIES.md (distilado: shipped vs pendiente)
- auditorias movidas a docs/audits/ y CLIPPER-COMPARISON-AUDIT.md versionado
2026-09-10 00:32:32 -06:00

11 KiB
Raw Permalink Blame History

Auditoría comparativa · Obsidian Web Clipper 1.7.1 vs yt-channel-scraper

Producto auditado: Obsidian Web Clipper 1.7.1 (extensión MV3) en D:\Obsidian Web Clipper - Chrome Web Store 1.7.1.0. Sistema actual: yt-channel-scraper (yt-dlp como única interfaz con YouTube; techo práctico ~300 videos/h). Alcance: solo auditoría, comparación y análisis de mejoras. Sin cambios de código. Base: verificación directa del bundle popup.js (offsets 380236–393083, clase YoutubeExtractor) además del doc previo YOUTUBE-TRANSCRIPT-AUDIT.md. Fecha: 2026-09-01.


0 · TL;DR

La extensión resuelve el mismo problema (metadatos + transcripción de YouTube sin API oficial) con una filosofía de red opuesta y complementaria a la del scraper:

Scraper (hoy) Extensión
Filosofía ante el fallo Backoff temporal: esperar y reintentar más tarde (Pacer + ThrottleGuard, abort limpio) Rotación de identidad: cambiar de cliente/recurso y degradar, casi nunca esperar
Requests por video 3 (watch + player + timedtext), medidos irreducibles vía yt-dlp (config.yaml:21-33) 1–2 (player InnerTube + timedtext); el 3º (next) solo si faltan capítulos
Identidad 1 cookie activa, cliente yt-dlp default, sin rotación (cookies.py:151-163) 3 clientes InnerTube en cascada (IOS → ANDROID+UA → WEB), sin cookies
Retry del mismo recurso Nunca (deliberado, ratelimit.py:18-22) Nunca tampoco: 1 intento por cliente, error silenciado, siguiente
Timeout 15–30 s (_yt_http.py:40, config.yaml:55) 4 s por intento (AbortSignal.timeout(4e3))
Criterio de éxito HTTP status (429/403 → clasificar y abortar) Contenido: r.ok && captionTracks.length > 0 — un 200 vacío se trata como fallo y se rota
Degradación del resultado skip_reason con taxonomía de causas Siempre entrega nota con metadatos; transcripción es opcional

Los insights accionables para el scraper están en §2, priorizados en §3.


1 · Qué hace la extensión (verificado en el bundle)

Cadena de extracción de YoutubeExtractor.extractAsync() (offset ~380236 de popup.js):

  1. DOM existente (costo 0): segmentos ya renderizados en la página.
  2. Ruta de red principal fetchTranscript():
    • fetchChapters(videoId) se dispara sin await (la promesa se resuelve en paralelo).
    • Track inline desde ytInitialPlayerResponse del DOM (costo 0), validando que videoDetails.videoId coincida con el de la URL (getValidatedPlayerResponse).
    • Si no hay inline: fetchPlayerData(videoId) → POST a youtubei/v1/player?prettyPrint=false con cascada:
      1. {clientName:"IOS", clientVersion:"20.10.3"} — sin UA especial.
      2. {clientName:"ANDROID", clientVersion:"20.10.38"} + User-Agent: com.google.android.youtube/20.10.38 (Linux; U; Android 14).
      3. {clientName:"WEB", clientVersion:"2.20240101.00.00"}.
      4. Fallback final: JSON embebido del DOM.
    • Cada intento: timeout 4 s, try{}catch{} silenciado, y se acepta solo si captionTracks.length > 0.
    • Descarga del track: GET track.baseUrl con guard de host (new URL(baseUrl).hostname.endsWith(".youtube.com")), UA Mozilla/5.0, Accept-Language si hay idioma preferido, timeout 4 s.
  3. Apertura programática del panel de transcripción (click + polling pollFor cada 250 ms, máx 20 intentos) como último recurso.

Capítulos (fetchChapters): primero inline desde ytInitialData (playerOverlays…multiMarkersPlayerBarRenderer.markersMap); si vacío, POST a youtubei/v1/next con cliente WEB; segundo fallback engagementPanels[*].macroMarkersListItemRenderer.

Puntos de red relevantes:

  • Los POST a InnerTube no llevan cookies (el fetch de la popup corre en contexto de extensión, credentials same-origin ⇒ youtube.com no recibe sesión). La ruta IOS/ANDROID funciona anónima.
  • El Origin: https://www.youtube.com / Referer los fuerza la regla DNR 9002 porque un browser no puede setear Origin — en Python sería simplemente otro header.
  • BilibiliExtractor (mismo bundle) sí cachea transcripciones: LRU Map con tope 300 entradas. YoutubeExtractor no cachea nada.

2 · Insights accionables para el scraper

I1 · Ruta InnerTube propia como modo degradado (impacto alto, esfuerzo medio)

Evidencia: la extensión obtiene metadatos completos + captionTracks con un solo POST anónimo a youtubei/v1/player con cliente IOS. videoDetails da título, autor, channelId, lengthSeconds, viewCount, keywords; microformat da publishDate, description, ownerChannelName. Nada de watch page.

Aplicación: hoy, cuando el ThrottleGuard trip (ratelimit.py:221-283), el job aborta limpio y todo queda pending hasta el siguiente pase del monitor. Una vía de salvage — POST directo a player (1 petición/video en vez de 3) para lo estrictamente necesario (transcripción + metadatos básicos) — permitiría seguir produciendo a ⅓ del costo durante los periodos en que la ruta completa (watch page incluida) está bloqueada. _yt_http.py ya es el lugar natural para ese cliente.

Advertencias:

  • config.yaml:28-32 dice "3 requests irreducibles — no re-litigar". Esa medición fue sobre yt-dlp restringido (player_skip=webpage, single-client), que pierde pistas. La ruta de la extensión es distinta: una llamada InnerTube propia con aceptación por contenido. No la invalida, pero habría que medirla antes de tratarla como reemplazo; como modo degradado opcional el riesgo es acotado.
  • Las versiones de cliente hardcodeadas de la extensión (20.10.3 / 20.10.38 / 2.20240101) tienen más de un año de rotación. Si se implementa, tomar las versiones vigentes de yt-dlp (que ya las mantiene) en vez de hardcodear, o aceptar el mismo mantenimiento que la extensión.
  • YouTube exige PO tokens en algunos clientes para formats/streaming; para metadatos/captions la ruta IOS/ANDROID ha seguido funcionando sin ellos (es la evidencia de esta extensión), pero es el punto que puede romperse.

I2 · Aceptación por contenido, no por status (impacto medio, esfuerzo bajo)

La extensión trata "HTTP 200 con respuesta inútil" como fallo y rota. El scraper ya clasifica causas (skip_reason, describe_missing_subtitle), pero la aceptación es binaria por status. Aplicable a: timedtext que devuelve 200 con cuerpo vacío/corrupto (hoy parsearía vacío y se marcaría "parsed empty" en vez de reintentable), y a cualquier futura llamada InnerTube propia.

I3 · Timeout corto con fail-fast en timedtext (impacto medio, esfuerzo bajo)

extract.py:301-321 baja subtítulos con timeout 15 s; config.yaml:55 pone 30 s de socket. El punto donde el throttling "más aparece" es precisamente timedtext (extract.py:302-307). Un timeout más agresivo (configurable, p. ej. 6–8 s para json3/srv1 — payloads pequeños) convertiría cuelgues de 15–30 s en un fallo clasificable como reintentable casi inmediato, liberando el pacer antes. La extensión usa 4 s para todo.

La extensión no rota cookies (viaja sobre la sesión real del usuario), pero su patrón estructural — ante el bloqueo, cambiar de identidad en vez de solo esperar — traducido al scraper es: el vault ya persiste múltiples cookies (cookies/<uuid>.txt + cookies_meta), pero resolve_active_path usa exactamente una (cookies.py:151-163). Al trip del breaker, cambiar a la siguiente cookie no expirada antes de rendirse al reloj multiplicaría el presupuesto efectivo por sesión sin nueva infraestructura. (Insight inspirado en el patrón de la extensión, no copiado de ella.)

I5 · youtubei/v1/next para capítulos sin watch page (habilitador de I1)

Si algún día se activa la ruta de 1 petición (I1), los capítulos —que hoy llegan vía info de yt-dlp desde la watch page (chapters.py:39-49)— se recuperan con un POST a next (cliente WEB) parseando playerOverlays…markersMap, con fallback a engagementPanels. El bundle de la extensión contiene la implementación de referencia exacta (offset ~393083). Solo necesario para videos con capítulos; el resto no paga el request.

I6 · Guard de host antes de descargar timedtext (hardening, esfuerzo mínimo)

fetchCaptionXml exige hostname.endsWith(".youtube.com") antes del GET al baseUrl del caption track. El scraper baja pick.url con yt_get sin validar host (extract.py:301-321). La URL viene de yt-dlp (confiable hoy), pero una línea de validación cierra la clase de riesgo "baseUrl corrupto/inyectado ⇒ GET con headers de navegador a un host arbitrario".

I7 · Detalles menores de protocolo (gratis si se implementa I1)

  • ?prettyPrint=false en llamadas InnerTube: menos payload.
  • Origin: https://www.youtube.com como header explícito en POSTs propios.
  • Lanzar la descarga dependiente como promesa paralela (la extensencia dispara fetchChapters sin await): en el scraper el equivalente sería solapar la descarga de timedtext con el siguiente video del pacer solo si el presupuesto de requests ya lo contempla — cuidado: hoy el pacer es la política de cortesía, no paralelizar contra él.

I8 · Paridades confirmadas (sin acción)

  • Selección de pista por idioma: pick_subtitle del scraper (política por idioma, rechazo de tlang=, detección -orig, prioridad json3) es más rica que pickCaptionTrack/findPreferredCaptionTrack de la extensión.
  • Degradación graciosa del resultado: taxonomía skip_reason ≥ "nota siempre con metadatos" de la extensión.
  • Caching: SQLite del scraper > LRU 300 del BilibiliExtractor; YoutubeExtractor ni siquiera cachea.
  • Validación de JSON inline contra videoId (getValidatedPlayerResponse): patrón correcto a recordar si algún día se cachean player responses o se reutilizan continuations entre sesiones (evita atribuir a un video la respuesta de otro).

3 · Priorización sugerida

# Mejora Contra qué límite ayuda Esfuerzo Riesgo
1 I3 timeout fail-fast en timedtext Throughput bajo throttling Bajo Bajo (hacerlo configurable)
2 I6 guard de host Hardening Mínimo Nulo
3 I4 rotación de cookies al trip del breaker Techo de presupuesto por sesión Medio Medio (cuenta de la cookie expuesta al mismo ritmo)
4 I1+I2+I5+I7 ruta InnerTube propia como modo degradado Bot wall: seguir produciendo a ⅓ de costo Medio-Alto Medio (requiere medición; mantenimiento de client versions)

4 · Qué NO copiar de la extensión

  1. Scraping del DOM del panel de transcripción (clicks + polling): requiere un browser real con fingerprint real; el scraper es headless. Solo cobraría sentido con Playwright + perfil real, que es otra conversación.
  2. Re-litigar los 3 requests/video vía yt-dlp (config.yaml:28-32): la medición del repo sigue en pie para yt-dlp. La vía InnerTube propia es una ruta paralela degradada, no un reemplazo de la ruta completa.
  3. Silenciado de errores (try{}catch{} sin logging): la extensión degradea muda; el scraper necesita auditabilidad (skip_reason ya la da).
  4. Versiones de cliente hardcodeadas: la extensión las parchea por release; el scraper ya delega eso en yt-dlp. Cualquier ruta propia debe heredar las versiones de yt-dlp, no duplicarlas.

Fin del informe.