← Todos los artículos
Técnico

YouTube Like Shortcut por dentro: pulsar un botón en una web que cambia sin avisar

Cómo una extensión de una sola función llega antes que el reproductor de YouTube, encuentra el botón de like entre variantes ocultas y layouts que cambian con el ancho de la ventana, y se asegura de que el like ha entrado aunque YouTube esté reconstruyendo la página.

En TikTok, desde el ordenador, la L da like al vídeo que estás viendo. En YouTube no existe ese atajo, y pasando Shorts lo echaba de menos. Así que lo añadí con una extensión para mí: YouTube Like Shortcut. Si buscas qué hace y cómo se usa, eso está en el artículo para usuarios.

Sobre el papel es trivial: escuchar una tecla y pulsar un botón. En la práctica, el botón está en una web que no es tuya, que cambia de estructura según el ancho de la ventana y que se reconstruye cada vez que pasas de vídeo. Este artículo cuenta lo que hizo falta para que funcione siempre, no solo casi siempre.

La primera versión: 196 líneas

La v1 era un único content.js sin dependencias ni paso de compilación. Escuchaba keydown en el documento y, si la tecla era la L:

  • en un Short (/shorts/...), buscaba el Short activo y pulsaba su botón de like;
  • en un vídeo normal (/watch?v=...), buscaba el botón de like de la página y lo pulsaba.

Ya tenía dos piezas que siguen hoy: no hacer nada si estás escribiendo y buscar el botón con varias estrategias, por si YouTube cambiaba su HTML. Lo demás llegó después, casi siempre por algo que fallaba.

Llegar antes que el reproductor

En un vídeo normal, YouTube ya usa la L: avanza 10 segundos. La v1 cancelaba el evento con preventDefault(), pero escuchaba en document, en la fase de burbuja. Para cuando el evento llegaba ahí, el reproductor ya lo había recibido y el vídeo había saltado.

Dos días después cambié a la fase de captura en window. Un evento de teclado baja desde window hasta el elemento con el foco y luego sube; escuchando en captura en window, la extensión es lo primero que lo ve, antes que cualquier manejador de YouTube:

window.addEventListener('keydown', handleKeydown, true)

// ...dentro de handleKeydown, cuando la tecla coincide:
event.stopImmediatePropagation()
event.stopPropagation()
event.preventDefault()

Con stopImmediatePropagation() el evento ya no llega al reproductor. Por eso existe la opción Solo en Shorts: con ella activada, en /watch la extensión sale antes de cancelar nada y la L vuelve a avanzar 10 segundos.

No disparar mientras escribes

Si el foco está en un campo de texto, la tecla tiene que pasar sin tocarla. El problema es que en YouTube casi todo son web components, y document.activeElement devuelve el componente que contiene el foco, no el <input> real que hay dentro de su Shadow DOM. Hay que bajar hasta el fondo:

let target = document.activeElement as HTMLElement | null
while (target && target.shadowRoot && target.shadowRoot.activeElement) {
  target = target.shadowRoot.activeElement as HTMLElement
}

Con el elemento real ya se puede decidir: INPUT, TEXTAREA, SELECT o cualquier cosa con contenteditable cuentan como escribir. La caja de comentarios de YouTube es un editor de texto enriquecido, y es el caso que más importa: nadie quiere dar like al escribir «lol».

Shorts: el que está en pantalla

En los Shorts, YouTube mantiene varios vídeos montados a la vez para que el desplazamiento sea fluido. El que estás viendo lleva el atributo is-active:

let activeShort = document.querySelector('ytd-reel-video-renderer[is-active]')

Si todavía no lo tiene (justo al cargar la página), se busca el primero visible. Dentro de ese Short, el like es #like-button button, y si no aparece, cualquier botón cuyo aria-label diga like o me gusta pero no dislike o no me gusta. Los Shorts han dado pocos problemas. Los vídeos normales, bastantes más.

Vídeos normales: un botón con muchas caras

En la página de un vídeo, el botón de like no es siempre el mismo componente. Depende de qué versión de la interfaz te sirva YouTube y del ancho de la ventana. Con la ventana grande aparece #segmented-like-button. Al estrecharla, ese contenedor desaparece y el botón vive dentro de like-button-view-model. Y además YouTube deja en la página variantes ocultas del mismo botón (icono pequeño, icono con texto, solo icono), de las que solo una se ve en cada momento.

