Exportar una playlist de Spotify a CSV o texto

Guarda cualquier playlist o álbum de Spotify en una hoja de cálculo: título, artistas, álbum, la fecha en que se añadió cada canción, duración y enlace. Gratis, en tu navegador y sin iniciar sesión en webs de terceros.

Funciona en Spotify para la web (open.spotify.com) en Windows, Mac, Linux y Chromebook. Abre la playlist y ejecuta el script.

export-spotify-playlist.js · 145 líneaspégalo en la consola (F12) de open.spotify.com
// Responsive Muse · Spotify Playlist Exporter · v1.0 · 2026-10-23
// Saves any Spotify playlist, album or your Liked Songs that you can open on open.spotify.com
// to CSV or TXT: title, artists, album, date added, duration and link. Useful to back up a
// playlist or move it to Apple Music, YouTube 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 = 'Exportar playlist de Spotify';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets, playlist converters) or 'txt' ("Artist - Title", one per line)

  if (location.hostname !== 'open.spotify.com' || !/\/(playlist|album|collection\/tracks)\b/.test(location.pathname)) {
    alert('Abre primero una playlist, un álbum o tus Canciones que te gustan en Spotify para la web, por ejemplo:\nhttps://open.spotify.com/collection/tracks');
    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 tracks = new Map();
  const clean = s => (s || '').replace(/\s+/g, ' ').trim();
  // Links on the page carry the language (/intl-es/track/…): keep only the ID
  const link = href => href ? 'https://open.spotify.com/' + href.replace(/^.*?\/(track|episode)\//, '$1/').split('?')[0] : '';

  // Spotify scrolls an inner panel, not the window
  const first = document.querySelector('[data-testid="tracklist-row"]');
  let scroller = first;
  while (scroller && !(scroller.scrollHeight > scroller.clientHeight + 50 && /auto|scroll/.test(getComputedStyle(scroller).overflowY))) {
    scroller = scroller.parentElement;
  }
  scroller = scroller || document.scrollingElement;
  const grid = first?.closest('[aria-rowcount]');   // the playlist's own list (the sidebar library is another grid)
  const expected = grid ? Number(grid.getAttribute('aria-rowcount')) - 1 : 0;   // minus the header row
  const heading = clean(document.querySelector('[data-testid="entityTitle"]')?.textContent);
  const pageAlbum = /\/album\//.test(location.pathname) ? heading : '';   // album pages don't link the album in each row

  // Read the songs currently on screen (rows are recycled while scrolling: key = row number)
  const collect = () => {
    for (const row of document.querySelectorAll('[data-testid="tracklist-row"]')) {
      const n = Number(row.closest('[aria-rowindex]')?.getAttribute('aria-rowindex')) - 1;
      const a = row.querySelector('a[href*="/track/"], a[href*="/episode/"]');
      if (!a || !(n > 0) || tracks.has(n)) continue;
      const cells = [...row.querySelectorAll('[role="gridcell"]')];
      const added = cells.find(c => !c.querySelector('a, button, img') && !/^\d+$|^\d+:\d\d(:\d\d)?$/.test(clean(c.textContent)));
      const duration = cells.map(c => clean(c.textContent)).reverse().find(t => /^\d+:\d\d(:\d\d)?$/.test(t)) || '';
      tracks.set(n, {
        n,
        title: clean(a.textContent),
        artist: [...row.querySelectorAll('a[href*="/artist/"], a[href*="/show/"]')].map(x => clean(x.textContent)).join(', '),
        album: clean(row.querySelector('a[href*="/album/"]')?.textContent) || pageAlbum,
        added: clean(added?.textContent),
        duration,
        url: link(a.getAttribute('href')),
      });
    }
  };

  // 1. Scroll to the end, collecting as we go, until nothing new appears three times in a row
  let stop = false;
  panel.stoppable(() => { stop = true; });
  scroller.scrollTo(0, 0);
  await sleep(800);
  let idle = 0;
  while (idle < 3 && !stop && !(expected && tracks.size >= expected)) {
    const before = tracks.size;
    collect();
    scroller.scrollBy(0, scroller.clientHeight * 0.8);
    await sleep(900);
    collect();
    idle = tracks.size === before ? idle + 1 : 0;
    panel.text(`Recogiendo canciones… ${tracks.size}${expected ? ' de ' + expected : ''}`);
  }
  const list = [...tracks.values()].sort((x, y) => x.n - y.n);
  if (!list.length) {
    panel.finish('No se han encontrado canciones. Puede que Spotify 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(t => `${t.artist} - ${t.title}`).join('\n')
    : ['#,Título,Artista,Álbum,Fecha en que se añadió,Duración,URL', ...list.map(t =>
        [t.n, t.title, t.artist, t.album, t.added, t.duration, t.url].map(cell).join(','))].join('\n');
  const missing = expected && list.length < expected ? ` (la playlist dice ${expected}; si faltan, baja hasta el final y vuelve a ejecutarlo)` : '';
  const name = (heading || clean(document.title.split(' | Spotify')[0]) || 'playlist').replace(/[\\/:*?"<>|]+/g, '_');
  deliver(`${stop ? 'Parado antes de terminar. ' : ''}Listo: ${many(list.length, 'canción')}${missing}.`, body, `${name}.${FORMAT}`);
  return list.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre open.spotify.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 23 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de open.spotify.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en open.spotify.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 145 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: 455ad1ef594e541b…
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 open.spotify.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre la playlist en open.spotify.com. Las playlists públicas y los álbumes funcionan sin iniciar sesión.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar y pulsa Enter.
  3. Copia el script, pégalo en la consola y pulsa Enter. Recorre la lista y te ofrece el archivo.

Qué obtienes

Una fila por canción con su posición, título, todos los artistas, álbum, la fecha en que se añadió a la playlist (como la muestra Spotify), duración y el enlace de Spotify. Si eliges TXT, obtienes una lista «Artista – Título» que entienden los conversores de playlists y los buscadores.

¿Por qué no un exportador online?

Casi todas las webs de exportación te piden entrar con Spotify y darles acceso a tu cuenta. Este script no necesita ningún permiso: lee la playlist que ya tienes en pantalla, en tu propio navegador.

Preguntas frecuentes

¿Spotify no tiene botón de exportar?

No. Las apps te dejan copiar canciones de una en una, pero no hay forma de guardar una playlist entera en un archivo. Este script lee la lista que ya tienes abierta.

¿Tengo que iniciar sesión o conectar mi cuenta?

No para playlists públicas y álbumes: se abren sin sesión. A diferencia de casi todos los exportadores online, no autorizas nada, así que ninguna app accede a tu cuenta.

¿Puedo pasar la playlist a Apple Music, YouTube Music o Tidal?

Sí. Expórtala en CSV e impórtala con un conversor de playlists, o usa el formato TXT («Artista - Título» por línea) y pégalo en uno.

¿Funciona con playlists largas?

Sí. Spotify solo mantiene unas pocas filas en pantalla, así que el script se desplaza y las recoge todas. Lo probamos con playlists de 150 canciones; tarda unos segundos.

Más herramientas para Spotify