Skip to content

Commit

Permalink
Merge pull request #8 from AnoldGH/haotian/main
Browse files Browse the repository at this point in the history
Separated token utilities and unified spotify router
  • Loading branch information
AnoldGH authored Nov 7, 2024
2 parents 11c06c6 + 8b3d23c commit bfbf8d6
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 61 deletions.
53 changes: 0 additions & 53 deletions backend/routes/spotifyAuth.js

This file was deleted.

20 changes: 19 additions & 1 deletion backend/routes/spotifyRouter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
const express = require("express");
const router = express.Router();
const axios = require("axios");
const { getAccessToken } = require("./spotifyAuth");
const { getAccessToken } = require("../utility/tokenManager");

// Get spotify access token with client credentials flow
// see https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow
router.get('/token', async (req, res) => {
try {
const accessToken = await getAccessToken()
res.json({
success: true,
access_token: accessToken
})
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve access token",
details: error.message
})
}
})

// Get track from track ID
router.get("/track/:id", async (req, res) => {
Expand Down
3 changes: 1 addition & 2 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const validator = require('validator');
const http = require('http');
const socketIo = require('socket.io');
const cron = require('node-cron');
const spotifyAuth = require('./routes/spotifyAuth.js').router
const spotifyRouter = require('./routes/spotifyRouter.js')

const app = express();
Expand All @@ -19,7 +18,7 @@ const db = require('./db.js');

// Routes
// --- Spotify access token
app.use('/api', spotifyAuth)
app.use('/api/spotify', spotifyRouter)
// spotify song routes
app.use('/songModel', spotifyRouter)

Expand Down
114 changes: 114 additions & 0 deletions backend/utility/tokenManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const express = require('express')
const query = require('querystring')
const axios = require('axios')

// Client ID and Secret
require("dotenv").config()
const CLIENT_ID = process.env.REACT_APP_SPOTIFY_CLIENT_ID
const CLIENT_SECRET = process.env.REACT_APP_SPOTIFY_CLIENT_SECRET

// Spotify Access Token
let accessToken = ""
let tokenExpires = 0 // when the token expires, milliseconds UTC
let refreshTimeout = null // timeout for the next refresh

// Max Retries to get Token
const PRELOAD_INT = 300000 // refresh PRELOAD_INT milliseconds before actual token expiration
const MAX_RETRIES = 10 // max retries when asking spotify server for an access token

// Fetch the access token
// returns (access_token, expires_in) or throws an error
const fetchToken = async() => {
console.log("Started to fetch token")

const authOptions = {
method: 'post',
url: 'https://accounts.spotify.com/api/token',
headers: {
'Authorization': 'Basic ' + (new Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64')),
'Content-Type': 'application/x-www-form-urlencoded'
},
data: query.stringify({
grant_type: 'client_credentials'
}),
json: true
}

console.log("Formed authOptions")

try {
const response = await axios(authOptions)

console.log("Fetched token: ", response.data)
return [response.data.access_token, response.data.expires_in]
} catch(error) {
console.log("Failed to retrieve token")
if (error.response) {
console.log(error.response.data)
console.log(error.response.status)
console.log(error.response.headers)
} else if (error.request) {
console.log(error.request)
} else {
console.log("Error", error.message)
}
console.log(error.config)
throw error
}
}

// Fetch and stores the token, along with its expiration
const fetchAndStoreToken = async() => {
try {
let expiresIn
[accessToken, expiresIn] = await fetchToken()
tokenExpires = Date.now() + expiresIn * 1000 // converts seconds to milliseconds UTC
scheduleTokenRefresh(expiresIn) // schedule a refresh
return accessToken
} catch(error) {
console.log("Error fetching and storing token")
throw error
}
}

// Schedule a token refresh to occur after expires_in seconds
const scheduleTokenRefresh = (expiresIn) => {
console.log(`Scheduling token-refresh in ${expiresIn * 1000 - PRELOAD_INT} milliseconds`)

// Clear existing timeout event
if (refreshTimeout) {
clearTimeout(refreshTimeout)
}

// Schedule a new timeout event
refreshTimeout = setTimeout(fetchAndStoreToken, expiresIn * 1000 - PRELOAD_INT)
}

// Get access token
// automaticlaly get a new one if token already timeout
const getAccessToken = async() => {
console.log("Getting request for Spotify access token")

// Get new token if don't have one / token expired
if (!accessToken || Date.now() >= tokenExpires) {
console.log("Retrieving new access token")

let count = 0
while (true) {
try {
console.log(`Retry: ${count} times...`)
await fetchAndStoreToken()
break
} catch(error) {
// TODO: handling error
if (++count == MAX_RETRIES) throw error
}
}
}
console.log(`Returning access token ${accessToken}`)
return accessToken
}

module.exports = {
getAccessToken
}
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function App() {
value={selectedGenre}
onChange={(e) => setSelectedGenre(e.target.value)}
>
<option value=""
<option value=""
disabled>Choose Genre
</option>
<option value="pop">Pop</option>
Expand Down

0 comments on commit bfbf8d6

Please sign in to comment.