Userhome
+Search user
- + +
diff --git a/README.md b/README.md index fafa56e..cbf7882 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,45 @@ -# Spotify recommender +# Spotify recommender + insights ### Google SPS Taiwan Team 6 -#### Run locally -- Navigate to working directory -- In terminal, run 'node server.js' -- Application should be running on local host port 8888 +Used Javascript, Node.js/Express.js, Python, and Google Datastore to create a Spotify playlist recommender + insights web application :) + +Music taste is unique to everyone and is hard to describe in words - with this app, you can compare your music preferences with friends and enjoy some new tracks! + + + +### Video demos +**Click on the images below** to watch the Youtube demos! + +#### Desktop view +[](https://youtu.be/qDLzCbezRU8) + +#### Tablet view +[](https://youtu.be/0BhAvgtzgTw) + +#### Mobile view +[](https://youtu.be/YwTwcW77QB0) +[](https://youtu.be/YwTwcW77QB0) + +### Contributors +- Phoebe (UI) + - UI beginning stages: colour palette, basic design; more responsive data population by Stephanie + - Assisted looking into machine learning models in beginning stages +- Stephanie (Spotify API, frontend + backend) + - Spotify API calls: login authorization flow, access tokens, get user, user playlists, playlist and track objects, personalized user insights, search Spotify user + - Frontend: communicate with server using Javascript fetching, ie. send selected user playlists, populate DOM with playlists and data after calling API, responsive navigation + grid layout to display playlists, cookies, user login status, lots of rendering! + - Backend: Node.js server, handles user authorization, page rendering, GET/POST requests, and Spotify API calls + - Client-server communication: Javascript fetch, GET, POST, JSON formatting/parsing +- Jessica C (Datastore) + - Created Datastore entities for tracks (name, artists, audio_featrures, track_id, playlist_id) + - Implemented functions for inserting/retrieving to/from Datastore, ie. addTracks(), getTrackByPlaylist(), getTrackById() +- Jessica F (Recommender) + - Modified Spotify's recommendation function to fit our app purpose and functionality, ex. getTracks(), getTracksByPlaylistId(), getRecommendations() + - Ran tracks from Datastore playlists through recommendation function to output a final playlist of recommended songs starting from 'most recommended' + +### To run locally +- Git clone the repository +- You might have to delete the `node_modules` folder and run `npm install` +- Navigate to working directory/repository +- In Terminal/Powershell, run `node server.js` or `npm run devStart` (nodemon) +- Application should be running on local host port 8888! diff --git a/demo/desktop_preview.png b/demo/desktop_preview.png new file mode 100644 index 0000000..8c0eef0 Binary files /dev/null and b/demo/desktop_preview.png differ diff --git a/demo/mobile_preview.png b/demo/mobile_preview.png new file mode 100644 index 0000000..278d9ff Binary files /dev/null and b/demo/mobile_preview.png differ diff --git a/demo/mobile_preview2.png b/demo/mobile_preview2.png new file mode 100644 index 0000000..44d0cbc Binary files /dev/null and b/demo/mobile_preview2.png differ diff --git a/demo/spotify_app_flow.png b/demo/spotify_app_flow.png new file mode 100644 index 0000000..d6bcbf6 Binary files /dev/null and b/demo/spotify_app_flow.png differ diff --git a/demo/tablet_preview.png b/demo/tablet_preview.png new file mode 100644 index 0000000..f244ecc Binary files /dev/null and b/demo/tablet_preview.png differ diff --git a/public/css/style.css b/public/css/style.css index 3887249..122e766 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -9,10 +9,29 @@ --spotify-green: #1ed760; } +button { + background-color: white; +} + +#searchButton { + font-family: 'Montserrat', sans-serif; + background-color: #1ed760; + width: 10%; + border-radius: 10px; + padding: 0.35em 1.2em; + margin: 0 0.3em 0.3em 0; + box-sizing: border-box; + text-decoration: none; + color: white; +} + .content { font-family: 'Montserrat', sans-serif; margin: 50px; color: white; + max-width: 1200px; + margin: 0 auto; + padding: 20px; } .center-text { @@ -305,6 +324,141 @@ body { padding-left: 20px; } +:root { + --spotify-green: #1ed760; +} + +#tracks { +background-color: black; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 1fr 2fr; + + margin-top: 50px; +} + +#recs { +background-color: black; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 1fr; + + margin-top: 5%; + margin-left: 10%; + margin-bottom: 5%; + margin-right: 10%; +} + +#playlist-info { + grid-row: 1 / span 1; + grid-column: 1 / span 1; + + /* display: grid; + grid-template-rows: 3fr 1fr 1fr; */ + margin: 0px auto; +} + +#playlist-cover { + grid-row: 1 / span 1; + padding: 30px 10px; + max-width: 300px; + max-height: 300px; +} + +#playlist-name { + grid-row: 2 / span 1; + margin: 0px auto; + font-size: 24px; + font-family: 'Montserrat', 'Noto Sans TC', sans-serif; + font-weight: bold; + color: white; +} + +#track-info { + display: flex; + flex-direction: column; + color: white; + margin: 0px 300px 0px 10px; +} + +#rec-track-info { +display: flex; +flex-direction: column; +color: white; +margin: 0px 50px 0px 50px; +} + +.track { + width: 100%; + margin: 15px 10px; +} + +.track-name { + color: white; + text-decoration: none; + font-size: 20px; + font-family: 'Montserrat', 'Noto Sans TC', sans-serif; + font-weight: bold; +} + +.track-name:hover { + background: linear-gradient(180deg,rgba(255,255,255,0) 70%, #5C5C5C 70%); +} + +.duration { + color: #C2C2C2; + padding: 0px 20px; +} + +#buttons { + grid-row: 3 / span 1; + grid-column: 1 / span 1; +} + +.button { + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + border-radius: 24px; + opacity: 0.9; + transition: 0.3s; + font-family: inherit; +} + +.button:hover { + opacity: 1.0; +} + +#confirm { + background-color: var(--spotify-green); +} + +#back { + background-color: #5C5C5C; +} + +.icon { + grid-row: 1 / span 2; + grid-column: 1 / span 1; + max-width: 10px; + max-height: 10px; + margin-right: 10px; + + cursor: pointer; +} + +.artist { + margin: 10px 0px; + padding-left: 20px; + color: white; +} + @media(max-width: 1333px) { .grid-item { flex-basis: 33.33%; @@ -508,141 +662,6 @@ body { background-color: #1ed760; } - :root { - --spotify-green: #1ed760; -} - -#tracks { - background-color: black; - display: grid; - grid-template-rows: 1fr; - grid-template-columns: 1fr 2fr; - - margin-top: 50px; -} - -#recs { - background-color: black; - display: grid; - grid-template-rows: 1fr; - grid-template-columns: 1fr; - - margin-top: 5%; - margin-left: 10%; - margin-bottom: 5%; - margin-right: 10%; -} - -#playlist-info { - grid-row: 1 / span 1; - grid-column: 1 / span 1; - - display: grid; - grid-template-rows: 3fr 1fr 1fr; - margin: 0px auto; -} - -#playlist-cover { - grid-row: 1 / span 1; - padding: 30px 0px; - max-width: 300px; - max-height: 300px; -} - -#playlist-name { - grid-row: 2 / span 1; - margin: 0px auto; - font-size: 24px; - font-family: 'Montserrat', 'Noto Sans TC', sans-serif; - font-weight: bold; - color: white; -} - -#track-info { - display: flex; - flex-direction: column; - color: white; - margin: 0px 300px 0px 10px; -} - -#rec-track-info { - display: flex; - flex-direction: column; - color: white; - margin: 0px 50px 0px 50px; -} - -.track { - width: 100%; - margin: 15px 10px; -} - -.track-name { - color: white; - text-decoration: none; - font-size: 20px; - font-family: 'Montserrat', 'Noto Sans TC', sans-serif; - font-weight: bold; -} - -.track-name:hover { - background: linear-gradient(180deg,rgba(255,255,255,0) 70%, #5C5C5C 70%); -} - -.duration { - color: #C2C2C2; - padding: 0px 20px; -} - -#buttons { - grid-row: 3 / span 1; - grid-column: 1 / span 1; -} - -.button { - border: none; - color: white; - padding: 15px 32px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; - margin: 4px 2px; - cursor: pointer; - border-radius: 24px; - opacity: 0.9; - transition: 0.3s; - font-family: inherit; -} - -.button:hover { - opacity: 1.0; -} - -#confirm { - background-color: var(--spotify-green); -} - -#back { - background-color: #5C5C5C; -} - -.icon { - grid-row: 1 / span 2; - grid-column: 1 / span 1; - max-width: 10px; - max-height: 10px; - margin-right: 10px; - - cursor: pointer; -} - -.artist { - margin: 10px 0px; - padding-left: 20px; - color: white; -} - input[type=text] { font-family: 'Montserrat', sans-serif; width: 80%; @@ -667,3 +686,13 @@ input[type=submit] { text-align: center; transition: all 0.2s; } + +@media(max-width: 815px) { + #searchButton { + width: auto; + } + + input[type=text] { + width: 75%; + } +} \ No newline at end of file diff --git a/public/scripts/script.js b/public/scripts/script.js index f824b7a..4953f78 100644 --- a/public/scripts/script.js +++ b/public/scripts/script.js @@ -197,7 +197,7 @@ async function loadPlaylists() { // create span and set text to playlist title const title = document.createElement('span'); title.setAttribute('class', 'item-title'); - title.textContent = playlist.name; + title.textContent = playlist.name.substring(0,12); // create span and set text to playlist title const tracks = document.createElement('span'); diff --git a/server.js b/server.js index d478835..caac603 100644 --- a/server.js +++ b/server.js @@ -41,7 +41,8 @@ var token = "1" var user = "2" var display_name = "display_name" var friend = "friend" -var playlist = [] +var playlist = "playlist" +var playlist_array = [] function assign_global(access_token, user_id, user_display_name) { token = access_token @@ -176,6 +177,12 @@ app.get('/refresh_token', function (req, res) { }); // GET for logged in status +app.get('/playlistArray', function (req, res) { + res.send(200, { + "playlistArray": playlist_array + }) +}) + app.get('/loginstatus', function (req, res) { if (token == "1") res.send(200, { "loggedin": false }) else res.send(200, { "loggedin": true, "username": display_name }) @@ -210,14 +217,13 @@ app.get('/playlist-tracks', function (req, res) { getTracks(); function getTracks() { - console.log(playlist) - console.log(token) + console.log(playlist_array) var select_playlist; - if (playlist.length == 1) { - select_playlist = playlist[0] - } else { - select_playlist = playlist[1] + if (playlist_array.length == 0) { + select_playlist = playlist + } else { // else there is already 1 CONFIRMED playlist in the array + select_playlist = playlist_array[1] } var playlistOptions = { @@ -239,21 +245,33 @@ app.get('/playlist-tracks', function (req, res) { // set up endpoint for POST (receiving playlist id selection) app.post('/playlistid', (req, res, next) => { - console.log(req.body); + playlist = req.body.playlist; + console.log(`/playlistid: ${playlist}`) - playlist.push(req.body.playlist); - console.log(playlist) + // best practices to end + res.json({ + status: 'Success: current playlist ID stored on server' + }) +}) + +// set up endpoint for POST (receiving confirmed playlist id selection) +app.post('/confirmPlaylist', (req, res, next) => { + var temp_playlist = req.body + console.log(`/confirmPlaylist: ${temp_playlist}`) + + playlist_array = temp_playlist; + console.log(playlist_array[0]) + console.log(playlist_array[1]) // best practices to end res.json({ - status: 'Success: playlist ID stored on server', - playlist: req.body.playlist + status: 'Success: playlist ID ARRAY stored on server', + playlist: playlist_array }) }) // set up endpoint for POST app.post('/track', (req, res, next) => { - console.log(req.body) track = req.body console.log("TRACK: " + track.track_id); @@ -329,20 +347,27 @@ app.get('/userinsights', (req, res) => { }) }) + +// GET for logged in status +app.get('/loginstatus', function (req, res) { + if (token == "1") res.send(200, {"loggedin": false}) + else res.send(200, {"loggedin": true, "username": display_name}) +}) + // get a list of recommendations for a playlist app.get('/recommendations', async function (req, res) { var initPlaylist = []; - await getTracksByPlaylistId(playlist2, async (err, songs) => { - console.log("PLAYLIST ID 2: " + playlist2); + await getTracksByPlaylistId(playlist_array[1], async (err, songs) => { + console.log("PLAYLIST ID 2: " + playlist_array[1]); if (err) { console.log(err) } for (var i = 0; i < songs.length; i++) { initPlaylist.push(songs[i].track_id); }; console.log("FIRST: " + initPlaylist); - getTracksByPlaylistId(playlist, async (err, songs) => { - console.log("PLAYLIST ID: " + playlist); + getTracksByPlaylistId(playlist_array[0], async (err, songs) => { + console.log("PLAYLIST ID: " + playlist_array[0]); if (err) { console.log(err) } for (var i = 0; i < songs.length; i++) { diff --git a/views/index.ejs b/views/index.ejs index ed89310..64319e4 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -19,7 +19,7 @@
This Spotify Recommender is a team project from SPS Taiwan Google program.
This recommender will use machine learning algorithms to recommend songs based on a user's input playlist from Spotify. The algorithm will recommend a list of songs based on the features of the playlist