spotify-web-app / server.js
findEthics
Fix OAuth redirect URI mismatch and add debugging
b96a724
Raw
History Blame Contribute Delete
9.51 kB
const express = require('express');
const path = require('path');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 7860;
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
// Determine if we're running locally or on Hugging Face Spaces
const isHuggingFaceSpaces = process.env.SPACE_ID && process.env.SPACE_AUTHOR_NAME && process.env.SPACE_REPO_NAME;
const isLocalDevelopment = process.env.NODE_ENV === 'development' || !isHuggingFaceSpaces;
let REDIRECT_URI;
if (isHuggingFaceSpaces) {
REDIRECT_URI = `https://${process.env.SPACE_AUTHOR_NAME}-${process.env.SPACE_REPO_NAME}.hf.space/callback`;
} else {
// Use 127.0.0.1 for local development as it's considered more secure by Spotify
REDIRECT_URI = `http://127.0.0.1:7860/callback`;
}
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
function generateRandomString(length) {
return crypto.randomBytes(60).toString('hex').slice(0, length);
}
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/auth/login', (req, res) => {
console.log('=== OAuth Login Initiated ===');
console.log('Environment:', isHuggingFaceSpaces ? 'Hugging Face Spaces' : 'Local Development');
console.log('Redirect URI:', REDIRECT_URI);
console.log('Client ID:', CLIENT_ID ? 'Set' : 'Missing');
console.log('Client Secret:', CLIENT_SECRET ? 'Set' : 'Missing');
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error('Missing Spotify credentials!');
return res.status(500).send('Server configuration error: Missing Spotify credentials');
}
const state = generateRandomString(16);
const scope = 'streaming user-read-email user-read-private playlist-read-private playlist-read-collaborative user-read-playback-state user-modify-playback-state user-read-currently-playing';
const authURL = 'https://accounts.spotify.com/authorize?' +
new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
scope: scope,
redirect_uri: REDIRECT_URI,
state: state
});
console.log('Redirecting to:', authURL);
res.redirect(authURL);
});
// Store tokens temporarily (in production, use proper session management)
let userTokens = {};
app.get('/callback', async (req, res) => {
console.log('=== OAuth Callback Received ===');
console.log('Query params:', req.query);
const code = req.query.code || null;
const state = req.query.state || null;
const error = req.query.error || null;
if (error) {
console.error('OAuth error:', error);
res.redirect(`/#error=${error}`);
return;
}
if (state === null) {
console.error('State mismatch - no state parameter');
res.redirect('/#error=state_mismatch');
return;
}
if (!code) {
console.error('No authorization code received');
res.redirect('/#error=no_code');
return;
}
try {
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64')
},
body: new URLSearchParams({
code: code,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
const data = await response.json();
if (response.ok) {
// Store tokens with a simple session ID
const sessionId = generateRandomString(16);
userTokens[sessionId] = {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + (data.expires_in * 1000)
};
// Redirect to playlist page with session ID
res.redirect(`/playlist.html?session=${sessionId}`);
} else {
res.redirect('/#error=invalid_token');
}
} catch (error) {
res.redirect('/#error=server_error');
}
});
// API endpoint to get user's access token
app.get('/api/token/:sessionId', (req, res) => {
const sessionId = req.params.sessionId;
const tokens = userTokens[sessionId];
if (!tokens) {
return res.status(404).json({ error: 'Session not found' });
}
// Check if token is expired
if (Date.now() > tokens.expires_at) {
return res.status(401).json({ error: 'Token expired' });
}
res.json({ access_token: tokens.access_token });
});
// Helper function to get all user playlists with pagination
async function getAllUserPlaylists(accessToken) {
const allPlaylists = [];
const maxPages = 20; // Search up to 1000 playlists (50 * 20)
for (let page = 0; page < maxPages; page++) {
const offset = page * 50;
const playlistsResponse = await fetch(`https://api.spotify.com/v1/me/playlists?limit=50&offset=${offset}`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!playlistsResponse.ok) {
throw new Error(`Failed to fetch playlists: ${playlistsResponse.status}`);
}
const playlistsData = await playlistsResponse.json();
// Add all playlists from this page
allPlaylists.push(...playlistsData.items);
// If we've reached the end of playlists, break
if (playlistsData.items.length < 50 || !playlistsData.next) {
break;
}
}
return allPlaylists;
}
// API endpoint to get all user playlists
app.get('/api/playlists/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
const tokens = userTokens[sessionId];
if (!tokens) {
return res.status(404).json({ error: 'Session not found' });
}
if (Date.now() > tokens.expires_at) {
return res.status(401).json({ error: 'Token expired' });
}
try {
const allPlaylists = await getAllUserPlaylists(tokens.access_token);
res.json({
playlists: allPlaylists,
total: allPlaylists.length
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
// API endpoint to get tracks for a specific playlist
app.get('/api/playlist/:playlistId/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
const playlistId = req.params.playlistId;
const tokens = userTokens[sessionId];
if (!tokens) {
return res.status(404).json({ error: 'Session not found' });
}
try {
// Get playlist details
const playlistResponse = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}`, {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
if (!playlistResponse.ok) {
return res.status(playlistResponse.status).json({ error: 'Failed to fetch playlist' });
}
const playlistData = await playlistResponse.json();
// Get playlist tracks
const tracksResponse = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
if (!tracksResponse.ok) {
return res.status(tracksResponse.status).json({ error: 'Failed to fetch tracks' });
}
const tracksData = await tracksResponse.json();
res.json({
playlist: playlistData,
tracks: tracksData.items
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
// API endpoint to get available devices
app.get('/api/devices/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
const tokens = userTokens[sessionId];
if (!tokens) {
return res.status(404).json({ error: 'Session not found' });
}
if (Date.now() > tokens.expires_at) {
return res.status(401).json({ error: 'Token expired' });
}
try {
const response = await fetch('https://api.spotify.com/v1/me/player/devices', {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
if (!response.ok) {
return res.status(response.status).json({ error: 'Failed to fetch devices' });
}
const data = await response.json();
res.json(data);
} catch (error) {
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
// API endpoint to transfer playback to a device
app.put('/api/transfer/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
const { device_id, play } = req.body;
const tokens = userTokens[sessionId];
if (!tokens) {
return res.status(404).json({ error: 'Session not found' });
}
if (Date.now() > tokens.expires_at) {
return res.status(401).json({ error: 'Token expired' });
}
try {
const response = await fetch('https://api.spotify.com/v1/me/player', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${tokens.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
device_ids: [device_id],
play: play !== undefined ? play : false
})
});
if (response.status === 204) {
res.json({ success: true });
} else {
const errorData = await response.json();
res.status(response.status).json(errorData);
}
} catch (error) {
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});