Export YouTube Music liked songs to CSV or Excel, free (no sign-in to other sites)

Download every song you liked on YouTube Music as a spreadsheet or a plain list: title, artist, album, duration and link. Free, in your own browser, without installing anything or giving a transfer site access to your Google account.

Sign in on music.youtube.com in a computer browser and open Liked Music (Library > Playlists > Liked Music), then run the script.

export-youtube-music-playlist.js · 140 linespaste in the DevTools console on music.youtube.com
// Responsive Muse · YouTube Music Playlist Exporter · v1.0 · 2026-10-31
// Saves any YouTube Music playlist or album you can open on music.youtube.com (your own playlists,
// your Liked Music or public ones) to CSV or TXT: title, artist, album, duration and link. Useful
// to back up a playlist or move it to Spotify, Apple Music or Tidal. Long playlists load while
// the script scrolls. It makes no network requests of its own and never reads cookies,
// passwords or tokens.
(async () => {
  const TOOL = 'Export YouTube Music playlist';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets, playlist converters) or 'txt' ("Artist - Title", one per line)

  if (location.hostname !== 'music.youtube.com' || !/^\/(playlist|browse)/.test(location.pathname)) {
    alert('Open a playlist on YouTube Music first, for example your Liked Music:\nhttps://music.youtube.com/playlist?list=LM');
    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 = 'display:block!important;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 tracks = new Map();
  const text = el => (el?.textContent || '').replace(/\s+/g, ' ').trim();

  // Read the songs currently on the page (YouTube Music keeps every loaded row in the page)
  const collect = () => {
    for (const row of document.querySelectorAll('ytmusic-responsive-list-item-renderer')) {
      const title = text(row.querySelector('.title-column .title') || row.querySelector('.title'));
      if (!title) continue;
      // Secondary columns: artist(s), then the album (only when it links to an album page;
      // on music videos and episodes that column holds the upload date instead)
      const cols = [...row.querySelectorAll('.secondary-flex-columns yt-formatted-string')].map(text);
      const hasAlbum = !!row.querySelector('.secondary-flex-columns a[href^="browse/"]');
      const watch = row.querySelector('a[href*="watch?v="]');
      const id = watch ? new URL(watch.href, location.origin).searchParams.get('v') : '';
      const link = id ? 'https://music.youtube.com/watch?v=' + id : (row.querySelector('.title a')?.href || '');
      const key = link || title + '|' + cols[0];
      if (tracks.has(key)) continue;
      tracks.set(key, {
        n: tracks.size + 1,
        title,
        artist: cols[0] || '',
        album: hasAlbum ? cols[1] || '' : '',
        duration: text(row.querySelector('.fixed-column yt-formatted-string, .fixed-columns yt-formatted-string')),
        url: link,
      });
    }
  };

  // YouTube Music scrolls an inner panel (div#content), not the window: find it from the first row
  const scroller = (() => {
    for (let e = document.querySelector('ytmusic-responsive-list-item-renderer'); e && e !== document.body; e = e.parentElement) {
      if (e.scrollHeight > e.clientHeight + 50 && /auto|scroll/.test(getComputedStyle(e).overflowY)) return e;
    }
    return document.scrollingElement;
  })();

  // 1. Scroll to the bottom again and again: YouTube Music loads the next 100 songs each time
  let stop = false;
  panel.stoppable(() => { stop = true; });
  let idle = 0;
  while (idle < 4 && !stop) {
    const before = tracks.size;
    collect();
    scroller.scrollTop = scroller.scrollHeight;
    document.querySelector('ytmusic-continuation-item-renderer')?.scrollIntoView({ block: 'end' });
    await sleep(1500);
    idle = tracks.size === before ? idle + 1 : 0;
    panel.text(`Collecting songs… ${tracks.size}`);
  }
  const list = [...tracks.values()];
  if (!list.length) {
    panel.finish('No songs found. YouTube Music 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(t => `${t.artist} - ${t.title}`).join('\n')
    : ['#,Title,Artist,Album,Duration,URL', ...list.map(t =>
        [t.n, t.title, t.artist, t.album, t.duration, t.url].map(cell).join(','))].join('\n');
  const name = (document.title.replace(/ - YouTube Music$/, '') || 'playlist').replace(/[\\/:*?"<>|]+/g, '_');
  deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'song')}.`, body, `${name}.${FORMAT}`);
  return list.length;
})();

🛡️ Chrome says “Don't paste code…”? That's normal. Type allow pasting in the console, press Enter and paste again. You only need to do it once.

Why does Chrome warn me?

Chrome shows this the first time anyone pastes into the console, to protect people from scams that ask them to paste unknown code. It's right to warn you, and that's why this script is fully visible above: 140 commented lines that send nothing anywhere and never touch your passwords or cookies. Why it's safe.

Rather not use the console? Drag the Export YouTube Music liked songs button to your bookmarks bar and click it on music.youtube.com. No warning that way.

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

Why it's safe to runAutomated check passed Nov 9, 2026 · tested on a real account Oct 31, 2026
  • Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is music.youtube.com itself loading your list.
  • Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
  • Only runs on music.youtube.com. Anywhere else it stops before doing anything.
  • Short and readable. 140 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: 08d5442e2f3900b8…
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 music.youtube.com.
Did it work for you?

How to use it

  1. Sign in on music.youtube.com in a computer browser and open Liked Music (Library > Playlists, or go straight to music.youtube.com/playlist?list=LM).
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. Copy the script, paste it in the console and press Enter. It scrolls through the list (100 songs at a time) and then offers the file.

What you get

One row per song with its position, title, artist, album (when YouTube Music shows one), duration and link. Choose TXT for a plain “Artist – Title” list that playlist converters and search boxes understand.

Why not Google Takeout or a converter?

Takeout sends your library, not always your likes, and only with video IDs. Transfer sites ask you to sign in with Google and their free plans limit how many songs you can move. This script reads the list already on your screen.

FAQ

Doesn't Google Takeout export my liked songs?

Takeout exports the songs in your library, not always the ones you liked, and you wait for an email with video IDs. This script reads your Liked Music list and gives you titles, artists and albums in a minute.

Can I move my liked songs to Spotify or Apple Music?

Yes. Export them as CSV and import the file with a playlist converter, or choose TXT ("Artist - Title" per line) and paste the list into one.

Is it safe?

The script runs in your own YouTube Music tab, sends nothing anywhere and never touches your password. You don't give any app access to your Google account, unlike transfer sites that ask you to sign in.

Why are there fewer songs than I liked?

YouTube Music hides songs that are no longer available (deleted or blocked in your country), so they can't be exported. Everything you can see in the list is exported.

More YouTube tools