Contexto: yt-extract — API quirks + deck TweetDeck (operativo, no re-derivar)

ENDPOINT videoTrainability (el observatorio):
  GET https://www.googleapis.com/youtube/v3/videoTrainability?id=VIDEO&key=KEY
  - Funciona con SOLO API key (sin OAuth).
  - 1 video por llamada (NO acepta lote: si mandas id="a,b" devuelve el string
    concatenado con permitted ["None"], sin resultados por video). ~1 unidad/call.
  - permitted: ["None"] (opt-out, DEFAULT) | ["All"] | [lista de parties].
  - Todo el trending MX + canales probados => OUT. El opt-in es raro; esa es la señal.
 
RESOLVER CANAL (yt/live.py resolve_channel_id):
  - forHandle NO es confiable (falla con handles reales ej @elchapucero, @ElFinancieroTV).
  - Usar fallback: forHandle -> search.list(type=channel). IDs "UC..." pasan directo.
  - CUIDADO: un customUrl puede apuntar a un canal VIEJO del mismo nombre.
    Ej: @aristeguionline -> canal con 5 videos de 2015-2017. El activo es otro ID.
    => siempre verificar edad del último video (stale flag).
 
COSTOS (quota 10,000/dia):
  search.list = 100 unidades (caro). videos.list / channels.list / playlistItems /
  videoTrainability ~= 1 unidad cada uno. Patrón barato por canal (~3 uds + N trainability):
    channels.list(contentDetails) -> playlistItems(uploads) -> videos.list -> N x trainability.
 
DECK (yt/deck.py): feeds.json -> build_deck -> print_deck (terminal) + deck.html (TweetDeck).
  feeds.json: {trending:{region,max}, per_channel, stale_days, channels:[ids/nombres]}.
 
HALLAZGO BUG ORGANICO: la señal de "repetibilidad" del dataset Kaggle (2018) es
a NIVEL CANAL, no video (15 reentradas de 33k). En vivo, lo análogo es:
  - "stale channel" (canal sin actividad reciente)
  - opt-in ALL vs OUT por canal.