I was building an app that helps me move tracks from my library to another playlist which requires me to go through the Spotify Authorisation workflow. I'm fairly certain the scopes are correct and I've managed to return the correct access and refresh tokens, but I cannot figure out how to only get the tracks from the user's library after the user has logged in and authorised their account for access.
I tried passing the authorisation flow into a function only to be called before the app gets the tracks but that didn't seem to work.
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = Express();
const port = 3030;
// CLIENT_SECRET stored in Config Vars
// const apiUrl = "https://accounts.spotify.com/api/token"; // Spotify Web API URL
const client_id = '467fab359c114e719ecefafd6af299e5'; // Client id
const client_secret = 'your_client_secret' // temp client secret
// const client_secret = process.env.CLIENT_SECRET;
const redirect_uri = 'http://localhost:3030/callback/'; // Callback URL
let AT, RT; // Stores access and refresh tokens
const scope = [
'user-read-private',
'user-read-email',
'user-library-read',
'playlist-read-private',
'playlist-modify-public',
'playlist-modify-private'
];
/**
* Generates a random string containing numbers and letters
* @param {number} length The length of the string
* @return {string} The generated string
*/
let generateRandomString = function (length) {
let text = '';
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
let stateKey = 'spotify_auth_state';
const authorizeSpotify = () => {
return new Promise((resolve, reject) => {
app.get('/', function (req, res) {
res.sendFile(__dirname + "/index.html");
res.redirect('/login');
});
app.use(Express.static(__dirname + '/index.html'))
.use(cors())
.use(cookieParser());
app.get('/login', function (req, res) {
let state = generateRandomString(16);
res.cookie(stateKey, state);
// app requests authorization
res.redirect('https://accounts.spotify.com/authorize?' +
querystring.stringify({
response_type: 'code',
client_id: client_id,
scope: scope,
redirect_uri: redirect_uri,
state: state
}));
});
app.get('/callback', function (req, res) {
// app requests refresh and access tokens
// after checking the state parameter
let code = req.query.code || null;
let state = req.query.state || null;
let storedState = req.cookies ? req.cookies[stateKey] : null;
console.log(state);
console.log(storedState);
if (state === null || state !== storedState) {
res.redirect('/#' +
querystring.stringify({
error: 'state_mismatch'
}));
} else {
res.clearCookie(stateKey);
let authOptions = {
url: 'https://accounts.spotify.com/api/token',
form: {
code: code,
redirect_uri: redirect_uri,
grant_type: 'authorization_code'
},
headers: {
'Authorization': 'Basic ' + (Buffer.from(client_id + ':' + client_secret).toString('base64'))
},
json: true
};
request.post(authOptions, function (error, response, body) {
if (!error && response.statusCode === 200) {
console.log(body);
AT = body.access_token;
RT = body.refresh_token;
let options = {
url: 'https://api.spotify.com/v1/me',
headers: { 'Authorization': 'Bearer ' + AT },
json: true
};
interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
previousExpires = body.expires_in;
res.send("Logged in!");
}
});
}
});
let interval;
let previousExpires = 0;
const requestToken = () => {
const authOptions = {
url: 'https://accounts.spotify.com/api/token',
headers: { 'Authorization': 'Basic ' + (Buffer.from(client_id + ':' + client_secret).toString('base64')) },
form: {
grant_type: 'refresh_token',
refresh_token: RT
},
json: true
};
request.post(authOptions, function (error, response, body) {
if (error || response.statusCode !== 200) {
console.error(error);
return;
}
AT = body.access_token;
if (body.refresh_token) {
RT = body.refresh_token;
}
console.log("Access Token refreshed!");
if (previousExpires != body.expires_in) {
clearInterval(interval);
interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
previousExpires = body.expires_in;
}
});
}
resolve({AT, RT});
});
};
// Write code for app here
// Function to get the user's library tracks
const getUserLibraryTracks = (AT) => {
return new Promise((resolve, reject) => {
const options = {
url: 'https://api.spotify.com/v1/me/tracks',
headers: { 'Authorization': 'Bearer ' + AT },
json: true
};
request.get(options, (error, response, body) => {
if (error || response.statusCode !== 200) {
console.log('Response:', body);
reject(error || new Error('Failed to get user library tracks'));
} else {
resolve(body.items.map(item => item.track));
}
});
});
};
// Function to get the tracks in a playlist
const getPlaylistTracks = (AT, playlistId) => {
return new Promise((resolve, reject) => {
const options = {
url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
headers: { 'Authorization': 'Bearer ' + AT },
json: true
};
request.get(options, (error, response, body) => {
if (error || response.statusCode !== 200) {
reject(error || new Error('Failed to get playlist tracks'));
} else {
resolve(body.items.map(item => item.track));
}
});
});
};
// Function to add tracks to a playlist
const addTracksToPlaylist = (AT, playlistId, trackIds) => {
return new Promise((resolve, reject) => {
const options = {
url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
headers: { 'Authorization': 'Bearer ' + AT },
json: true,
body: { uris: trackIds }
};
request.post(options, (error, response, body) => {
if (error || response.statusCode !== 201) {
reject(error || new Error('Failed to add tracks to playlist'));
} else {
resolve();
}
});
});
};
// Function to update the playlist with new tracks
const updatePlaylist = async (playlistId) => {
try {
const {AT, RT } = await authorizeSpotify();
const libraryTracks = await getUserLibraryTracks(AT);
const playlistTracks = await getPlaylistTracks(AT, playlistId);
const trackIdsToAdd = libraryTracks
.filter(track => !playlistTracks.some(playlistTrack => playlistTrack.id === track.id))
.map(track => track.uri);
await addTracksToPlaylist(AT, playlistId, trackIdsToAdd);
console.log('Playlist updated successfully');
} catch (error) {
console.error('Failed to update playlist:', error);
}
};
// Call the updatePlaylist function to update the playlist
updatePlaylist('your_playlist_id');
app.listen(port, () => console.log(`Listening on port: ${port}`));
Running the code, I keep receiving this message:
[nodemon] starting `node autoadd.js`
Listening on port: 3030
Response: { error: { status: 401, message: 'Invalid access token' } }
Failed to update playlist: Error: Failed to get user library tracks
at Request._callback (file:///home/nero/Projects/Autoadd/autoadd.js:195:25)
at Request.self.callback (/home/nero/Projects/Autoadd/node_modules/request/request.js:185:22)
at Request.emit (events.js:314:20)
at Request.<anonymous> (/home/nero/Projects/Autoadd/node_modules/request/request.js:1154:10)
at Request.emit (events.js:314:20)
at IncomingMessage.<anonymous> (/home/nero/Projects/Autoadd/node_modules/request/request.js:1076:12)
at Object.onceWrapper (events.js:420:28)
at IncomingMessage.emit (events.js:326:22)
at endReadableNT (_stream_readable.js:1241:12)
at processTicksAndRejections (internal/process/task_queues.js:84:21)
Of course, after running the code, I browse to the localhost:3030
address and it logs in successfully with my account. The console logging this:
{
access_token: 'BQBC1CAN2Wv3PIR1XdwTuQwgrHjQ1eCgQJqAZ0PWBNAiHGk6OKqsJFeafJEqBXBWfg1qpOvVxfEJ4SF77OHgxn9OvxS8Lg9Na0NSFlz1iWR26xztSJEq4Or-hwUKB2yE_Y-X6yPvzaScar7HDFADSQtVMxOx1Z8wq3hbi498i0bGTTnYccFTijopoSxbwfKvbfMTRxNrdUJt0z8u_w',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'AQC3bMXEM23qjQqOOXrC5Tcsvt6ijfp2umMyz466u1DCi9nNN2J9jsU0Q4ilYq2cu19xA80fhrljQSutWrFGyBzOUV3i1mytO4UBEjbbKOHuKXFXwEYV83Rxzo-7ic_-YFA',
scope: 'playlist-modify-private'
}