TubeMarks

TubeMarks — Bookmark the moments that matter

Extensión de Chrome (Manifest V3) para guardar momentos concretos de vídeos y Shorts de YouTube y volver a ellos con un clic. El nombre combina “Tube” y “bookmarks”.

Estás viendo un vídeo, en el minuto 08:42 pasa algo que quieres recordar, pulsas un botón, escribes Se cae de la silla y listo. Después abres la extensión y vuelves a ese segundo exacto.


1. Qué hace

Guarda una marca con toda la información necesaria para recuperar el momento:

Campo Ejemplo
title #ParenLaMano Completo - 09/05
videoUrl https://www.youtube.com/watch?v=EyaMgKF9bxU
videoId EyaMgKF9bxU
timestampSeconds 2822
timestampLabel 47:02
note Se cae de la silla
category funny
favorite true
channelName Vorterix (cuando se puede leer del DOM)
thumbnailUrl miniatura oficial del vídeo
createdAt / updatedAt fechas ISO 8601
isShort false

2. Funcionalidades

Guardar

Organizar

Gestionar

Reproducir

3. Stack

No se usa React: la interfaz son dos pantallas con estado local y unas pocas listas. Con document.createElement y textContent el resultado es más pequeño, más rápido de auditar y, además, elimina por construcción cualquier riesgo de inyección de HTML.

4. Arquitectura

public/
  manifest.json            Manifiesto (se copia tal cual a dist/)
  icons/                   Iconos PNG generados por script
src/
  background/
    service-worker.ts      Único punto de escritura; atajo de teclado; apertura de marcas
  content/
    content-script.ts      Orquestador dentro de YouTube
    youtube-navigation.ts  Detección de la navegación SPA
    youtube-player.ts      Lectura del <video> y de los metadatos
    injected-button.ts     Botón dentro de los controles del reproductor
    timeline-markers.ts    Marcas del vídeo actual sobre la barra de progreso
    overlay-ui.ts          Panel de nota y avisos (shadow DOM)
    overlay.css            Estilos del overlay (se inyectan como hoja construida)
  popup/                   popup.html · popup.ts · popup.css
  options/                 options.html · options.ts · options.css
  storage/
    bookmark-repository.ts Repositorio con cola de escritura
    storage-area.ts        Adaptador de chrome.storage (+ versión en memoria para tests)
  shared/
    models.ts constants.ts messages.ts result.ts
    time.ts youtube-url.ts validation.ts csv.ts import-file.ts query.ts
    dom.ts download.ts base.css
scripts/
  generate-icons.mjs verify-dist.mjs clean.mjs dev.mjs

Flujo de datos. El popup y la página de opciones leen y escriben con el repositorio; todo lo que nace dentro de YouTube (botón del reproductor, panel de nota, atajo de teclado) pasa por el service worker. Así existe un solo camino de guardado y las validaciones no se duplican.

Mensajería tipada. src/shared/messages.ts define los identificadores, la forma de cada mensaje y la respuesta que le corresponde (MessageResponseMap). No hay cadenas mágicas repartidas por el código y toda respuesta viaja en un Result<T>, de modo que quien llama trata el error de forma explícita.

Service worker efímero. Manifest V3 puede detener el service worker en cualquier momento. No se guarda nada en memoria: cada operación relee chrome.storage.local. Si una pestaña de YouTube se abrió antes de instalar la extensión y no tiene el content script, el service worker lo inyecta con chrome.scripting antes de hablar con ella.

Reinstalación en caliente. Al recargar la extensión desde chrome://extensions, el content script que ya estaba en las pestañas abiertas queda huérfano: sigue vivo en el DOM, pero sus llamadas a chrome.* fallan. Por eso el content script no usa una simple bandera «ya cargado», que impediría recuperarse: publica una instancia en window.__youtubeStar con un método dispose(), y cada nueva carga desmonta la anterior (listeners, temporizadores, botón y overlay) antes de montarse. Así una segunda inyección nunca duplica la interfaz y, además, la extensión vuelve a funcionar sin necesidad de recargar la pestaña.

