├── LICENSE.txt ├── README.md ├── app.js ├── bin └── www ├── controllers └── SpotifyController.js ├── package.json ├── public ├── favicon.ico └── stylesheets │ └── style.css ├── routes └── index.js └── views ├── index.ejs └── visualisation.ejs /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [Nadia Campo Woytuk, Stefan Aleksik] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [musicScapes](https://musicscapes.herokuapp.com/) 3 | ------------------------------------------------- 4 | 5 | 6 | 7 | Hack for Spotify's devX Stockholm 2017 8 | 9 | "musicScapes" generates a minimalistic landscape based on your recent Spotify activity. 10 | 11 | The mountains and peaceful colors are generated from the audio features of the last 50 songs you've listened to. The landscape changes depending on if you listen to happy or sad songs, energetic or calm ones, if you've been a recent active listener and other track features. 12 | 13 | Listen to a couple new songs and see how it reflects on your landscape. 14 | 15 | This app was originally created as part of Spotify's devX hackathon in Stockholm, 2017. 16 | 17 | The app has been developed by: 18 | Nadia Campo Woytuk - front end 19 | Stefan Aleksik - back end 20 | 21 | Using [p5.js](https://p5js.org/), [randomColor](https://randomcolor.llllll.li/) 22 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var expressSession = require('express-session'); 8 | 9 | var index = require('./routes/index'); 10 | var users = require('./routes/users'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'ejs'); 17 | 18 | // uncomment after placing your favicon in /public 19 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ 23 | extended: false 24 | })); 25 | app.use(cookieParser()); 26 | app.use(express.static(path.join(__dirname, 'public'))); 27 | app.use(expressSession({ 28 | secret: 'secret' 29 | })); 30 | 31 | app.use('/', index); 32 | app.use('/users', users); 33 | 34 | // catch 404 and forward to error handler 35 | app.use(function(req, res, next) { 36 | var err = new Error('Not Found'); 37 | err.status = 404; 38 | next(err); 39 | }); 40 | 41 | // error handler 42 | app.use(function(err, req, res, next) { 43 | // set locals, only providing error in development 44 | res.locals.message = err.message; 45 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 46 | 47 | // render the error page 48 | res.status(err.status || 500); 49 | res.render('error'); 50 | }); 51 | 52 | module.exports = app; 53 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('three:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /controllers/SpotifyController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Stefan Aleksik on 05.1.2018. 3 | */ 4 | var SpotifyWebApi = require('spotify-web-api-node'); 5 | var querystring = require('querystring'); 6 | 7 | var client_id = ''; 8 | var client_secret = ''; 9 | var redirect_uri = ''; 10 | var stateKey = 'spotify_auth_state'; 11 | 12 | 13 | //Spotify Login 14 | module.exports.spotifyLogin = function (res) { 15 | var state = generateRandomString(16); 16 | res.cookie(stateKey, state); 17 | // your application requests authorization 18 | var scope = 'user-read-email user-read-recently-played'; 19 | res.redirect('https://accounts.spotify.com/authorize?' + 20 | querystring.stringify({ 21 | response_type: 'code', 22 | client_id: client_id, 23 | scope: scope, 24 | redirect_uri: redirect_uri, 25 | state: state 26 | })); 27 | }; 28 | 29 | function generateRandomString(length) { 30 | var text = ''; 31 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 32 | 33 | for (var i = 0; i < length; i++) { 34 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 35 | } 36 | return text; 37 | }; 38 | 39 | //Spotify callback + sessions 40 | 41 | var spotifyApi = new SpotifyWebApi({ 42 | clientId: client_id, 43 | clientSecret: client_secret, 44 | redirectUri: redirect_uri 45 | }); 46 | 47 | module.exports.spotifyCallback = function (req, res) { 48 | spotifyApi.authorizationCodeGrant(req.query.code).then(function(data) { 49 | spotifyApi.setAccessToken(data.body.access_token); 50 | spotifyApi.setRefreshToken(data.body.refresh_token); 51 | return spotifyApi.getMe() 52 | 53 | }).then(function(data) { 54 | spotifyApi.getMyRecentlyPlayedTracks({ 55 | limit: 50 56 | }).then(function(data) { 57 | var arr = [], songIDs = []; 58 | data.body.items.forEach(function(p) { 59 | var obj = { 60 | id: p.track.id, 61 | played_at: p.played_at, 62 | name: p.track.name 63 | }; 64 | 65 | arr.push(obj); 66 | songIDs.push(p.track.id); 67 | 68 | }); 69 | //calculating the time difference 70 | var startTime = Date.parse(arr[arr.length - 1].played_at); 71 | var endTime = Date.parse(arr[0].played_at); 72 | //convert to hours 73 | var timeDif = (endTime - startTime) / (1000 * 60 * 60); 74 | 75 | if (timeDif < 10) { 76 | req.session.timeDiff = 0; 77 | console.log('timeDIff' + 0) 78 | } else if (timeDif > 10 && timeDif < 18) { 79 | req.session.timeDiff = 1; 80 | console.log('timeDIff' + 1) 81 | } else { 82 | req.session.timeDiff = 2; 83 | console.log('timeDIff' + 2) 84 | } 85 | spotifyApi.getAudioFeaturesForTracks(songIDs).then(function(data) { 86 | 87 | var danceability = 0, key = [], loudness = 0, valence = 0, tempo = 0, mode = 0, energy = 0, speechiness = 0, 88 | acousticness = 0, instrumentalness = 0, liveness = 0; 89 | 90 | data.body.audio_features.forEach(function(p1, p2, p3) { 91 | danceability += p1.danceability; 92 | key.push(p1.key); 93 | loudness += p1.loudness; 94 | valence += p1.valence; 95 | tempo += p1.tempo; 96 | mode += p1.mode; 97 | energy += p1.energy; 98 | speechiness += p1.speechiness; 99 | acousticness += p1.acousticness; 100 | instrumentalness += p1.instrumentalness; 101 | liveness += p1.liveness; 102 | }); 103 | var obj = { 104 | danceability: danceability / data.body.audio_features.length, 105 | key: frequent(key), 106 | loudness: loudness / data.body.audio_features.length, 107 | valence: valence / data.body.audio_features.length, 108 | tempo: tempo / data.body.audio_features.length, 109 | mode: Math.round(mode / data.body.audio_features.length), 110 | energy: energy / data.body.audio_features.length, 111 | speechiness: speechiness / data.body.audio_features.length, 112 | acousticness: acousticness / data.body.audio_features.length, 113 | instrumentalness: instrumentalness / data.body.audio_features.length, 114 | liveness: liveness / data.body.audio_features.length 115 | }; 116 | req.session.obj = obj; 117 | res.redirect('/musicScape'); 118 | }); 119 | }); 120 | req.session.user = data.body.id.length > 10? data.body.display_name : data.body.id; 121 | }); 122 | }; 123 | 124 | //function from: https://stackoverflow.com/a/1053865/7044471 125 | function frequent(array) { 126 | if(array.length == 0) 127 | return null; 128 | var modeMap = {}; 129 | var maxEl = array[0], maxCount = 1; 130 | for(var i = 0; i < array.length; i++) 131 | { 132 | var el = array[i]; 133 | if(modeMap[el] == null) 134 | modeMap[el] = 1; 135 | else 136 | modeMap[el]++; 137 | if(modeMap[el] > maxCount) 138 | { 139 | maxEl = el; 140 | maxCount = modeMap[el]; 141 | } 142 | } 143 | return maxEl; 144 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.18.2", 10 | "cookie-parser": "~1.4.3", 11 | "debug": "~2.6.9", 12 | "ejs": "~2.5.7", 13 | "express": "~4.15.5", 14 | "express-session": "^1.15.6", 15 | "morgan": "~1.9.0", 16 | "querystring": "^0.2.0", 17 | "serve-favicon": "~2.4.5", 18 | "spotify-web-api-node": "^2.5.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanAleksik/musicScape/d09b30483e21f7e8be4f64cb46bd8a12060ed4d3/public/favicon.ico -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | /* Title and Tooltip container */ 7 | 8 | .t1 { 9 | font-family: 'Work Sans', sans-serif; 10 | text-align: center; 11 | margin-top: 80vh; 12 | color: #a0a0a0; 13 | font-size: 2em; 14 | cursor: pointer; 15 | } 16 | 17 | @media only screen and (max-device-width: 768px) { 18 | /* For mobile phones: */ 19 | .t1 { 20 | font-size: 3em; 21 | } 22 | } 23 | 24 | .t2 { 25 | font-family: 'Work Sans', sans-serif; 26 | text-align: center; 27 | margin-top: 5vh; 28 | color: #a0a0a0; 29 | font-size: 20px; 30 | } 31 | 32 | .t3 { 33 | font-family: 'Work Sans', sans-serif; 34 | text-align: center; 35 | font-size: 2em; 36 | background-color: #d1d1d1; 37 | border: none; 38 | color: white; 39 | padding: 0.4em 0.8em; 40 | display: inline-block; 41 | outline: none; 42 | cursor: pointer; 43 | } 44 | 45 | .t3:hover { 46 | outline: none; 47 | } 48 | 49 | .t4 { 50 | font-family: 'Work Sans', sans-serif; 51 | text-align: center; 52 | line-height: 100px; 53 | margin-top: 5vh; 54 | color: #a0a0a0; 55 | font-size: 3em; 56 | } 57 | 58 | .t5 { 59 | font-family: 'Work Sans', sans-serif; 60 | text-align: center; 61 | margin-top: 1vh; 62 | margin-bottom: 5vh; 63 | color: #a0a0a0; 64 | font-size: 1.5em; 65 | } 66 | 67 | @media only screen and (max-device-width: 768px) { 68 | /* For mobile phones: */ 69 | .t3 { 70 | font-size: 5em; 71 | padding: 0.2em 0.5em; 72 | } 73 | .t4 { 74 | font-size: 5em; 75 | margin-bottom: 0.5em; 76 | } 77 | .t5 { 78 | font-size: 2.5em; 79 | margin-bottom: 1em; 80 | } 81 | } 82 | 83 | canvas { 84 | padding: 0; 85 | margin: auto; 86 | display: block; 87 | width: 800px; 88 | height: 600px; 89 | position: absolute; 90 | top: 0; 91 | bottom: 0; 92 | left: 0; 93 | right: 0; 94 | } 95 | 96 | /* Tooltip suff */ 97 | 98 | /* Tooltip text */ 99 | 100 | .t1 .tooltiptext { 101 | font-family: 'Roboto', sans-serif; 102 | visibility: hidden; 103 | background-color: #f7f7f7; 104 | opacity: 0.9; 105 | color: #a0a0a0; 106 | text-align: center; 107 | padding: 0.5em 0.5em; 108 | border-radius: 6px; 109 | font-size: 0.35em; 110 | text-align: center; 111 | bottom: 20%; 112 | position: absolute; 113 | z-index: 1; 114 | left: 50%; 115 | margin-left: -15%; 116 | width: 30%; 117 | } 118 | 119 | @media only screen and (max-device-width: 768px) { 120 | /* For mobile phones: */ 121 | .t1 .tooltiptext { 122 | font-size: 0.5em; 123 | margin-left: -40%; 124 | width: 80%; 125 | } 126 | } 127 | 128 | /* Show the tooltip text when you mouse over the tooltip container */ 129 | 130 | .t1:hover .tooltiptext { 131 | visibility: visible; 132 | } 133 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var spotifyController = require('../controllers/SpotifyController') 4 | 5 | 6 | /* GET home page. */ 7 | router.get('/', function(req, res, next) { 8 | res.render('index', { 9 | title: 'musicScape' 10 | }); 11 | }); 12 | 13 | router.get('/musicScape', function(req, res, next) { 14 | 15 | // this is an JSON obj that contains the avg of the song features 16 | var obj = req.session.obj; 17 | // this is the time difference between the first song and the last played song 18 | var landscapeSize = req.session.timeDiff; 19 | // this is userName 20 | var user = req.session.user; 21 | res.render('visualisation', { 22 | title: 'musicScape', 23 | d: obj, 24 | t: landscapeSize, 25 | u: user 26 | }); 27 | 28 | }); 29 | 30 | router.get('/spotifycallback', function(req, res) { 31 | spotifyController.spotifyCallback(req, res) 32 | }); 33 | 34 | router.get('/login', function(req, res) { 35 | spotifyController.spotifyLogin(res); 36 | }); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 |
24 |
25 |
musicScapes
26 |
landscapes based on your latest Spotify listens
27 | 28 |
29 |
30 | 31 | 32 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /views/visualisation.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <%= title %> 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | <%= u %> 24 | 's musicScape 25 | 26 |

The backgound of your musicScape is because you've recently listened to music that relates to emotions.

27 |

It's in your musicScape because your recent music is mostly in mode.

28 |

The mountains are jagged because you've listened to energetic songs.

29 |

You've been listener during the past 24 hours, so your musicScape has of mountains.

30 |

The mountains are tones of because your songs are mostly in the key .

31 |
32 |
33 | 34 | 35 | 386 | 387 | 388 | --------------------------------------------------------------------------------