diff --git a/README.md b/README.md index e870fac..eee0884 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.js b/app.js index 163fbfe..4cb22b7 100644 --- a/app.js +++ b/app.js @@ -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 }); diff --git a/public/index.html b/public/index.html index 99ad836..7b02799 100644 --- a/public/index.html +++ b/public/index.html @@ -3,40 +3,36 @@ Spotify Duplicate Finder - + +
-
-

This is an example of the Authorization Code flow

- Log in with Spotify -
+

Duplicates finder + + + Log in with Spotify + +

+
-

Duplicates finder

- -
+

Playlists + +

-
@@ -44,217 +40,42 @@
- - - - - + + diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..bdf0496 --- /dev/null +++ b/public/script.js @@ -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 = "
"; + + for (var i = 0, l = items.length; i < l; i++) { + out = out + options.fn(items[i]); + } + + return out + "
"; + }); + + 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 open an issue.' + }); + } 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 return to login.' + }); + } + } + } + }); +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..ff36c6c --- /dev/null +++ b/public/style.css @@ -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; +}