Por eso la búsqueda prueba cuatro estrategias en orden y se queda con el primer botón que se pueda pulsar de verdad:

  1. #segmented-like-button button: la interfaz nueva con la ventana grande.
  2. like-button-view-model button: el sistema de componentes nuevo, en cualquier ancho. Es el que salva las ventanas pequeñas.
  3. ytd-toggle-button-renderer button: la interfaz antigua.
  4. Recorrer todos los botones y mirar el aria-label, como en los Shorts.

offsetParent mentía

Para saber si un botón se ve, la versión anterior usaba offsetParent !== null, el truco clásico. Falla con los elementos position: fixed, porque su offsetParent es siempre null aunque estén perfectamente visibles, y algunos layouts de YouTube los usan. El resultado: en ciertos tamaños de ventana, el botón bueno se descartaba y no se daba like.

Ahora la comprobación mira el tamaño real en pantalla, que además descarta las variantes ocultas (miden 0 × 0):

function isButtonInteractable(button: Element): boolean {
  const el = button as HTMLButtonElement
  const rect = el.getBoundingClientRect()
  return rect.width > 0 && rect.height > 0
    && el.isConnected
    && !el.disabled
    && el.getAttribute('aria-disabled') !== 'true'
}

El fallo que desaparecía al abrir DevTools

El like fallaba con la ventana a cierto tamaño, pero al abrir las herramientas de desarrollo para investigarlo dejaba de fallar.

La razón: DevTools acoplado a un lado estrecha la ventana, YouTube cambia de layout y el botón pasa a ser otro componente. Al abrir las herramientas estaba cambiando justo lo que quería observar. La solución fue abrirlas en una ventana aparte, para que el ancho de la página no se moviera. Con un fallo que depende del tamaño de la ventana, acoplar DevTools es cambiar las condiciones del experimento.

Likes que no entraban

El fallo más serio salió en vídeos normales con la reproducción automática: pulsabas L, la animación aparecía, pero el like no se registraba. Pasaba más en sesiones largas y recargar lo arreglaba.

La causa está en cómo navega YouTube. Es una aplicación de una sola página: al pasar al siguiente vídeo no carga una página nueva, sino que reutiliza el componente de la página de vídeo y reconstruye el botón de like por dentro. Durante ese instante, el <button> existe, se ve y pasa todas las comprobaciones, pero todavía no tiene sus manejadores de eventos. El click() llega al elemento correcto en el momento equivocado y no pasa nada.

La animación salía igualmente porque el código solo comprobaba que había encontrado el botón, no que YouTube hubiera hecho caso al clic. Lo resolví en dos partes.

Esperar a que termine la navegación

YouTube emite sus propios eventos al navegar: yt-navigate-start al empezar y yt-navigate-finish al terminar. La extensión los escucha y bloquea el atajo mientras tanto, con un margen extra para que el botón termine de montarse:

Cargando diagrama...

El tope de 5 segundos está por si yt-navigate-finish no llega nunca. Sin él, un evento perdido dejaría el atajo bloqueado hasta recargar la página.

Comprobar que el like ha entrado

Aun así, no me fío de un solo clic. El botón de like indica su estado en aria-pressed. Después de pulsar, la extensión espera 150 ms y comprueba si ese atributo ha cambiado. Si no ha cambiado, el clic se ha perdido: espera 250 ms más, vuelve a buscar el botón y lo pulsa otra vez.

const prevPressed = button.getAttribute('aria-pressed')
button.click()

await new Promise<void>(resolve => setTimeout(resolve, 150))
if (button.getAttribute('aria-pressed') !== prevPressed) return

await new Promise<void>(resolve => setTimeout(resolve, 250))
const retryButton = findStandardVideoLikeButton()
if (retryButton && retryButton.isConnected && retryButton.getAttribute('aria-pressed') === prevPressed) {
  retryButton.click()
}

La última condición evita un doble clic: si YouTube solo estaba tardando en actualizar el atributo y el primer clic sí había entrado, al reintentar el estado ya habría cambiado y se quedaría así. Un reintento a ciegas quitaría el like que acababas de dar.

