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}`); });