Skip to content

Instantly share code, notes, and snippets.

@skinkade
Created June 30, 2021 13:26
Show Gist options
  • Save skinkade/9d5c7a11ff65dd76e82d7b72e5560bff to your computer and use it in GitHub Desktop.
Save skinkade/9d5c7a11ff65dd76e82d7b72e5560bff to your computer and use it in GitHub Desktop.
Export Spotify Playlist to CSV
// While browsing a playlist in your browser,
// i.e. browser address is https://open.spotify.com/playlist/{playlistId},
// copy/paste these functions into the browser console,
// then run exportCurrentPlaylist()
// It should prompt a download for:
// [Playlist name] - [Playlist ID].csv
// Containing:
// Song, Artist(s), Album, Song Link, Main Artist Link, Album Link
// Disclaimer: written quickly by a non-JS dev
// Note: should probably work within the desktop client via Electron inspector (untested)
function getCurrentPlaylistId() {
return document
.querySelector('[data-testid="playlist-page"]')
.getAttribute('data-test-uri')
.split(':')[2];
}
function getAccessToken() {
const config = JSON.parse(document.getElementById('config').textContent);
if (config['accessTokenExpirationTimestampMs'] <= Date.now()) {
throw "Access token has expired. Refresh page to get a new one."
}
return config['accessToken'];
}
async function getPlaylistInfo(playlistId, accessToken) {
const url = `https://api.spotify.com/v1/playlists/${playlistId}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}
async function getTracks(playlistId, accessToken, next) {
throw 'Not implemented';
}
function processTrackData(tracks) {
let processed = [];
tracks.forEach(t => {
let info = {};
const track = t['track'];
info['song'] = track['name'];
info['songLink'] = track['external_urls']['spotify'];
const album = track['album'];
info['album'] = album['name'];
info['albumLink'] = album['external_urls']['spotify'];
const artists = album['artists'];
info['artists'] = artists.map(a => a['name']).join(', ');
info['mainArtistLink'] = artists[0]['external_urls']['spotify'];
processed.push(info);
})
return processed;
}
async function getExportData() {
const playlistId = getCurrentPlaylistId();
const accessToken = getAccessToken();
const playlistInfo = await getPlaylistInfo(playlistId, accessToken);
let exportData = {};
exportData['playlistId'] = playlistId;
exportData['playlistName'] = playlistInfo['name'];
// TODO: for >100 tracks, use getTracks() to fetch the rest
exportData['tracks'] = processTrackData(playlistInfo['tracks']['items']);
return exportData;
}
// May or may not be sufficient for this purpose
function csvEscapeValue(str) {
return ['"', str, '"'].join('');
}
function generateTrackCsv(tracks) {
let rows = [];
rows.push([
'Song',
'Artist(s)',
'Album',
'Song Link',
'Main Artist Link',
'Album Link'
].join(','));
tracks.forEach(track => {
const info = [
csvEscapeValue(track['song']),
csvEscapeValue(track['artists']),
csvEscapeValue(track['album']),
track['songLink'],
track['mainArtistLink'],
track['albumLink']
]
.join(',');
rows.push(info);
})
// Don't know if we need the \r for Windows,
// but adding it doesn't create problems on Linux
return rows.join('\r\n');
}
function downloadCsv(basename, contents) {
let a = document.createElement('a');
a.download = basename + '.csv';
// Needs encoding because of newlines
a.href = `data:text/csv,` + encodeURIComponent(contents);
a.click();
}
async function exportCurrentPlaylist() {
const exportData = await getExportData();
const nameAndId = [
exportData['playlistName'],
exportData['playlistId']
].join(' - ');
const csvContent = generateTrackCsv(exportData['tracks']);
downloadCsv(nameAndId, csvContent);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment