Spaces:
Build error
Build error
| 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}`); | |
| }); |