Exportar tu historial de YouTube a Excel (CSV)

Descarga tu historial de reproducciones como hoja de cálculo: el día que viste cada vídeo, título, canal, duración, cuánto viste y el enlace. Sin Google Takeout y sin esperar un email.

export-history.js · 154 líneaspégalo en la consola (F12) de www.youtube.com
// Responsive Muse · YouTube Watch History Exporter · v1.0 · 2026-10-22
// Saves your YouTube watch history to CSV or TXT: the day you watched each video, title, channel,
// duration, how much you watched and the link. No Google Takeout and no waiting for an email.
// Runs only on youtube.com/feed/history. YouTube loads older days while the script scrolls.
// It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Exportar historial de YouTube';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets) or 'txt' (one URL per line)
  const LIMIT = 0;             // stop after this many videos (0 = the whole history)

  if (location.hostname !== 'www.youtube.com' || location.pathname !== '/feed/history') {
    alert('Abre primero tu historial (con tu sesión iniciada):\nhttps://www.youtube.com/feed/history');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${n === 1 ? word : ({ canal: 'canales', 'canción': 'canciones', 'publicación': 'publicaciones', 'elemento guardado': 'elementos guardados' })[word] || word + 's'}`;   // 1 vídeo, 2 vídeos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancelar', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Parar', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Cerrar', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
  const deliver = (summary, body, file) => {
    const save = () => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
      a.download = file;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`${summary}\nGuardado como ${file} (mira tu carpeta de Descargas).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopiado. Pégalo en una hoja de cálculo o en una nota.`))
      .catch(() => panel.text(`${summary}\nEl navegador ha bloqueado el portapapeles: usa Descargar.`));
    panel.actions(summary, [['Cerrar', false, () => panel.close()], ['Copiar', false, copy], ['Descargar', true, save]]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const videos = new Map();

  // Day headers say "Today", "Yesterday", a weekday or a date: turn them into YYYY-MM-DD when possible
  const iso = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  const dayOf = label => {
    const now = new Date();
    const back = n => iso(new Date(now.getFullYear(), now.getMonth(), now.getDate() - n));
    const l = label.toLowerCase();
    if (/^(today|hoy)$/.test(l)) return back(0);
    if (/^(yesterday|ayer)$/.test(l)) return back(1);
    const days = [['sunday', 'domingo'], ['monday', 'lunes'], ['tuesday', 'martes'], ['wednesday', 'miércoles'],
      ['thursday', 'jueves'], ['friday', 'viernes'], ['saturday', 'sábado']];
    const wd = days.findIndex(d => d.includes(l));
    if (wd >= 0) return back((now.getDay() - wd + 7) % 7 || 7);
    // Spanish dates: "3 oct" or "16 oct 2021"
    const es = l.match(/^(\d{1,2}) (?:de )?([a-zé]{3})[a-zé.]* ?(?:de )?(\d{4})?$/);
    if (es) {
      const m = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'].indexOf(es[2]);
      if (m >= 0) return iso(new Date(Number(es[3] || now.getFullYear()), m, Number(es[1])));
    }
    if (!/\d/.test(label)) return label;   // a header we don't recognise: keep it as shown
    const withYear = /\d{4}/.test(label) ? label : `${label}, ${now.getFullYear()}`;   // "Oct 3" this year
    const t = Date.parse(withYear);
    return isNaN(t) ? label : iso(new Date(t));
  };

  // Read the videos currently loaded: new layout (yt-lockup-view-model) and old (ytd-video-renderer)
  const collect = () => {
    for (const section of document.querySelectorAll('ytd-item-section-renderer')) {
      const day = (section.querySelector('#title')?.textContent || '').trim();
      for (const el of section.querySelectorAll('yt-lockup-view-model, ytd-video-renderer')) {
        const link = el.querySelector('a[href^="/watch?v="], a[href^="/shorts/"]');
        if (!link) continue;
        const href = link.getAttribute('href');
        const id = (href.match(/[?&]v=([\w-]{11})/) || href.match(/\/shorts\/([\w-]{11})/) || [])[1];
        if (!id || videos.has(id)) continue;
        const text = sel => (el.querySelector(sel)?.textContent || '').trim();
        const row = (el.querySelector('[class*="MetadataRow"]')?.innerText || '').split('\n').map(s => s.trim()).filter(s => s && s !== '•');
        const watched = el.querySelector('[class*="ProgressBarSegment"], #progress')?.style.width || '';
        videos.set(id, {
          day: day ? dayOf(day) : '',
          title: el.querySelector('h3[title]')?.getAttribute('title') || text('#video-title') || text('h3'),
          channel: text('ytd-channel-name a') || row[0] || '',
          duration: text('.ytBadgeShapeText, ytd-thumbnail-overlay-time-status-renderer #text, badge-shape'),
          watched,
          url: href.startsWith('/shorts/') ? 'https://www.youtube.com/shorts/' + id : 'https://www.youtube.com/watch?v=' + id,
        });
        if (LIMIT && videos.size >= LIMIT) return;
      }
    }
  };

  // 1. Scroll to the end, collecting as we go, until nothing new appears four times in a row
  let stop = false;
  panel.stoppable(() => { stop = true; });
  let idle = 0;
  while (idle < 4 && !stop && !(LIMIT && videos.size >= LIMIT)) {
    const before = videos.size;
    collect();
    window.scrollTo(0, document.documentElement.scrollHeight);
    await sleep(1500);
    idle = videos.size === before ? idle + 1 : 0;
    panel.text(`Leyendo tu historial… ${many(videos.size, 'vídeo')}`);
  }
  const list = [...videos.values()];
  if (!list.length) {
    panel.finish('No se han encontrado vídeos. ¿Tu historial está en pausa o vacío? Si no, puede que YouTube haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }

  // 2. Build the file and offer it
  const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
  const body = FORMAT === 'txt'
    ? list.map(v => v.url).join('\n')
    : ['Visto el,Título,Canal,Duración,Visto,URL', ...list.map(v =>
        [v.day, v.title, v.channel, v.duration, v.watched, v.url].map(cell).join(','))].join('\n');
  deliver(`${stop ? 'Parado antes de terminar. ' : ''}Listo: ${many(list.length, 'vídeo')}.`, body, `youtube-history.${FORMAT}`);
  return list.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre www.youtube.com y pulsa el marcador. Ejecuta el mismo script con las opciones de arriba.

Por qué es seguro ejecutarloComprobación automática superada 23 oct 2026 · probado en una cuenta real el 22 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de www.youtube.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en www.youtube.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 154 líneas comentadas, sin minificar ni ofuscar.
  • Muestra su progreso en la página. Un pequeño recuadro te dice qué está haciendo y al final te ofrece Descargar o Copiar.
  • El mismo código para todos. SHA-256 de la versión por defecto: 9a58cf628ed02542…
Compruébalo tú mismo en 30 segundos
  1. Léelo: todas las líneas están arriba, con comentarios.
  2. Busca en el código fetch, XMLHttpRequest, cookie, localStorage o eval. No aparece ninguna.
  3. Abre la pestaña Red (Network) antes de ejecutarlo: solo verás peticiones a www.youtube.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre tu historial con tu sesión iniciada.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar (o allow pasting si lo tienes en inglés) y pulsa Enter.
  3. Elige las opciones, copia el script, pégalo en la consola y pulsa Enter. Al terminar aparece un recuadro: pulsa Descargar (o Copiar).

Qué obtienes

Una fila por vídeo: Visto el, Título, Canal, Duración, Visto, URL. Los encabezados «Hoy», «Ayer» y los días de la semana se convierten en fechas reales, así que puedes ordenar y filtrar. Funciona con YouTube en español o en inglés.

Preguntas frecuentes

¿No es Google Takeout la forma oficial?

Takeout funciona, pero esperas un email, descargas un zip y recibes un HTML o JSON que aún tienes que convertir. Este script te da la hoja de cálculo en un minuto, desde la página que estás viendo.

¿Hasta dónde llega?

Hasta donde llegue tu historial. YouTube carga los días anteriores mientras el script se desplaza; años de historial pueden tardar unos minutos. Usa el límite si solo quieres los últimos vídeos.

¿Qué significa la columna «Visto»?

Cuánto viste del vídeo, según la barra roja de progreso de YouTube (por ejemplo 45%). Queda vacía cuando YouTube no muestra barra.

¿Incluye Shorts?

Sí, los Shorts que aparecen en tu historial se exportan con su enlace.

Más herramientas gratis

Limpiar

Exportar y hacer copia