Created
January 18, 2020 10:29
-
-
Save crgeary/9d2ca581e813bdf8ebc9f97d781973e8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.Helpers = { | |
authorize: function() { | |
var client_id = this.getQueryParam('app_client_id'); | |
// Use Exportify application client_id if none given | |
if (client_id == '') { | |
client_id = "9950ac751e34487dbbe027c4fd7f8e99" | |
} | |
window.location = "https://accounts.spotify.com/authorize" + | |
"?client_id=" + client_id + | |
"&redirect_uri=" + encodeURIComponent([location.protocol, '//', location.host, location.pathname].join('')) + | |
"&scope=playlist-read-private%20playlist-read-collaborative" + | |
"&response_type=token"; | |
}, | |
// http://stackoverflow.com/a/901144/4167042 | |
getQueryParam: function(name) { | |
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); | |
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), | |
results = regex.exec(location.search); | |
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); | |
}, | |
apiCall: function(url, access_token) { | |
return $.ajax({ | |
url: url, | |
headers: { | |
'Authorization': 'Bearer ' + access_token | |
} | |
}).fail(function (jqXHR, textStatus) { | |
if (jqXHR.status == 401) { | |
// Return to home page after auth token expiry | |
window.location = window.location.href.split('#')[0] | |
} else if (jqXHR.status == 429) { | |
// API Rate-limiting encountered | |
window.location = window.location.href.split('#')[0] + '?rate_limit_message=true' | |
} else { | |
// Otherwise report the error so user can raise an issue | |
alert(jqXHR.responseText); | |
} | |
}) | |
} | |
} | |
var PlaylistTable = React.createClass({ | |
getInitialState: function() { | |
return { | |
playlists: [], | |
playlistCount: 0, | |
nextURL: null, | |
prevURL: null | |
}; | |
}, | |
loadPlaylists: function(url) { | |
var userId = ''; | |
var firstPage = typeof url === 'undefined' || url.indexOf('offset=0') > -1; | |
window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) { | |
userId = response.id; | |
// Show starred playlist if viewing first page | |
if (firstPage) { | |
return $.when.apply($, [ | |
window.Helpers.apiCall( | |
"https://api.spotify.com/v1/users/" + userId + "/starred", | |
this.props.access_token | |
), | |
window.Helpers.apiCall( | |
"https://api.spotify.com/v1/users/" + userId + "/playlists", | |
this.props.access_token | |
) | |
]) | |
} else { | |
return window.Helpers.apiCall(url, this.props.access_token); | |
} | |
}.bind(this)).done(function() { | |
var response; | |
var playlists = []; | |
if (arguments[1] === 'success') { | |
response = arguments[0]; | |
playlists = arguments[0].items; | |
} else { | |
response = arguments[1][0]; | |
playlists = $.merge([arguments[0][0]], arguments[1][0].items); | |
} | |
if (this.isMounted()) { | |
this.setState({ | |
playlists: playlists, | |
playlistCount: response.total, | |
nextURL: response.next, | |
prevURL: response.previous | |
}); | |
$('#playlists').fadeIn(); | |
$('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId) | |
} | |
}.bind(this)) | |
}, | |
exportPlaylists: function() { | |
PlaylistsExporter.export(this.props.access_token, this.state.playlistCount); | |
}, | |
componentDidMount: function() { | |
this.loadPlaylists(this.props.url); | |
}, | |
render: function() { | |
if (this.state.playlists.length > 0) { | |
return ( | |
<div id="playlists"> | |
<Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/> | |
<table className="table table-hover"> | |
<thead> | |
<tr> | |
<th style={{width: "30px"}}></th> | |
<th>Name</th> | |
<th style={{width: "150px"}}>Owner</th> | |
<th style={{width: "100px"}}>Tracks</th> | |
<th style={{width: "120px"}}>Public?</th> | |
<th style={{width: "120px"}}>Collaborative?</th> | |
<th style={{width: "100px"}} className="text-right"><button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}><span className="fa fa-file-archive-o"></span> Export All</button></th> | |
</tr> | |
</thead> | |
<tbody> | |
{this.state.playlists.map(function(playlist, i) { | |
return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>; | |
}.bind(this))} | |
</tbody> | |
</table> | |
<Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/> | |
</div> | |
); | |
} else { | |
return <div className="spinner"></div> | |
} | |
} | |
}); | |
var PlaylistRow = React.createClass({ | |
exportPlaylist: function() { | |
PlaylistExporter.export(this.props.access_token, this.props.playlist); | |
}, | |
renderTickCross: function(condition) { | |
if (condition) { | |
return <i className="fa fa-lg fa-check-circle-o"></i> | |
} else { | |
return <i className="fa fa-lg fa-times-circle-o" style={{ color: '#ECEBE8' }}></i> | |
} | |
}, | |
renderIcon: function(playlist) { | |
if (playlist.name == 'Starred') { | |
return <i className="glyphicon glyphicon-star" style={{ color: 'gold' }}></i>; | |
} else { | |
return <i className="fa fa-music"></i>; | |
} | |
}, | |
render: function() { | |
playlist = this.props.playlist | |
if(playlist.uri==null) return ( | |
<tr key={this.props.key}> | |
<td>{this.renderIcon(playlist)}</td> | |
<td>{playlist.name}</td> | |
<td colSpan="2">This playlist is not supported</td> | |
<td>{this.renderTickCross(playlist.public)}</td> | |
<td>{this.renderTickCross(playlist.collaborative)}</td> | |
<td> </td> | |
</tr> | |
); | |
return ( | |
<tr key={this.props.key}> | |
<td>{this.renderIcon(playlist)}</td> | |
<td><a href={playlist.uri}>{playlist.name}</a></td> | |
<td><a href={playlist.owner.uri}>{playlist.owner.id}</a></td> | |
<td>{playlist.tracks.total}</td> | |
<td>{this.renderTickCross(playlist.public)}</td> | |
<td>{this.renderTickCross(playlist.collaborative)}</td> | |
<td className="text-right"><button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}><span className="glyphicon glyphicon-save"></span> Export</button></td> | |
</tr> | |
); | |
} | |
}); | |
var Paginator = React.createClass({ | |
nextClick: function(e) { | |
e.preventDefault() | |
if (this.props.nextURL != null) { | |
this.props.loadPlaylists(this.props.nextURL) | |
} | |
}, | |
prevClick: function(e) { | |
e.preventDefault() | |
if (this.props.prevURL != null) { | |
this.props.loadPlaylists(this.props.prevURL) | |
} | |
}, | |
render: function() { | |
if (this.props.nextURL != null || this.props.prevURL != null) { | |
return ( | |
<nav className="paginator text-right"> | |
<ul className="pagination pagination-sm"> | |
<li className={this.props.prevURL == null ? 'disabled' : ''}> | |
<a href="#" aria-label="Previous" onClick={this.prevClick}> | |
<span aria-hidden="true">«</span> | |
</a> | |
</li> | |
<li className={this.props.nextURL == null ? 'disabled' : ''}> | |
<a href="#" aria-label="Next" onClick={this.nextClick}> | |
<span aria-hidden="true">»</span> | |
</a> | |
</li> | |
</ul> | |
</nav> | |
) | |
} else { | |
return <div> </div> | |
} | |
} | |
}); | |
// Handles exporting all playlist data as a zip file | |
var PlaylistsExporter = { | |
export: function(access_token, playlistCount) { | |
var playlistFileNames = []; | |
window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) { | |
var limit = 20; | |
var userId = response.id; | |
// Initialize requests with starred playlist | |
var requests = [ | |
window.Helpers.apiCall( | |
"https://api.spotify.com/v1/users/" + userId + "/starred", | |
access_token | |
) | |
]; | |
// Add other playlists | |
for (var offset = 0; offset < playlistCount; offset = offset + limit) { | |
var url = "https://api.spotify.com/v1/users/" + userId + "/playlists"; | |
requests.push( | |
window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token) | |
) | |
} | |
$.when.apply($, requests).then(function() { | |
var playlists = []; | |
var playlistExports = []; | |
// Handle either single or multiple responses | |
if (typeof arguments[0].href == 'undefined') { | |
$(arguments).each(function(i, response) { | |
if (typeof response[0].items === 'undefined') { | |
// Single playlist | |
playlists.push(response[0]); | |
} else { | |
// Page of playlists | |
$.merge(playlists, response[0].items); | |
} | |
}) | |
} else { | |
playlists = arguments[0].items | |
} | |
$(playlists).each(function(i, playlist) { | |
playlistFileNames.push(PlaylistExporter.fileName(playlist)); | |
playlistExports.push(PlaylistExporter.csvData(access_token, playlist)); | |
}); | |
return $.when.apply($, playlistExports); | |
}).then(function() { | |
var zip = new JSZip(); | |
var responses = []; | |
$(arguments).each(function(i, response) { | |
zip.file(playlistFileNames[i], response) | |
}); | |
var content = zip.generate({ type: "blob" }); | |
saveAs(content, "spotify_playlists.zip"); | |
}); | |
}); | |
} | |
} | |
// Handles exporting a single playlist as a CSV file | |
var PlaylistExporter = { | |
export: function(access_token, playlist) { | |
this.csvData(access_token, playlist).then(function(data) { | |
var blob = new Blob(["\uFEFF" + data], { type: "text/csv;charset=utf-8" }); | |
saveAs(blob, this.fileName(playlist)); | |
}.bind(this)) | |
}, | |
csvData: function(access_token, playlist) { | |
var requests = []; | |
var limit = 100; | |
for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) { | |
requests.push( | |
window.Helpers.apiCall(playlist.tracks.href.split('?')[0] + '?offset=' + offset + '&limit=' + limit, access_token) | |
) | |
} | |
return $.when.apply($, requests).then(function() { | |
var responses = []; | |
// Handle either single or multiple responses | |
if (typeof arguments[0] != 'undefined') { | |
if (typeof arguments[0].href == 'undefined') { | |
responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] }); | |
} else { | |
responses = [arguments[0]]; | |
} | |
} | |
var trackItems = responses.map(function(response) { | |
return response.items.map(function(item) { | |
return item; | |
}); | |
}); | |
// Flatten the array of pages | |
trackItems = $.map(trackItems, function(n) { return n }) | |
var featureRequests = []; | |
trackItems.forEach(function (track) { | |
featureRequests.push( | |
window.Helpers.apiCall('https://api.spotify.com/v1/audio-features/' + track.track.id, access_token) | |
); | |
}); | |
return $.when.apply($, featureRequests).then(function() { | |
var featureResponses = []; | |
if (typeof arguments[0] != 'undefined') { | |
if (typeof arguments[0].href == 'undefined') { | |
featureResponses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] }); | |
} else { | |
featureResponses = [arguments[0]]; | |
} | |
} | |
var tracks = trackItems.map(function (item) { | |
var audioFeatures = featureResponses.filter(function (r) { return r.id == item.track.id })[0]; | |
return [ | |
item.track.id, | |
item.track.uri, | |
item.track.name, | |
item.track.artists.map(function(artist) { return artist.name }).join(', '), | |
item.track.album.name, | |
item.track.disc_number, | |
item.track.track_number, | |
item.track.duration_ms, | |
audioFeatures.danceability, | |
item.added_by == null ? '' : item.added_by.uri, | |
item.added_at, | |
].map(function(track) { return '"' + track + '"'; }); | |
}); | |
tracks.unshift([ | |
"ID", | |
"Spotify URI", | |
"Track Name", | |
"Artist Name", | |
"Album Name", | |
"Disc Number", | |
"Track Number", | |
"Track Duration (ms)", | |
"Danceability", | |
"Added By", | |
"Added At", | |
]); | |
csvContent = ''; | |
tracks.forEach(function(infoArray, index){ | |
dataString = infoArray.join(","); | |
csvContent += index < tracks.length ? dataString+ "\n" : dataString; | |
}); | |
return csvContent; | |
}); | |
}); | |
}, | |
fileName: function(playlist) { | |
return playlist.name.replace(/[^a-z0-9\- ]/gi, '').replace(/[ ]/gi, '_').toLowerCase() + ".csv"; | |
} | |
} | |
$(function() { | |
var vars = window.location.hash.substring(1).split('&'); | |
var key = {}; | |
for (i=0; i<vars.length; i++) { | |
var tmp = vars[i].split('='); | |
key[tmp[0]] = tmp[1]; | |
} | |
if (window.Helpers.getQueryParam('rate_limit_message') != '') { | |
// Show rate limit message | |
$('#rateLimitMessage').show(); | |
} else if (typeof key['access_token'] === 'undefined') { | |
$('#loginButton').css('display', 'inline-block') | |
} else { | |
React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment