Download any TIDAL playlist, album or your favourite tracks 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 account.
Works on TIDAL for the web (tidal.com) on Windows, Mac, Linux and Chromebook. Open the playlist, then run the script.
// Responsive Muse · TIDAL Playlist Exporter · v1.0 · 2026-11-12
// Saves any TIDAL playlist, album or your favourite tracks that you can open on tidal.com
// to CSV or TXT: title, artists, album, duration and link. Useful to back up a playlist or
// move it to Spotify, Apple Music, YouTube Music or Deezer. 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 TIDAL playlist';
const FORMAT = 'csv'; // 'csv' (Excel, Sheets, playlist converters) or 'txt' ("Artist - Title", one per line)
if (!/(^|\.)tidal\.com$/.test(location.hostname) || !/\/(playlist|album|mix)\/|\/my-collection\/tracks/.test(location.pathname)) {
alert('Open a playlist, an album or your favourite tracks on tidal.com first, for example:\nhttps://tidal.com/my-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} ${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 clean = s => (s || '').replace(/\s+/g, ' ').trim();
const grid = document.querySelector('[data-test="media-table"][aria-rowcount], [aria-rowcount]');
const expected = grid ? Number(grid.getAttribute('aria-rowcount')) : 0;
const heading = [...document.querySelectorAll('main [data-test="title"], main h1')].map(e => clean(e.textContent)).find(Boolean) || '';
const pageAlbum = /\/album\//.test(location.pathname) ? heading : '';
const field = (row, name) => row.querySelector(`[data-test="${name}"]`);
// Read the songs currently on screen (rows are recycled while scrolling: key = row number)
const collect = () => {
for (const row of document.querySelectorAll('[data-test="tracklist-row"][data-track-id]')) {
const n = Number(row.dataset.index) + 1;
if (!(n > 0) || tracks.has(n)) continue;
const artists = [...row.querySelectorAll('[data-test="track-row-artist"] a')].map(x => clean(x.textContent));
tracks.set(n, {
n,
title: clean(field(row, 'table-cell-title')?.textContent),
artist: artists.join(', ') || clean(field(row, 'track-row-artist')?.textContent),
album: clean(field(row, 'track-row-album')?.textContent) || pageAlbum,
duration: clean(field(row, 'duration')?.textContent),
url: `https://tidal.com/track/${row.dataset.trackId}`,
});
}
};
// 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; });
document.querySelector('[data-test="tracklist-row"]')?.scrollIntoView({ block: 'start' });
await sleep(800);
let idle = 0;
while (idle < 3 && !stop && !(expected && tracks.size >= expected)) {
const before = tracks.size;
collect();
[...document.querySelectorAll('[data-test="tracklist-row"]')].pop()?.scrollIntoView({ block: 'start' });
await sleep(700);
collect();
idle = tracks.size === before ? idle + 1 : 0;
panel.text(`Collecting songs… ${tracks.size}${expected ? ' of ' + expected : ''}`);
}
const list = [...tracks.values()].sort((x, y) => x.n - y.n);
if (!list.length) {
panel.finish('No songs found. Is the playlist empty or private? If not, TIDAL 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 missing = expected && list.length < expected ? ` (the playlist says ${expected}; scroll down and run it again if some are missing)` : '';
const name = (heading || clean(document.title.split(' - TIDAL')[0]) || 'playlist').replace(/[\\/:*?"<>|]+/g, '_');
deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'song')}${missing}.`, 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: 132 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 TIDAL playlist button to your bookmarks bar and click it on tidal.com. No warning that way.
Bookmarklet: drag the middle button to your bookmarks bar, open tidal.com and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is tidal.com itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on tidal.com. Anywhere else it stops before doing anything.
- Short and readable. 132 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:
81fc0655b3d415a3…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to tidal.com.
How to use it
- Open the playlist on tidal.com in a computer browser. Public playlists and albums work without signing in; for your own playlists or favourite tracks, sign in first.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Copy the script, paste it in the console and press Enter. It scrolls through the list and then offers the file.
What you get
One row per song with its position, title, artists, album, duration and TIDAL link. Choose TXT for a plain “Artist – Title” list that playlist converters and search boxes understand.
Why not a transfer site?
TIDAL can import music from other services but has no way to save your own playlists as a file. Transfer sites ask you to sign in to both services and limit free transfers. This script reads the list you already have on screen and sends nothing anywhere.
FAQ
Doesn't TIDAL have an export button?
No. TIDAL lets you import music from other services, but not save your own playlists as a file. This script reads the playlist you have open.
Do I need to log in or connect my account?
Not for public playlists and albums: they open without signing in. Your own playlists and favourite tracks need you to be signed in. You never authorize any app.
Can I move the playlist to Spotify, Apple Music or Deezer?
Yes. Export it as CSV and import the file with a playlist converter, or use the TXT format ("Artist - Title" per line) and paste it into one.
Does it work with long playlists?
Yes. TIDAL only keeps a few rows on screen, so the script scrolls and collects them all.
