Export your YouTube watch history to CSV

Download your YouTube watch history as a spreadsheet: the day you watched each video, title, channel, duration, how much of it you watched and the link. No Google Takeout, no waiting for an email.

export-history.js · 154 linespaste in the DevTools console on 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 = 'Export watch history';
  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('Open your watch history first (signed in):\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} ${word}${n === 1 ? '' : 's'}`;   // 1 video, 2 videos
  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('Cancel', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Close', 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}\nSaved as ${file} (check your Downloads).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
      .catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
    panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', 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(`Reading your history… ${many(videos.size, 'video')}`);
  }
  const list = [...videos.values()];
  if (!list.length) {
    panel.finish('No videos found. Is your watch history paused or empty? If not, YouTube may have changed its layout: please report it on 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')
    : ['Watched on,Title,Channel,Duration,Watched,URL', ...list.map(v =>
        [v.day, v.title, v.channel, v.duration, v.watched, v.url].map(cell).join(','))].join('\n');
  deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'video')}.`, body, `youtube-history.${FORMAT}`);
  return list.length;
})();

Bookmarklet: drag the middle button to your bookmarks bar, open www.youtube.com and click the bookmark. It runs the same script with the options above.

Why it's safe to runAutomated check passed Oct 23, 2026 · tested on a real account Oct 22, 2026
  • Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.youtube.com itself loading your list.
  • Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
  • Only runs on www.youtube.com. Anywhere else it stops before doing anything.
  • Short and readable. 154 commented lines, not minified or obfuscated.
  • Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
  • Same code for everyone. SHA-256 of the default version: fd6eab56f30b4a42…
Verify it yourself in 30 seconds
  1. Read it: every line is above, with comments.
  2. Search the code for fetch, XMLHttpRequest, cookie, localStorage or eval. None of them appear.
  3. Open the Network tab before running it: you'll only see requests to www.youtube.com.
Did it work for you?

How to use it

  1. Open youtube.com/feed/history while signed in.
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. Pick a format, copy the script, paste it in the console and press Enter. It scrolls back through your history, then shows a box: click Download (or Copy).

What you get

One row per video: Watched on, Title, Channel, Duration, Watched, URL. “Today”, “Yesterday” and weekday headers are turned into real dates, so you can sort and filter the file. Use it to find a video you half remember, to see which channels you watch most, or to keep a backup before you clear your history.

Why not Google Takeout?

Takeout sends a zip with a watch-history.html or JSON file that you then have to convert, and the help pages online all end with “now run this Python script”. This exporter reads the history page you already have open and gives you the spreadsheet directly.

FAQ

Isn't Google Takeout the official way?

Takeout works, but you wait for an email, download a zip and get an HTML or JSON file you still have to convert. This script gives you a spreadsheet in a minute, from the page you're looking at.

How far back does it go?

As far as your history goes. YouTube loads older days while the script scrolls; years of history can take a few minutes. Use the limit if you only want the latest videos.

What does the "Watched" column mean?

How much of the video you watched, as YouTube's red progress bar shows it (for example 45%). It's empty when YouTube shows no bar.

Does it include Shorts?

Yes, the Shorts that appear in your history are exported with their link.

More free YouTube tools

Clean up

Export & back up