From b1ec45015df5a12d556aff21de6076782f93c467 Mon Sep 17 00:00:00 2001 From: Christoph Rohrer Date: Sat, 10 Jun 2017 12:34:35 +0200 Subject: [PATCH] Better refreshing of accessTokens When making a lot of consecutive search requests the accessToken might expire. Now the application refreshes the accessToken when this happens while searching. --- spotifyOAuth.js | 117 ++++++++++++++++++++---------------- spotifyPlaylist.js | 38 +++++++----- spotifySearch.js | 146 +++++++++++++++++++++++++-------------------- 3 files changed, 172 insertions(+), 129 deletions(-) diff --git a/spotifyOAuth.js b/spotifyOAuth.js index ec49436..e0fbdf9 100644 --- a/spotifyOAuth.js +++ b/spotifyOAuth.js @@ -6,6 +6,8 @@ var http = require('http'); var https = require('https'); var fs = require('fs'); var url = require('url') ; +var Promise = require('bluebird'); +var logger = require('./logger'); var querystring = require('querystring'); var config = JSON.parse(fs.readFileSync('config.json', 'utf8')); var server = http.createServer(handleRequest); @@ -30,14 +32,16 @@ var spotifyOAuth = { }); }, refresh: function(){ - try { - console.log('refreshing token...'); - var refreshToken = fs.readFileSync('refreshToken', 'utf8'); - getToken(undefined, refreshToken); - } catch (e){ - console.log('no refreshToken found'); - spotifyOAuth.authenticate(); - } + return new Promise(resolve => { + try { + console.log('refreshing token...'); + let refreshToken = fs.readFileSync('refreshToken', 'utf8'); + resolve(getToken(undefined, refreshToken)); + } catch (e){ + logger.log('no refreshToken found'); + spotifyOAuth.authenticate(); + } + }); }, getAccessToken: function(){ try { @@ -47,7 +51,9 @@ var spotifyOAuth = { return spotifyOAuth.accessToken; } catch (e){ console.log('no accessToken found'); - spotifyOAuth.refresh(); + spotifyOAuth.refresh() + .then(require('./main').start) + .catch(error => logger.log(error, 'spotifyOAuth.getAccessToken')); } return false; } @@ -63,58 +69,64 @@ function handleRequest(request, response){ response.end('You can close this Window now.'); if(queryObject.code){ // this is the authorization code - getToken(queryObject.code); - server.close(); + getToken(queryObject.code) + .then(server.close) + .catch(error => logger.log(error)); } }); } /** * one of the params is required! - * @param code authCode or undefined - * @param refresh refreshToken or undefined + * @param {String|undefined} code authCode or undefined + * @param {String|undefined} refresh refreshToken or undefined + * @returns {Promise} */ function getToken(code, refresh){ - var jsonData; - if(refresh){ - jsonData = { - grant_type: "refresh_token", - refresh_token: refresh - }; - } else { - jsonData = { - grant_type: "authorization_code", - code: code, - redirect_uri: REDIRECT_URI - }; - } - var idAndSecret = config.clientId+':'+config.clientSecret; - var authString = 'Basic ' + new Buffer(idAndSecret).toString('base64'); - var data = querystring.stringify(jsonData); - var tokenReq = https.request({ - hostname: 'accounts.spotify.com', - path: '/api/token', - method: 'POST', - headers: { - 'Authorization': authString, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(data) + return new Promise((resolve, reject) => { + var jsonData; + if(refresh){ + jsonData = { + grant_type: "refresh_token", + refresh_token: refresh + }; + } else { + jsonData = { + grant_type: "authorization_code", + code: code, + redirect_uri: REDIRECT_URI + }; } - }, function(res){ - var body = ''; - res.on('data', function(chunk){ - body += new Buffer(chunk).toString(); + var idAndSecret = config.clientId+':'+config.clientSecret; + var authString = 'Basic ' + new Buffer(idAndSecret).toString('base64'); + var data = querystring.stringify(jsonData); + var tokenReq = https.request({ + hostname: 'accounts.spotify.com', + path: '/api/token', + method: 'POST', + headers: { + 'Authorization': authString, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(data) + } + }, function(res){ + var body = ''; + res.on('data', function(chunk){ + body += new Buffer(chunk).toString(); + }); + res.on('end', function(){ + var responseJson = JSON.parse(body); + writeAccessToken(responseJson.access_token); + writeRefreshToken(responseJson.refresh_token); + spotifyOAuth.accessToken = responseJson.access_token.replace(/\s/g,''); + resolve(spotifyOAuth.accessToken); + }); }); - res.on('end', function(){ - var responseJson = JSON.parse(body); - writeAccessToken(responseJson.access_token); - writeRefreshToken(responseJson.refresh_token); - spotifyOAuth.accessToken = responseJson.access_token.replace(/\s/g,''); - require('./main').start(); + tokenReq.on('error', () => { + reject('Error Refreshing Auth Token'); }); + tokenReq.end(data); }); - - tokenReq.end(data); } function writeAccessToken(token){ @@ -125,10 +137,15 @@ function writeRefreshToken(token){ writeFile('refreshToken', token); } +/** + * writes file in fs + * @param {String} name + * @param {String} content + */ function writeFile(name, content){ if(content){ fs.writeFileSync(name, content); - console.log(name + ' saved!'); + logger.log(name + ' saved!'); } } diff --git a/spotifyPlaylist.js b/spotifyPlaylist.js index 0df72b1..6502f27 100644 --- a/spotifyPlaylist.js +++ b/spotifyPlaylist.js @@ -53,7 +53,8 @@ function getTracks(playlistName, offset){ spotifyHelper.checkForRateLimit(res, 'requesting playlist tracks', () => spotifyPlaylist.getTracks(playlistName, offset)) .then(() => { if(res.statusCode === 401){ - spotifyOAuth.refresh(); + spotifyOAuth.refresh() + .then(require('./main').start); } else { var error = "Error getting tracks from playlist. Status "+res.statusCode; logger.log(error, playlistName); @@ -117,35 +118,44 @@ function addTracks(playlistName, results){ return Promise.all(requests); function makeAddRequest(items, timeout){ - return new Promise(resolve => { - setTimeout(() => { - var addRequest = https.request({ + return new Promise((resolve, reject) => { + let tryCounter = 0; // count how often this was tried to prevent endless loop + setTimeout(sendRequest, timeout); + + function sendRequest(){ + if(tryCounter > 5){ + return reject('request to add to playlist failed 5 times'); + } + tryCounter += 1; + + let addRequest = https.request({ hostname: 'api.spotify.com', - path: '/v1/users/'+config.userId+'/playlists/'+playlistConfig.playlistId+'/tracks?position=0&uris='+items.join(), // join all 40 track ids for the query string + path: '/v1/users/' + config.userId + '/playlists/' + playlistConfig.playlistId + '/tracks?position=0&uris=' + items.join(), // join all 40 track ids for the query string method: 'POST', headers: { - 'Authorization': 'Bearer '+ accessToken, + 'Authorization': 'Bearer ' + accessToken, 'Accept': 'application/json' } }, function(res){ - if(res.statusCode === 201){ - logger.log('Success! Added '+ items.length + ' tracks.', playlistName); + if (res.statusCode === 201) { + logger.log('Success! Added ' + items.length + ' tracks.', playlistName); resolve(); - return; } else { spotifyHelper.checkForRateLimit(res, 'adding to playlist', () => resolve(spotifyPlaylist.addTracks(playlistName, results))) - .then(() => { - if(res.statusCode === 401){ - spotifyOAuth.refresh(); + .then(() =>{ + if (res.statusCode === 401) { + spotifyOAuth.refresh() + .then(sendRequest) // try again + .catch(error => logger.log(error, playlistName)); } else { - logger.log("Error adding to playlist. Status "+res.statusCode, playlistName); + logger.log("Error adding to playlist. Status " + res.statusCode, playlistName); process.exit(1); } }); } }); addRequest.end(); - }, timeout); + } }); } } diff --git a/spotifySearch.js b/spotifySearch.js index 2f95f9f..9bd17b5 100644 --- a/spotifySearch.js +++ b/spotifySearch.js @@ -41,90 +41,106 @@ function searchTracks(tracks, playlistName){ */ function sendSearchRequest(track, timeOut, playlistName){ var time = timeOut || 0; + let tryCounter = 0; // count requests for one track to prevent infinite loop (in case of error the request might be tried again) return new Promise((resolve, reject) => { - setTimeout(() => makeRequest(resolve, reject), time); + setTimeout(() => { + resolve(makeRequest().catch(error => reject(error))); + }, time); }); /** - * @param {function} resolve Callback to resolve promise - * @param {function} reject + * @return {Promise} */ - function makeRequest(resolve, reject){ - let accessToken = spotifyOAuth.getAccessToken(); - - if(accessToken === false || accessToken === ''){ - return reject('no access token found'); - } - - var spotifySearchReq = https.request({ - hostname: "api.spotify.com", - path: "/v1/search?type=track&q=artist:"+encodeURIComponent(track.artist+' ')+'track:'+encodeURIComponent(track.title), - method: 'GET', - headers: { - 'Authorization': 'Bearer '+ accessToken, - 'Accept': 'application/json' + function makeRequest(){ + return new Promise((resolve, reject) => { + if(tryCounter > 3){ + return reject('Error: SearchRequest for "'+ track.artist + ' - ' + track.title + '" was tried more than 3 times.'); + } + tryCounter += 1; + let accessToken = spotifyOAuth.getAccessToken(); + + if(accessToken === false || accessToken === ''){ + return reject('no access token found'); } - }, function(res){ - spotifyHelper.checkForRateLimit(res, 'searching track', () => sendSearchRequest(track, 0, playlistName)) - .then((rateLimitPromise) => { - if(rateLimitPromise){ - return rateLimitPromise; - } - var jsonResponse = ''; - res.on('data', function(chunk){ - jsonResponse += chunk; - }); - res.on('end', function(){ - var spotifyPlaylist = require('./spotifyPlaylist'), - result = JSON.parse(jsonResponse); - if(result.error && result.error.status === 429){ - return resolve(); // rateLimit is exceeded - this is already handled above by spotifyHelper + var spotifySearchReq = https.request({ + hostname: "api.spotify.com", + path: "/v1/search?type=track&q=artist:"+encodeURIComponent(track.artist+' ')+'track:'+encodeURIComponent(track.title), + method: 'GET', + headers: { + 'Authorization': 'Bearer '+ accessToken, + 'Accept': 'application/json' + } + }, function(res){ + spotifyHelper.checkForRateLimit(res, 'searching track', () => sendSearchRequest(track, 0, playlistName)) + .then((rateLimitPromise) => { + if(rateLimitPromise){ + return rateLimitPromise; } + var jsonResponse = ''; + res.on('data', function(chunk){ + jsonResponse += chunk; + }); + res.on('end', function(){ + var spotifyPlaylist = require('./spotifyPlaylist'), + result = JSON.parse(jsonResponse); + + if(result.error){ + switch (result.error.status) { + case 429: + // rateLimit is exceeded - this is already handled above by spotifyHelper + return resolve(); + break; + case 401: + // accessToken might be expired + return resolve(spotifyOAuth.refresh() + .then(makeRequest().catch(error => logger.log(error, playlistName)))); + break; + default: + let message = result.error.message || 'Unknown error while searching'; + if(process.stdout.isTTY){ + process.stdout.write('\n'); + } + logger.log('Error searching for "'+ track.artist + ' - ' + track.title + '": ' + message, playlistName); + return resolve(); + } + } - if(result.error){ - let message = result.error.message || 'Unknown error while searching'; - if(process.stdout.isTTY){ - process.stdout.write('\n'); + if(!result.tracks || !result.tracks.items) { + return resolve(); } - logger.log('Error searching for "'+ track.artist + ' - ' + track.title + '": ' + message, playlistName); - return resolve(); - } - if(!result.tracks || !result.tracks.items) { - return resolve(); - } + if(process.stdout.isTTY){ + process.stdout.write('.'); // writes one dot per search Request for visualization + } - if(process.stdout.isTTY){ - process.stdout.write('.'); - } + result.tracks.items.some(function(item){ // iterate all items and break on success (return true) + let titleMatches = cleanString(item.name.toUpperCase()) === cleanString(track.title), + isAlreadyInPlaylist = spotifyPlaylist.tracks.indexOf(item.uri) > -1, + artistMatches = item.artists.some(function(artist){ + return (track.artist.indexOf(artist.name.toUpperCase()) > -1); + }); - result.tracks.items.some(function(item){ // iterate all items and break on success (return true) - var titleMatches = cleanString(item.name.toUpperCase()) === cleanString(track.title), - isAlreadyInPlaylist = spotifyPlaylist.tracks.indexOf(item.uri) > -1, - artistMatches = item.artists.some(function(artist){ - return (track.artist.indexOf(artist.name.toUpperCase()) > -1); - }); + if(!artistMatches || !titleMatches){ + return false; + } - if(!artistMatches || !titleMatches){ - return false; - } + if(isAlreadyInPlaylist){ // avoid duplicates + resolve(); + return true; // stops iterating results + } - if(isAlreadyInPlaylist){ // avoid duplicates - resolve(); - return true; // stops iterating results - } + resolve(encodeURIComponent(item.uri)); + return true; + }); - resolve(encodeURIComponent(item.uri)); - return true; + resolve(); }); - - resolve(); }); - }); - }); + }); - spotifySearchReq.end(); + spotifySearchReq.end(); + }); } }