From e9e5b5fd279ffa46cbcda5317a8fc104687bc6e3 Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 24 Nov 2024 03:21:02 +0100 Subject: [PATCH 1/9] Commits --- app.js | 4 ++-- controllers/authController.js | 4 ++-- controllers/playlistController.js | 16 ++++++++++++++++ middleware/authMiddleware.js | 12 ++++++++++++ middleware/cors.js | 10 ++++++++++ routes/playlistRoutes.js | 6 ++++-- services/spotifyService.js | 18 +++++++++++++++--- utils/spotifyApi.js | 6 +++--- 8 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 middleware/authMiddleware.js create mode 100644 middleware/cors.js diff --git a/app.js b/app.js index 6427517..dcd1e7b 100644 --- a/app.js +++ b/app.js @@ -2,14 +2,14 @@ const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); -const cors = require('cors'); +const cors = require('./middleware/cors'); const authRoutes = require('./routes/authRoutes'); const playlistRoutes = require('./routes/playlistRoutes'); dotenv.config(); const app = express(); -app.use(cors()); +app.use(cors); app.use(bodyParser.json()); app.use(cookieParser()); diff --git a/controllers/authController.js b/controllers/authController.js index 2d145ad..52bd0d1 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -16,9 +16,9 @@ const callback = async (req, res) => { spotifyApi.setRefreshToken(refresh_token); res.cookie('spotify_access_token', access_token, { httpOnly: true }); - res.redirect('/playlist/'); + res.redirect('http://localhost:5173/home'); } catch (error) { - res.status(500).json({ error: error.message }); + res.redirect('http://localhost:3000/login?error=auth_failed'); } }; diff --git a/controllers/playlistController.js b/controllers/playlistController.js index 6d733fb..e455a1c 100644 --- a/controllers/playlistController.js +++ b/controllers/playlistController.js @@ -10,6 +10,21 @@ const getPlaylists = async (req, res) => { } }; +const getPlaylistSongs = async (req, res) => { + try { + const { playlistId } = req.params; + const playlist = await spotifyService.getPlaylist(playlistId); + const tracks = await spotifyService.getPlaylistTracks(playlistId); + + res.json({ + playlist: playlist, + songs: tracks.items.map(item => item.track) + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + const createPlaylist = async (req, res) => { try { const { seedTrackId } = req.body; @@ -23,5 +38,6 @@ const createPlaylist = async (req, res) => { module.exports = { getPlaylists, + getPlaylistSongs, createPlaylist, }; diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..ff5880c --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,12 @@ +const auth = (req, res, next) => { + const token = req.cookies.spotify_access_token; + + if (!token) { + return res.status(401).json({ error: 'Authentication required' }); + } + + req.spotifyToken = token; + next(); + }; + + module.exports = auth; \ No newline at end of file diff --git a/middleware/cors.js b/middleware/cors.js new file mode 100644 index 0000000..dfee3f6 --- /dev/null +++ b/middleware/cors.js @@ -0,0 +1,10 @@ +const cors = require('cors'); + +const corsOptions = { + origin: 'http://localhost:5173', + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] +}; + +module.exports = cors(corsOptions); \ No newline at end of file diff --git a/routes/playlistRoutes.js b/routes/playlistRoutes.js index b3c436a..feaf67f 100644 --- a/routes/playlistRoutes.js +++ b/routes/playlistRoutes.js @@ -1,9 +1,11 @@ const express = require('express'); const playlistController = require('../controllers/playlistController'); +const auth = require('../middleware/authMiddleware'); const router = express.Router(); -router.get('/', playlistController.getPlaylists); -router.post('/create', playlistController.createPlaylist); +router.get('/', auth, playlistController.getPlaylists); +router.get('/:playlistId', playlistController.getPlaylistSongs); +router.post('/create', auth, playlistController.createPlaylist); module.exports = router; diff --git a/services/spotifyService.js b/services/spotifyService.js index 175e561..c65383b 100644 --- a/services/spotifyService.js +++ b/services/spotifyService.js @@ -9,8 +9,18 @@ const setAccessToken = (accessToken) => { spotifyApi.setAccessToken(accessToken); }; -const getUserPlaylists = async (offset = 0, limit = 20) => { - const response = await spotifyApi.getUserPlaylists({ offset, limit }); +const getUserPlaylists = async (offset = 0) => { + const response = await spotifyApi.getUserPlaylists({ offset }); + return response.body; +}; + +const getPlaylist = async (playlistId) => { + const response = await spotifyApi.getPlaylist(playlistId); + return response.body; +}; + +const getPlaylistTracks = async (playlistId) => { + const response = await spotifyApi.getPlaylistTracks(playlistId); return response.body; }; @@ -41,7 +51,7 @@ const createPlaylistFromSeedTrack = async (userId, seedTrackId) => { const recommendations = await getTrackRecommendations(seedTrackId); const trackUris = recommendations.map(track => track.uri); - const playlistName = 'Music for You'; + const playlistName = 'Groovz'; const description = `Similar songs to ${seedTrackName}`; const playlistId = await createPlaylist(userId, playlistName, description, trackUris); @@ -52,6 +62,8 @@ module.exports = { getAuthUrl, setAccessToken, getUserPlaylists, + getPlaylist, + getPlaylistTracks, getTrackRecommendations, createPlaylistFromSeedTrack, }; diff --git a/utils/spotifyApi.js b/utils/spotifyApi.js index 4ac3a5c..50c911a 100644 --- a/utils/spotifyApi.js +++ b/utils/spotifyApi.js @@ -1,9 +1,9 @@ const SpotifyWebApi = require('spotify-web-api-node'); const spotifyApi = new SpotifyWebApi({ - clientId: process.env.SPOTIFY_CLIENT_ID, - clientSecret: process.env.SPOTIFY_CLIENT_SECRET, - redirectUri: process.env.SPOTIFY_REDIRECT_URI, + clientId: '5e3eef3570b74a37af3438268b820e32', + clientSecret: 'ecda63e51490449d9c94b26f9fd9571a', + redirectUri: 'http://localhost:4000/auth/callback', }); module.exports = spotifyApi; \ No newline at end of file From 7a45f40c35ed52bb9deaab5cc7caf4924a3cc8f9 Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 24 Nov 2024 05:33:25 +0100 Subject: [PATCH 2/9] liked songs can now be used --- controllers/playlistController.js | 13 +++++ images/1.jpg | Bin 0 -> 9358 bytes images/2.jpg | Bin 0 -> 8584 bytes images/3.jpg | Bin 0 -> 8678 bytes images/4.jpg | Bin 0 -> 8746 bytes routes/playlistRoutes.js | 7 ++- services/spotifyService.js | 82 ++++++++++++++++++++++++++++-- 7 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 images/1.jpg create mode 100644 images/2.jpg create mode 100644 images/3.jpg create mode 100644 images/4.jpg diff --git a/controllers/playlistController.js b/controllers/playlistController.js index e455a1c..6bae9ac 100644 --- a/controllers/playlistController.js +++ b/controllers/playlistController.js @@ -10,6 +10,18 @@ const getPlaylists = async (req, res) => { } }; +const getLikedSongs = async (req, res) => { + try { + const likedSongs = await spotifyService.getLikedSongs(); + res.json(likedSongs); + } catch (error) { + console.error('Error in getLikedSongs controller:', error); + res.status(error.statusCode || 500).json({ + error: error.message || 'An error occurred while fetching liked songs' + }); + } +}; + const getPlaylistSongs = async (req, res) => { try { const { playlistId } = req.params; @@ -38,6 +50,7 @@ const createPlaylist = async (req, res) => { module.exports = { getPlaylists, + getLikedSongs, getPlaylistSongs, createPlaylist, }; diff --git a/images/1.jpg b/images/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..461df1abb4d7a9fdfc93453fc52878870c1858db GIT binary patch literal 9358 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 GNB#}e=+h4X literal 0 HcmV?d00001 diff --git a/images/4.jpg b/images/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8d316bb51f52919ad4ae03ab010f482d8c11472 GIT binary patch literal 8746 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 literal 0 HcmV?d00001 diff --git a/routes/playlistRoutes.js b/routes/playlistRoutes.js index feaf67f..e0fc5f3 100644 --- a/routes/playlistRoutes.js +++ b/routes/playlistRoutes.js @@ -1,11 +1,16 @@ const express = require('express'); +const spotifyApi = require('../utils/spotifyApi'); const playlistController = require('../controllers/playlistController'); const auth = require('../middleware/authMiddleware'); const router = express.Router(); -router.get('/', auth, playlistController.getPlaylists); +router.get('/liked-songs', playlistController.getLikedSongs); +router.get('/', playlistController.getPlaylists); router.get('/:playlistId', playlistController.getPlaylistSongs); router.post('/create', auth, playlistController.createPlaylist); + + + module.exports = router; diff --git a/services/spotifyService.js b/services/spotifyService.js index c65383b..4075d7d 100644 --- a/services/spotifyService.js +++ b/services/spotifyService.js @@ -1,5 +1,21 @@ const spotifyApi = require('../utils/spotifyApi'); +const path = require('path'); +const playlistImages = [ + path.join(__dirname, '../images/1.jpg'), + path.join(__dirname, '../images/2.jpg'), + path.join(__dirname, '../images/3.jpg'), + path.join(__dirname, '../images/4.jpg') +]; + + +const convertImageToBase64 = async (imagePath) => { + const fs = require('fs').promises; + const buffer = await fs.readFile(imagePath); + return buffer.toString('base64'); +}; + + const getAuthUrl = () => { const scopes = ['user-library-read', 'playlist-modify-private', 'playlist-modify-public']; return spotifyApi.createAuthorizeURL(scopes, 'state-key'); @@ -10,10 +26,33 @@ const setAccessToken = (accessToken) => { }; const getUserPlaylists = async (offset = 0) => { - const response = await spotifyApi.getUserPlaylists({ offset }); - return response.body; + const playlistResponse = await spotifyApi.getUserPlaylists({ offset }); + + const likedSongsResponse = await spotifyApi.getMySavedTracks(); + + return { + playlists: playlistResponse.body, + likedSongs: likedSongsResponse.body + }; }; +const getLikedSongs = async () => { + try { + const response = await spotifyApi.getMySavedTracks({ + limit: 50, + offset: 0 + }); + return response.body; + } catch (error) { + console.error('Error fetching liked songs:', error); + if (error.statusCode) { + throw new Error(`Spotify API error: ${error.statusCode} - ${error.message}`); + } + throw error; + } +}; + + const getPlaylist = async (playlistId) => { const response = await spotifyApi.getPlaylist(playlistId); return response.body; @@ -25,11 +64,39 @@ const getPlaylistTracks = async (playlistId) => { }; const getTrackRecommendations = async (seedTrackId) => { + const seedTrackFeatures = await spotifyApi.getAudioFeaturesForTrack(seedTrackId); + const response = await spotifyApi.getRecommendations({ seed_tracks: [seedTrackId], - limit: 10, + limit: 100, + target_instrumentalness: seedTrackFeatures.body.instrumentalness, + target_acousticness: seedTrackFeatures.body.acousticness, + min_instrumentalness: Math.max(0, seedTrackFeatures.body.instrumentalness - 0.2), + max_instrumentalness: Math.min(1, seedTrackFeatures.body.instrumentalness + 0.2), + target_key: seedTrackFeatures.body.key, + target_mode: seedTrackFeatures.body.mode, + target_time_signature: seedTrackFeatures.body.time_signature, + min_popularity: 20, }); - return response.body.tracks; + + const tracks = response.body.tracks; + let totalDurationMs = 0; + const TARGET_DURATION_MS = 60 * 60 * 1000; + const MARGIN_MS = 2 * 60 * 1000; + const selectedTracks = []; + + for (const track of tracks) { + if (totalDurationMs + track.duration_ms <= TARGET_DURATION_MS + MARGIN_MS) { + selectedTracks.push(track); + totalDurationMs += track.duration_ms; + } + + if (totalDurationMs >= TARGET_DURATION_MS - MARGIN_MS) { + break; + } + } + + return selectedTracks; }; const createPlaylist = async (userId, name, description, trackUris) => { @@ -41,6 +108,12 @@ const createPlaylist = async (userId, name, description, trackUris) => { const playlistId = playlistResponse.body.id; await spotifyApi.addTracksToPlaylist(playlistId, trackUris); + const randomImageIndex = Math.floor(Math.random() * playlistImages.length); + const imagePath = playlistImages[randomImageIndex]; + + const imageData = await convertImageToBase64(imagePath); + await spotifyApi.uploadCustomPlaylistCoverImage(playlistId, imageData); + return playlistId; }; @@ -62,6 +135,7 @@ module.exports = { getAuthUrl, setAccessToken, getUserPlaylists, + getLikedSongs, getPlaylist, getPlaylistTracks, getTrackRecommendations, From 5c5acec047494a6e4a2916fe35681043b65b38d7 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 01:15:27 +0100 Subject: [PATCH 3/9] commits --- controllers/authController.js | 3 +++ controllers/playlistController.js | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/controllers/authController.js b/controllers/authController.js index 52bd0d1..51c5fc5 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -12,6 +12,9 @@ const callback = async (req, res) => { const data = await spotifyApi.authorizationCodeGrant(code); const { access_token, refresh_token } = data.body; + console.log('Granted Scopes:', data.body.scope); + + spotifyApi.setAccessToken(access_token); spotifyApi.setRefreshToken(refresh_token); diff --git a/controllers/playlistController.js b/controllers/playlistController.js index 6bae9ac..692bb08 100644 --- a/controllers/playlistController.js +++ b/controllers/playlistController.js @@ -39,15 +39,18 @@ const getPlaylistSongs = async (req, res) => { const createPlaylist = async (req, res) => { try { + console.log('Create playlist request body:', req.body); + console.log('User ID:', req.query.userId); const { seedTrackId } = req.body; - const userId = req.query.userId; - const playlistId = await spotifyService.createPlaylistFromSeedTrack(userId, seedTrackId); + const playlistId = await spotifyService.createPlaylistFromSeedTrack(req.query.userId, seedTrackId); res.json({ message: 'Playlist created successfully', playlistId }); } catch (error) { + console.log('Error in createPlaylist:', error); res.status(500).json({ error: error.message }); } }; + module.exports = { getPlaylists, getLikedSongs, From a8bb00eae971111a689b340e64af71c52e3c763e Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 01:15:38 +0100 Subject: [PATCH 4/9] urls --- controllers/authController.js | 4 ++-- middleware/cors.js | 2 +- utils/spotifyApi.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/authController.js b/controllers/authController.js index 51c5fc5..f7aa339 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -19,9 +19,9 @@ const callback = async (req, res) => { spotifyApi.setRefreshToken(refresh_token); res.cookie('spotify_access_token', access_token, { httpOnly: true }); - res.redirect('http://localhost:5173/home'); + res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com//home'); } catch (error) { - res.redirect('http://localhost:3000/login?error=auth_failed'); + res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com//login?error=auth_failed'); } }; diff --git a/middleware/cors.js b/middleware/cors.js index dfee3f6..0bafd09 100644 --- a/middleware/cors.js +++ b/middleware/cors.js @@ -1,7 +1,7 @@ const cors = require('cors'); const corsOptions = { - origin: 'http://localhost:5173', + origin: 'http://localhost:5173' || 'https://main.d1n7z7zw3v28b1.amplifyapp.com/', credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] diff --git a/utils/spotifyApi.js b/utils/spotifyApi.js index 50c911a..81542b7 100644 --- a/utils/spotifyApi.js +++ b/utils/spotifyApi.js @@ -3,7 +3,7 @@ const SpotifyWebApi = require('spotify-web-api-node'); const spotifyApi = new SpotifyWebApi({ clientId: '5e3eef3570b74a37af3438268b820e32', clientSecret: 'ecda63e51490449d9c94b26f9fd9571a', - redirectUri: 'http://localhost:4000/auth/callback', + redirectUri: 'https://groovz-backend-js.onrender.com/auth/callback', }); module.exports = spotifyApi; \ No newline at end of file From feca88b0538ad5621bc471ec6d412799012aa4c0 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 01:24:27 +0100 Subject: [PATCH 5/9] hmm --- utils/spotifyApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/spotifyApi.js b/utils/spotifyApi.js index 81542b7..50c911a 100644 --- a/utils/spotifyApi.js +++ b/utils/spotifyApi.js @@ -3,7 +3,7 @@ const SpotifyWebApi = require('spotify-web-api-node'); const spotifyApi = new SpotifyWebApi({ clientId: '5e3eef3570b74a37af3438268b820e32', clientSecret: 'ecda63e51490449d9c94b26f9fd9571a', - redirectUri: 'https://groovz-backend-js.onrender.com/auth/callback', + redirectUri: 'http://localhost:4000/auth/callback', }); module.exports = spotifyApi; \ No newline at end of file From 77ac9cbcba3284942c0458c1ceb787926feaf491 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 01:24:36 +0100 Subject: [PATCH 6/9] commits --- utils/spotifyApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/spotifyApi.js b/utils/spotifyApi.js index 50c911a..81542b7 100644 --- a/utils/spotifyApi.js +++ b/utils/spotifyApi.js @@ -3,7 +3,7 @@ const SpotifyWebApi = require('spotify-web-api-node'); const spotifyApi = new SpotifyWebApi({ clientId: '5e3eef3570b74a37af3438268b820e32', clientSecret: 'ecda63e51490449d9c94b26f9fd9571a', - redirectUri: 'http://localhost:4000/auth/callback', + redirectUri: 'https://groovz-backend-js.onrender.com/auth/callback', }); module.exports = spotifyApi; \ No newline at end of file From 99510ff31ae54a441e29988754c340ebc2627f46 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 02:08:47 +0100 Subject: [PATCH 7/9] commit --- controllers/authController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/authController.js b/controllers/authController.js index f7aa339..a727bb5 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -19,9 +19,9 @@ const callback = async (req, res) => { spotifyApi.setRefreshToken(refresh_token); res.cookie('spotify_access_token', access_token, { httpOnly: true }); - res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com//home'); + res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com/home'); } catch (error) { - res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com//login?error=auth_failed'); + res.redirect('https://main.d1n7z7zw3v28b1.amplifyapp.com/login?error=auth_failed'); } }; From 96fe49d3accbc4416d10bb54dc4f5fbbd3fc468f Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 02:12:14 +0100 Subject: [PATCH 8/9] cors --- middleware/cors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/cors.js b/middleware/cors.js index 0bafd09..0faaa91 100644 --- a/middleware/cors.js +++ b/middleware/cors.js @@ -1,7 +1,7 @@ const cors = require('cors'); const corsOptions = { - origin: 'http://localhost:5173' || 'https://main.d1n7z7zw3v28b1.amplifyapp.com/', + origin: ['http://localhost:5173', 'https://main.d1n7z7zw3v28b1.amplifyapp.com/'], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] From 35bf4f8349bd8997c09772e9ed72afb4bcf6d9ac Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 30 Nov 2024 02:14:30 +0100 Subject: [PATCH 9/9] . --- middleware/cors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/cors.js b/middleware/cors.js index 0faaa91..681b690 100644 --- a/middleware/cors.js +++ b/middleware/cors.js @@ -1,7 +1,7 @@ const cors = require('cors'); const corsOptions = { - origin: ['http://localhost:5173', 'https://main.d1n7z7zw3v28b1.amplifyapp.com/'], + origin: ['http://localhost:5173', 'https://main.d1n7z7zw3v28b1.amplifyapp.com'], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization']