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'
}
My understanding is you want to get my tracks and adding into the playlist except for duplicated(already existing) songs.
Using express
for REST server, axios
for Spotify REST POST call.
Using Authorization Code Flow
for getting access token
GET /playlists/{playlist_id}/tracks
GET /me/tracks
POST /playlists/{playlist_id}/tracks
Save as update_songs.js
const express = require("express")
const axios = require('axios')
const cors = require("cors");
const app = express()
app.use(cors())
CLIENT_ID = "<your client ID>"
CLIENT_SECRET = "<your client secret>"
PORT = 3030 // it is located in Spotify dashboard's Redirect URIs, my port is 3000
REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is 'http://localhost:3000/callback'
PLAYLIST_ID = 'your playlist ID'
SCOPE = [
'user-read-private',
'user-read-email',
'user-library-read',
'playlist-read-private',
'playlist-modify-public',
'playlist-modify-private'
]
const getToken = async (code) => {
try {
const resp = await axios.post(
url = 'https://accounts.spotify.com/api/token',
data = new URLSearchParams({
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'code': code
}),
config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
})
return Promise.resolve(resp.data.access_token);
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const addSongs = async (playlist_id, tracks, token) => {
try {
const uris = []
for(const track of tracks) {
if (track.new) {
uris.push(track.uri)
}
}
const chunkSize = 100;
for (let i = 0; i < uris.length; i += chunkSize) {
const sub_uris = uris.slice(i, i + chunkSize);
const resp = await axios.post(
url = `https://api.spotify.com/v1/playlists/${playlist_id}/tracks`,
data = {
'uris': sub_uris
},
config = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
}
})
}
return Promise.resolve('OK');
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const getPlaylistTracks = async (playlist, token) => {
try {
let next = 1
const tracks = []
url = `https://api.spotify.com/v1/playlists/${playlist}`
while (next != null) {
const resp = await axios.get(
url,
config = {
headers: {
'Accept-Encoding': 'application/json',
'Authorization': `Bearer ${token}`,
}
}
)
items = []
if (resp.data.items) {
items = resp.data.items
} else if (resp.data.tracks.items) {
items = resp.data.tracks.items
}
for(const item of items) {
if (item.track?.name != null) {
tracks.push({
name: item.track.name,
external_urls: item.track.external_urls.spotify,
uri: item.track.uri,
new: false
})
}
}
if (resp.data.items) {
url = resp.data.next
} else if (resp.data.tracks.items) {
url = resp.data.tracks.next
} else {
break
}
next = url
}
return Promise.resolve(tracks)
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const update_track = (arr, track) => {
const { length } = arr;
const id = length + 1;
const found = arr.some(el => el.external_urls === track.external_urls);
if (!found) {
arr.push({ name : track.name, external_urls: track.external_urls, uri: track.uri, new: true })
};
return arr;
}
const updatePlaylistTracks = async (my_tracks, previous_tracks, token) => {
try {
new_tracks = previous_tracks.map(a => Object.assign({}, a));
// update new playlist with my_tracks and previous_tracks
for(const track of my_tracks) {
new_tracks = update_track(new_tracks, track)
}
return Promise.resolve(new_tracks)
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const getMyTracks = async (token) => {
try {
let offset = 0
let next = 1
const limit = 50;
const tracks = [];
while (next != null) {
const resp = await axios.get(
url = `https://api.spotify.com/v1/me/tracks/?limit=${limit}&offset=${offset}`,
config = {
headers: {
'Accept-Encoding': 'application/json',
'Authorization': `Bearer ${token}`,
}
}
);
for(const item of resp.data.items) {
if(item.track?.name != null) {
tracks.push({
name: item.track.name,
external_urls: item.track.external_urls.spotify,
uri: item.track.uri,
new: false,
added_at: item.added_at
})
}
}
offset = offset + limit
next = resp.data.next
}
return Promise.resolve(tracks)
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
app.get("/login", (request, response) => {
const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&client_id=${CLIENT_ID}&scope=${SCOPE}&state=123456&redirect_uri=${REDIRECT_URI}&prompt=consent`
response.redirect(redirect_url);
})
app.get("/callback", async (request, response) => {
const code = request.query["code"]
getToken(code)
.then(access_token => {
getMyTracks(access_token)
.then(my_tracks => {
getPlaylistTracks(PLAY_LIST_ID, access_token)
.then(previous_tracks => {
updatePlaylistTracks(my_tracks, previous_tracks, access_token)
.then(new_tracks => {
addSongs(PLAY_LIST_ID, new_tracks, access_token)
.then(OK => {
return response.send({
'my tracks Total:': my_tracks.length,
'my tracks': my_tracks,
'previous playlist Total:': previous_tracks.length,
'previous playlist': previous_tracks,
'new playlist Total:': new_tracks.length,
'new playlist': new_tracks,
'add song result': OK });
})
})
})
})
})
.catch(error => {
console.log(error.message);
})
})
app.listen(PORT, () => {
console.log(`Listening on :${PORT}`)
})
npm install express axios cors
From terminal
node update_songs.js
By browser
http://locahost:3030/login
From Browser
My library list songs: total 837 songs
Previous Playlist songs: total 771 songs
Before adding songs (previous playlist songs)
After adding songs: total 1,591 songs
For checking if any new songs are in my library, can see when it added_at
You need to add one line of code into getMyTracks()
tracks.push({
name: item.track.name,
external_urls: item.track.external_urls.spotify,
uri: item.track.uri,
new: false,
added_at: item.added_at
})
Step By Step, this code can get my library songs.
const express = require("express")
const axios = require('axios')
const cors = require("cors");
const app = express()
app.use(cors())
CLIENT_ID = "<your client ID>"
CLIENT_SECRET = "<your client secret>"
PORT = 3030 // it is located in Spotify dashboard's Redirect URIs
REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is 'http://localhost:3000/callback'
SCOPE = [
'user-read-private',
'user-read-email',
'user-library-read',
'playlist-read-private',
'playlist-modify-public',
'playlist-modify-private'
]
app.get("/login", (request, response) => {
const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&client_id=${CLIENT_ID}&scope=${SCOPE}&state=123456&redirect_uri=${REDIRECT_URI}&prompt=consent`
response.redirect(redirect_url);
})
const getToken = async (code) => {
try {
const resp = await axios.post(
'https://accounts.spotify.com/api/token',
new URLSearchParams({
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'code': code
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
})
return Promise.resolve(resp.data.access_token);
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const getMyTracks = async (token) => {
try {
let offset = 0
let next = 1
const limit = 50;
const tracks = [];
while (next != null) {
const resp = await axios.get(
url = `https://api.spotify.com/v1/me/tracks/?limit=${limit}&offset=${offset}`,
config = {
headers: {
'Accept-Encoding': 'application/json',
'Authorization': `Bearer ${token}`,
}
}
);
for(const item of resp.data.items) {
if(item.track?.name != null) {
tracks.push({
name: item.track.name,
external_urls: item.track.external_urls.spotify,
uri: item.track.uri,
new: false,
added_at: item.added_at
})
}
}
offset = offset + limit
next = resp.data.next
}
return Promise.resolve(tracks)
} catch (err) {
console.error(err)
return Promise.reject(err)
}
};
app.get("/callback", async (request, response) => {
const code = request.query["code"]
getToken(code)
.then(access_token => {
getMyTracks(access_token)
.then(my_tracks => {
return response.send({
'total:' : my_tracks.length,
'my tracks': my_tracks
});
})
})
.catch(error => {
console.log(error.message);
})
})
app.listen(PORT, () => {
console.log(`Listening on :${PORT}`)
})
Step By Step, this code can get playlist songs.
const express = require("express")
const axios = require('axios')
const cors = require("cors");
const app = express()
app.use(cors())
CLIENT_ID = "<your client ID>"
CLIENT_SECRET = "<your client secret>"
PORT = 3030 // it is located in Spotify dashboard's Redirect URIs
REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is 'http://localhost:3000/callback'
PLAYLIST_ID = 'your playlist ID'
SCOPE = [
'user-read-private',
'user-read-email',
'user-library-read',
'playlist-read-private',
'playlist-modify-public',
'playlist-modify-private'
]
app.get("/login", (request, response) => {
const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&client_id=${CLIENT_ID}&scope=${SCOPE}&state=123456&redirect_uri=${REDIRECT_URI}&prompt=consent`
response.redirect(redirect_url);
})
const getToken = async (code) => {
try {
const resp = await axios.post(
'https://accounts.spotify.com/api/token',
new URLSearchParams({
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'code': code
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
})
return Promise.resolve(resp.data.access_token);
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
const getPlaylistTracks = async (playlist, token) => {
try {
let next = 1
const tracks = []
url = `https://api.spotify.com/v1/playlists/${playlist}`
while (next != null) {
const resp = await axios.get(
url,
config = {
headers: {
'Accept-Encoding': 'application/json',
'Authorization': `Bearer ${token}`,
}
}
)
items = []
if (resp.data.items) {
items = resp.data.items
} else if (resp.data.tracks.items) {
items = resp.data.tracks.items
}
for(const item of items) {
if (item.track?.name != null) {
tracks.push({
name: item.track.name,
external_urls: item.track.external_urls.spotify,
uri: item.track.uri,
new: false
})
}
}
if (resp.data.items) {
url = resp.data.next
} else if (resp.data.tracks.items) {
url = resp.data.tracks.next
} else {
break
}
next = url
}
return Promise.resolve(tracks)
} catch (err) {
console.error(err)
return Promise.reject(err)
}
}
app.get("/callback", async (request, response) => {
const code = request.query["code"]
getToken(code)
.then(access_token => {
getPlaylistTracks(PLAYLIST_ID, access_token)
.then(tracks => {
return response.send({
'total:' : tracks.length,
'playlist tracks': tracks
});
})
})
.catch(error => {
console.log(error.message);
})
})
app.listen(PORT, () => {
console.log(`Listening on :${PORT}`)
})