El sonido y la animación se muestran al momento, sin esperar a la comprobación. Si el primer clic falla, el reintento es invisible para quien lo usa.

De un script a una extensión configurable

En abril migré la extensión a WXT, un framework para extensiones que genera el manifest.json, compila TypeScript y recarga en caliente al desarrollar. Con él llegaron las traducciones y, después, las opciones: la tecla configurable con modificadores, el modo solo Shorts, el sonido y la animación.

Las preferencias se guardan en chrome.storage.sync, que se sincroniza entre los equipos con la misma cuenta de Chrome. El content script no las lee una vez al cargar: se suscribe a los cambios.

storage.watch<string | null>('sync:shortcutKey', (newValue) => {
  currentShortcutKey = (newValue || 'l').toLowerCase()
})

Cambias la tecla en el popup y la pestaña de YouTube que ya tenías abierta la usa al momento, sin recargar.

Los modificadores se comparan de forma exacta: si el atajo es L a secas, Ctrl + L no lo activa. Así no se pisa con otros atajos del navegador ni de YouTube.

Un sonido sin archivos de audio

El pop de confirmación no es un .mp3: lo genera la Web Audio API en el momento. Es un oscilador senoidal que sube de 380 a 760 Hz en 100 ms, con una envolvente de volumen que lo hace sonar suave y lo apaga en 130 ms.

osc.frequency.setValueAtTime(380, ctx.currentTime)
osc.frequency.exponentialRampToValueAtTime(760, ctx.currentTime + 0.1)

gain.gain.setValueAtTime(0.001, ctx.currentTime)
gain.gain.linearRampToValueAtTime(0.12, ctx.currentTime + 0.015)
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12)

No hay nada que cargar y suena sin retraso.

Pedir una reseña, una sola vez

La extensión cuenta en chrome.storage.local cuántos likes has dado con ella. Cuando llegas a diez, el popup muestra un aviso con un enlace a las reseñas. Al pulsarlo o cerrarlo, no vuelve a aparecer. Diez likes significa que ya la has usado de verdad, que es cuando tu opinión vale algo. El contador no sale del navegador.

Lo que me enseñó

De todo lo que he publicado, esta es la extensión más pequeña: un content script, un popup y ninguna dependencia en ejecución aparte del almacenamiento de WXT. Y es la que más usuarios tiene, más de 240. Creo que la razón es que resuelve un gesto concreto que ya tenías en la cabeza: quien viene de TikTok ya sabe lo que hace antes de instalarla, y no hay nada más que aprender.

Lo otro que aprendí viene de las valoraciones. Con pocas reseñas, cada una pesa muchísimo, y un par de personas me pusieron notas bajas por fallos muy concretos. Los arreglé rápido, pero la nota se queda. En una herramienta que hace una sola cosa, que esa cosa falle aunque sea de vez en cuando es todo lo que el usuario ve. Por eso el trabajo más largo de la extensión no fue añadir funciones: fue la verificación del clic y el reintento.

Qué haría distinto

La extensión no tiene tests automáticos, y es lo primero que cambiaría. La búsqueda del botón y la comprobación de si estás escribiendo son funciones que se pueden probar con HTML de ejemplo copiado de los distintos layouts de YouTube. Así, cuando YouTube cambie, sabría qué estrategia ha dejado de funcionar sin tener que probar a mano en cada tamaño de ventana.

La última estrategia busca like o me gusta en el aria-label, y eso solo funciona con YouTube en inglés o en español. La extensión está traducida a 15 idiomas, pero si YouTube está en japonés y fallan las tres primeras estrategias, la cuarta no encontrará nada. Para que funcione igual en cualquier idioma, habría que identificar el botón por su estructura y no por su texto.

Y los tiempos (400 ms de margen, 150 ms para comprobar, 250 ms para reintentar) salen de probar en mi equipo. Funcionan, pero en un equipo lento YouTube podría tardar más en montar el botón. Esperar a que el botón esté listo, por ejemplo vigilando sus cambios con un MutationObserver, sería más robusto que esperar un tiempo fijo.

Puedes instalar YouTube Like Shortcut desde la Chrome Web Store.