Merge pull request #1 from Crocmagnon/master

Merge code
This commit is contained in:
Philippe Vienne 2016-04-17 19:08:11 +02:00
commit 2f16f86c5a
5 changed files with 269 additions and 220 deletions

View file

@ -7,7 +7,7 @@ This app lets you find duplicates in your Spotify playlists.
## Usage
When using the application, you should first login with Spotify. The app only requests access to your private and collaborative playlists.
After login, you can get all the playlists by clicking the `Get playlists` button. It may take a while if you have many ones.
After login, you can get all the playlists by clicking the `Refresh` button. It may take a while if you have many ones.
After playlists are loaded, you can click on any name to launch duplicates finding. This may also take a while as Spotify only allows me to retrieve tracks 100 by 100.

2
app.js
View file

@ -142,7 +142,7 @@ app.get('/get_playlists', function (req, res) {
json: true
};
getAllPages(authOptions, [], function(data) {
getAllPages(authOptions, [], function (data) {
res.send({
'data': data
});

View file

@ -3,40 +3,36 @@
<head>
<title>Spotify Duplicate Finder</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<style type="text/css">
#login, #loggedin {
display: none;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 500px;
}
</style>
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/font-awesome/3.0.2/css/font-awesome.css">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div id="login">
<h1>This is an example of the Authorization Code flow</h1>
<a href="/login" class="btn btn-primary">Log in with Spotify</a>
</div>
<h1>Duplicates finder
<button class="btn btn-default has-spinner" id="obtain-new-token">
Refresh token <span class="spinner"><i class="icon-spin icon-refresh"></i></span>
</button>
<span id="login">
<a href="/login" class="btn btn-lg btn-success">Log in with Spotify</a>
</span>
</h1>
<div id="error"></div>
<div id="loggedin">
<div id="user-profile">
</div>
<h1>Duplicates finder</h1>
<button class="btn btn-default" id="get-playlists">Get playlists</button>
<div id="spin-playlists"></div>
<div class="media">
<div class="pull-left">
<h2 id="playlists-title">Playlists
<button class="btn btn-default has-spinner" id="get-playlists">
Refresh <span class="spinner"><i class="icon-spin icon-refresh"></i></span>
</button>
</h2>
<div id="playlists">
</div>
</div>
<div class="media-body">
<div id="spin-dups"></div>
<div id="dups">
</div>
</div>
@ -44,217 +40,42 @@
</div>
</div>
<script id="user-profile-template" type="text/x-handlebars-template">
<h1>Logged in as {{display_name}}</h1>
<div id="oauth">
</div>
<button class="btn btn-default" id="obtain-new-token">Obtain new token using the refresh token</button>
<script id="playlists-template" type="text/x-handlebars-template">
{{#list playlists}}
<a class="pl_item list-group-item has-spinner" href="/pl/{{pl_uid}}/{{pl_id}}">
{{pl_name}}
<span class="spinner"><i class="icon-spin icon-refresh"></i></span>
<i class="glyphicon glyphicon-chevron-right"></i>
</a>
{{/list}}
</script>
<script id="oauth-template" type="text/x-handlebars-template">
<h2>oAuth info</h2>
<dl class="dl-horizontal">
<dt>Access token</dt>
<dd class="text-overflow">{{access_token}}</dd>
<dt>Refresh token</dt>
<dd class="text-overflow">{{refresh_token}}></dd>
</dl>
<script id="user-profile-template" type="text/x-handlebars-template">
Logged in as {{display_name}}
</script>
<script id="dups-template" type="text/x-handlebars-template">
<h2>Duplicates in {{pl_name}}</h2>
{{message}}
{{#list dups}}<div class="list-group-item">{{dup_trackname}} - {{dup_artist}}</div>{{/list}}
{{#list dups}}
<div class="list-group-item">{{dup_trackname}} - {{dup_artist}}</div>
{{/list}}
</script>
<script id="playlists-template" type="text/x-handlebars-template">
<h2>Playlists</h2>
{{#list playlists}}<a class="pl_item list-group-item" href="/pl/{{pl_uid}}/{{pl_id}}">{{pl_name}}</a>{{/list}}
<script id="error-template" type="text/x-handlebars-template">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<strong>{{err_title}}</strong> {{{err_content}}}.
</div>
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0-alpha.1/handlebars.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.3.min.js"></script>
<script src="https://fgnass.github.io/spin.js/spin.min.js"></script>
<script>
(function () {
/**
* Obtains parameters from the hash of the URL
* @return Object
*/
function getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while (e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
Handlebars.registerHelper('list', function (items, options) {
var out = "<div class='list-group'>";
for (var i = 0, l = items.length; i < l; i++) {
out = out + options.fn(items[i]);
}
return out + "</div>";
});
// Spinner options
var spinnerOpts = {
lines: 13 // The number of lines to draw
, length: 0 // The length of each line
, width: 28 // The line thickness
, radius: 58 // The radius of the inner circle
, scale: 0.25 // Scales overall size of the spinner
, corners: 1 // Corner roundness (0..1)
, color: '#000' // #rgb or #rrggbb or array of colors
, opacity: 0.25 // Opacity of the lines
, rotate: 0 // The rotation offset
, direction: 1 // 1: clockwise, -1: counterclockwise
, speed: 1 // Rounds per second
, trail: 60 // Afterglow percentage
, fps: 20 // Frames per second when using setTimeout() as a fallback for CSS
, zIndex: 2e9 // The z-index (defaults to 2000000000)
, className: 'spinner' // The CSS class to assign to the spinner
, top: '50%' // Top position relative to parent
, left: '50%' // Left position relative to parent
, shadow: false // Whether to render a shadow
, hwaccel: false // Whether to use hardware acceleration
, position: 'absolute' // Element positioning
};
var userProfileSource = document.getElementById('user-profile-template').innerHTML,
userProfileTemplate = Handlebars.compile(userProfileSource),
userProfilePlaceholder = document.getElementById('user-profile');
var playlistsSource = document.getElementById('playlists-template').innerHTML,
playlistsTemplate = Handlebars.compile(playlistsSource),
playlistsPlaceholder = document.getElementById('playlists');
var dupsSource = document.getElementById('dups-template').innerHTML,
dupsTemplate = Handlebars.compile(dupsSource),
dupsPlaceholder = document.getElementById('dups');
var oauthSource;
var oauthTemplate;
var oauthPlaceholder;
var params = getHashParams();
var access_token = params.access_token,
refresh_token = params.refresh_token,
error = params.error;
if (error) {
alert('There was an error during the authentication');
} else {
if (access_token) {
$.ajax({
url: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': 'Bearer ' + access_token
},
success: function (response) {
userProfilePlaceholder.innerHTML = userProfileTemplate(response);
oauthSource = document.getElementById('oauth-template').innerHTML;
oauthTemplate = Handlebars.compile(oauthSource);
oauthPlaceholder = document.getElementById('oauth');
// render oauth info
oauthPlaceholder.innerHTML = oauthTemplate({
access_token: access_token,
refresh_token: refresh_token
});
$('#login').hide();
$('#loggedin').show();
}
});
} else {
// render initial screen
$('#login').show();
$('#loggedin').hide();
}
document.getElementById('get-playlists').addEventListener('click', function () {
var target = document.getElementById('spin-playlists');
var spinner = new Spinner(spinnerOpts).spin(target);
$.ajax({
url: '/get_playlists',
data: {
'access_token': access_token
}
}).done(function (data) {
var pl = data.data.map(function (item, index, array) {
return {
pl_uid: item.owner.id,
pl_name: item.name,
pl_id: item.id
}
});
spinner.stop();
playlistsPlaceholder.innerHTML = playlistsTemplate({
playlists: pl
});
})
}, false);
$(document).on('click', '.pl_item', function (e) {
e.preventDefault();
var pl_name = $(this).text();
var target = document.getElementById('spin-dups');
var spinner = new Spinner(spinnerOpts).spin(target);
$.ajax({
url: $(this).attr('href'),
data: {
'access_token': access_token
}
}).done(function (data) {
var dups = data.data.map(function (item) {
return {
dup_trackname: item.track.name,
dup_artist: item.track.artists[0].name
}
});
spinner.stop();
if (data.data.length > 0) {
dupsPlaceholder.innerHTML = dupsTemplate({
dups: dups,
pl_name: pl_name
});
}
else {
dupsPlaceholder.innerHTML = dupsTemplate({
dups: [],
message: "No duplicate found.",
pl_name: pl_name
});
}
})
});
$(document).on('click', '#obtain-new-token', function () {
$.ajax({
url: '/refresh_token',
data: {
'refresh_token': refresh_token
}
}).done(function (data) {
access_token = data.access_token;
oauthPlaceholder.innerHTML = oauthTemplate({
access_token: access_token,
refresh_token: refresh_token
});
});
});
}
})();
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
<script src="./script.js"></script>
</body>
</html>

182
public/script.js Normal file
View file

@ -0,0 +1,182 @@
'use strict';
var access_token;
var refresh_token;
var error;
var userProfileSource = document.getElementById('user-profile-template').innerHTML,
userProfileTemplate = Handlebars.compile(userProfileSource),
userProfilePlaceholder = document.getElementById('user-profile');
var playlistsSource = document.getElementById('playlists-template').innerHTML,
playlistsTemplate = Handlebars.compile(playlistsSource),
playlistsPlaceholder = document.getElementById('playlists');
var dupsSource = document.getElementById('dups-template').innerHTML,
dupsTemplate = Handlebars.compile(dupsSource),
dupsPlaceholder = document.getElementById('dups');
var errorSource = document.getElementById('error-template').innerHTML,
errorTemplate = Handlebars.compile(errorSource),
errorPlaceholder = document.getElementById('error');
(function () {
/**
* Obtains parameters from the hash of the URL
* @return Object
*/
function getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while (e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
Handlebars.registerHelper('list', function (items, options) {
var out = "<div class='list-group'>";
for (var i = 0, l = items.length; i < l; i++) {
out = out + options.fn(items[i]);
}
return out + "</div>";
});
var params = getHashParams();
access_token = params.access_token;
refresh_token = params.refresh_token;
error = params.error;
if (error) {
errorPlaceholder.innerHTML = errorTemplate({
err_title: 'Error!',
err_content: 'There was an error during the authentication. Feel free to <a href="https://github.com/Crocmagnon/Spotify-Duplicate-Finder/issues" class="alert-link">open an issue</a>.'
});
} else {
if (access_token) {
getPersonnalInfo(true, userProfilePlaceholder, userProfileTemplate);
} else {
// render initial screen
$('#login').show();
$('#loggedin').hide();
}
document.getElementById('get-playlists').addEventListener('click', function () {
var button = $(this);
button.addClass('loading');
$.ajax({
url: '/get_playlists',
data: {
'access_token': access_token
}
}).done(function (data) {
var pl = data.data.map(function (item) {
return {
pl_uid: item.owner.id,
pl_name: item.name,
pl_id: item.id
}
});
button.removeClass('loading');
$('#dups').hide();
playlistsPlaceholder.innerHTML = playlistsTemplate({
playlists: pl
});
})
}, false);
$(document).on('click', '.pl_item', function (e) {
e.preventDefault();
var pl_name = $(this).text();
$('.pl_item').removeClass('active');
var currentElement = $(this);
currentElement.addClass('active');
currentElement.addClass('loading');
$('#dups').hide();
$.ajax({
url: $(this).attr('href'),
data: {
'access_token': access_token
}
}).done(function (data) {
var dups = data.data.map(function (item) {
return {
dup_trackname: item.track.name,
dup_artist: item.track.artists[0].name
}
});
currentElement.removeClass('loading');
if (data.data.length > 0) {
dupsPlaceholder.innerHTML = dupsTemplate({
dups: dups,
pl_name: pl_name
});
}
else {
dupsPlaceholder.innerHTML = dupsTemplate({
dups: [],
message: "No duplicate found.",
pl_name: pl_name
});
}
$('#dups').show();
})
});
$(document).on('click', '#obtain-new-token', function () {
refreshToken();
});
}
})();
function refreshToken() {
var button = $('#obtain-new-token');
button.addClass('loading');
$.ajax({
url: '/refresh_token',
data: {
'refresh_token': refresh_token
}
}).done(function (data) {
access_token = data.access_token;
button.removeClass('loading');
});
}
function getPersonnalInfo(first, userProfilePlaceholder, userProfileTemplate) {
$.ajax({
url: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': 'Bearer ' + access_token
},
success: function (response) {
userProfilePlaceholder.innerHTML = userProfileTemplate(response);
$('#login').hide();
$('#obtain-new-token').show();
$('#loggedin').show();
},
error: function (response) {
if (response.status == 401) {
if (first) {
refreshToken();
getPersonnalInfo(false, userProfilePlaceholder, userProfileTemplate);
}
else {
errorPlaceholder.innerHTML = errorTemplate({
err_title: 'Error!',
err_content: 'Error while refreshing token. Please <a href="/" class="alert-link">return to login</a>.'
});
}
}
}
});
}

46
public/style.css Normal file
View file

@ -0,0 +1,46 @@
#login, #loggedin, #obtain-new-token {
display: none;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 500px;
}
.spinner {
display: none;
width: 0;
float: right;
margin-left: 5px;
}
.has-spinner.loading .spinner {
display: inline-block;
width: 16px; /* This doesn't work, just fix for unkown width elements */
}
#get-playlists {
float: right;
}
.pl_item {
text-align: left;
}
.glyphicon.glyphicon-chevron-right {
float: right;
display: inline-block;
width: 16px;
margin-left: 5px;
}
#loggedin .media .pull-left {
min-width: 20%;
}
.loading .glyphicon-chevron-right.glyphicon-chevron-right {
display: none;
width: 0;
}