Decisiones técnicas

Redondeo del segundo. Se trunca con Math.floor, nunca se redondea al más cercano. YouTube salta exactamente al segundo indicado en t=, así que truncar garantiza empezar igual o ligeramente antes del instante que vio el usuario: el momento marcado no se pierde nunca. Redondear hacia arriba podría dejar fuera el inicio de una frase o de un gag.

Shorts. Se detectan y se guardan igual que cualquier vídeo (con isShort: true para poder mostrarlo en la interfaz), pero al abrirlos se usa siempre la URL estándar watch?v=ID&t=Ns. El reproductor de Shorts ignora el parámetro t, de modo que conservar /shorts/ID haría que el vídeo empezara desde el principio.

Detección de la navegación SPA. Se combinan tres señales: los eventos propios de YouTube (yt-navigate-finish, yt-page-data-updated…), un MutationObserver sobre el elemento <title> (un solo nodo, muy barato) y un temporizador de un segundo como red de seguridad. El temporizador cubre los casos que los eventos no ven: sustitución del elemento <video> y reconstrucción de la barra de controles al entrar en pantalla completa o modo cine. Se descartó observar todo el body porque en YouTube dispara miles de veces por minuto sin aportar nada. La inserción del botón es idempotente (comprueba si sigue conectado a la barra actual), así que no se duplican botones por muchas veces que se llame.

Icono adaptado al reproductor. YouTube ha cambiado el lienzo de sus iconos: el reproductor clásico usa viewBox="0 0 36 36" con width/height="100%" y el moderno («delhi») viewBox="0 0 24 24" con tamaño fijo. En lugar de fijar uno, el botón copia esos atributos del icono de ajustes vecino y escala su trazado en consecuencia, de modo que encaja y queda centrado en ambos. Si el reproductor es tan estrecho que YouTube oculta los botones que no le caben, la inserción se deshace y toma el relevo el botón flotante.

Aislamiento de la interfaz inyectada. El panel de nota y los avisos viven en un shadow root con una hoja de estilo construida (CSSStyleSheet + adoptedStyleSheets). Ni YouTube afecta a esos estilos ni la extensión toca los de YouTube, y al no insertar ningún <style> en la página la política de seguridad de contenido de YouTube no puede bloquearlo. Las pulsaciones de teclas dentro del panel se detienen en el host para que no activen los atajos de YouTube (espacio, k, flechas).

Duplicados. Una marca se considera repetida si coinciden vídeo, segundo y nota (normalizada). Dos notas distintas en el mismo segundo son intencionadas y se conservan las dos.

Importación en la página de opciones. Chrome cierra el popup al abrir el selector de archivos, así que la importación vive en la página de opciones, que es una pestaña normal. La exportación sí funciona desde el popup porque la descarga se dispara antes de que se cierre.

5. Instalación de dependencias

npm install

6. Desarrollo

npm run dev

Compila en modo --watch (aplicación y content script en paralelo) sobre dist/. Una extensión se carga desde el sistema de archivos, así que no se usa el servidor de desarrollo de Vite: todo tiene que existir como archivo real. Tras cada cambio, pulsa Actualizar en chrome://extensions (y recarga la pestaña de YouTube si tocaste el content script).

7. Build

npm run build

Genera los iconos, limpia dist/, compila las dos pasadas y comprueba que en dist/ está todo lo que declara el manifiesto. Al terminar imprime la ruta exacta que hay que cargar en Chrome.

Contenido de dist/:

dist/
  manifest.json
  service-worker.js
  content-script.js
  popup/popup.html
  options/options.html
  assets/*.js  assets/*.css
  icons/icon16.png  icon32.png  icon48.png  icon128.png

Otros comandos:

npm run typecheck   # tsc --noEmit
npm test            # Vitest
npm run verify      # comprueba dist/ sin recompilar
npm run icons       # regenera los iconos PNG

8. Cómo cargarla en Chrome

  1. npm install && npm run build
  2. Abre chrome://extensions.
  3. Activa Modo de desarrollador (arriba a la derecha).
  4. Pulsa Cargar descomprimida.
  5. Selecciona la carpeta dist del proyecto, no la raíz.

Si Chrome dice «No se ha podido cargar el archivo de manifiesto», casi siempre es porque se ha seleccionado la carpeta equivocada. La carpeta correcta es la que contiene manifest.json, service-worker.js y content-script.js: es decir, dist/. El comando npm run build la imprime al terminar.

9. Cómo utilizarla

Guardar un momento

Volver a un momento

Abre el popup y pulsa cualquier marca, o su botón ▶. Si el vídeo ya está abierto en otra pestaña, se reutiliza y salta al segundo guardado.

Buscar y filtrar

Escribe en el buscador (la tecla / lleva el foco allí), usa las categorías, el botón ★ Favoritos o los desplegables de vídeo y canal. Limpiar quita todos los filtros. El orden y los filtros elegidos se recuerdan para la próxima vez. Si las categorías no caben, recórrelas con las flechas laterales o con la rueda del ratón sobre esa fila.

Ganar espacio para la lista

El popup mide 400 × 600 px como máximo, así que las dos secciones superiores se pliegan:

Con las dos plegadas, la lista pasa de una tarjeta visible a tres. Ambos estados se recuerdan.

Editar y eliminar

El lápiz abre la edición en línea (nota, categoría y favorito; Ctrl + Intro guarda, Esc cancela). La papelera pide confirmación en la propia tarjeta.

Exportar e importar

Importar un CSV de otra extensión

Se admiten archivos CSV con una columna de vídeo y otra de tiempo. Se reconoce el formato habitual:

Title,Video ID,Bookmark Name,Bookmark Time
"Andrés Calamaro - Acústico Rock &amp; Pop (1997) ","ILI0fpo6Y4Y","","47:30"
"#ParenLaMano Completo - 09/05 | Vorterix ","EyaMgKF9bxU","Nace Estelita","47:02"

10. Permisos solicitados

Permiso Para qué se usa
storage Guardar las marcas y las preferencias en chrome.storage.local.
tabs Saber si la pestaña activa es un vídeo de YouTube y localizar una pestaña que ya tenga ese vídeo abierto para reutilizarla.
scripting Inyectar el content script en pestañas de YouTube que se abrieron antes de instalar o actualizar la extensión.
host_permissions: https://www.youtube.com/*, https://m.youtube.com/* Leer el reproductor y dibujar el botón. La extensión no tiene acceso a ningún otro sitio.

Permisos que no se piden, a propósito:

11. Limitaciones conocidas

12. Pruebas

npm test          # una pasada
npm run test:watch

Pruebas automatizadas sobre las piezas con lógica de verdad:

Archivo Cubre
shared/time.test.ts Truncado del segundo, formato mm:ss / h:mm:ss, parseo inverso, fechas relativas
shared/youtube-url.test.ts Extracción del ID (watch, Shorts, youtu.be, embed, live), URLs con timestamp, miniaturas
shared/validation.test.ts Normalización de datos de almacenamiento, validación del JSON importado, duplicados, fusión sin pérdidas
shared/csv.test.ts Analizador CSV (comillas, comas internas, separadores), entidades HTML, tiempos flexibles, detección de formato
shared/query.test.ts Búsqueda sin acentos, filtros combinados, las cinco ordenaciones, facetas
storage/bookmark-repository.test.ts Alta, edición, borrado, escrituras simultáneas, datos corruptos, importación, errores de almacenamiento
content/timeline-markers.test.ts Filtrado por vídeo, posición relativa, agrupación y extremos de la barra

Las APIs de Chrome se aíslan detrás de StorageArea (src/storage/storage-area.ts), que tiene una implementación en memoria para las pruebas: el repositorio se prueba entero sin navegador.

13. Posibles mejoras futuras