├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── index.css
├── app
│ ├── content
│ │ ├── alert-icons
│ │ │ ├── check.png
│ │ │ └── exclamation.png
│ │ ├── playlists
│ │ │ └── playlists.js
│ │ └── playlist-details
│ │ │ ├── hybrid-rap-details.json
│ │ │ └── experimental-details.json
│ ├── components
│ │ ├── Header
│ │ │ ├── synthetic-logo@2x.png
│ │ │ └── Header.js
│ │ ├── Player
│ │ │ ├── Scrubber.js
│ │ │ ├── TrackInformation.js
│ │ │ ├── Timestamps.js
│ │ │ ├── Controls.js
│ │ │ └── Player.js
│ │ ├── Button.js
│ │ ├── Slider
│ │ │ ├── utils.js
│ │ │ ├── SliderSelector.js
│ │ │ └── Slider.js
│ │ ├── SongStats
│ │ │ ├── StatGraphRow.js
│ │ │ ├── StatElements.js
│ │ │ └── SongStatistics.js
│ │ ├── PlaylistDetails
│ │ │ ├── PlaylistDetails.js
│ │ │ └── PlaylistDetailRow.js
│ │ ├── Avatar
│ │ │ └── avatar.js
│ │ ├── PlaylistSelector
│ │ │ ├── PlaylistSelector.js
│ │ │ └── playlistsList.js
│ │ ├── Radar
│ │ │ └── RadarSection.js
│ │ ├── Instructions
│ │ │ ├── Promotion.js
│ │ │ └── HowItWorks.js
│ │ └── GenreSelector
│ │ │ └── GenreSelector.js
│ ├── styles
│ │ ├── details.css
│ │ ├── playlist-details.css
│ │ ├── select.css
│ │ ├── how-it-works.css
│ │ ├── slider.css
│ │ ├── buttons.css
│ │ ├── compiled-player.css
│ │ └── main.css
│ ├── genres.json
│ ├── javascripts
│ │ └── helpers.js
│ └── App.js
├── App.test.js
├── index.js
└── registerServiceWorker.js
├── .gitignore
├── README.md
├── package.json
└── LICENSE
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/content/alert-icons/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/content/alert-icons/check.png
--------------------------------------------------------------------------------
/src/app/content/alert-icons/exclamation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/content/alert-icons/exclamation.png
--------------------------------------------------------------------------------
/src/app/components/Header/synthetic-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/components/Header/synthetic-logo@2x.png
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './app/App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/src/app/components/Player/Scrubber.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Scrubber extends Component{
4 | render() {
5 | return (
6 |
9 | )
10 | }
11 | };
12 |
13 | export default Scrubber;
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Synthetic",
3 | "name": "Synthetic App - Spotify Add-on",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#191414",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # vscode
24 | .vscode
25 |
26 | # eslint
27 | .eslintcache
--------------------------------------------------------------------------------
/src/app/components/Player/TrackInformation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class TrackInformation extends Component{
4 | render() {
5 | return (
6 |
7 |
{this.props.track.name}
8 |
{this.props.track.artist}
9 |
{this.props.track.album}
10 |
11 | )
12 | }
13 | };
14 |
15 | export default TrackInformation;
--------------------------------------------------------------------------------
/src/app/styles/details.css:
--------------------------------------------------------------------------------
1 | .song-info {
2 | font-size: 18px;
3 | min-height: 75px;
4 | transition: 0.5s;
5 | }
6 |
7 | .example-enter {
8 | opacity: 0.01;
9 | }
10 |
11 | .example-enter.example-enter-active {
12 | opacity: 1;
13 | transition: opacity 500ms ease-in;
14 | }
15 |
16 | .example-leave {
17 | opacity: 1;
18 | }
19 |
20 | .example-leave.example-leave-active {
21 | opacity: 0.01;
22 | transition: opacity 300ms ease-in;
23 | }
--------------------------------------------------------------------------------
/src/app/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const buttonStyles = {
4 | borderColor: '#eee',
5 | borderWidth: 0,
6 | outline: 'none'
7 | };
8 |
9 | const Button = ({ className, disabled, id, onClick, value, style = {} }) => (
10 |
17 | {value}
18 |
19 | );
20 |
21 | export default Button;
--------------------------------------------------------------------------------
/src/app/genres.json:
--------------------------------------------------------------------------------
1 | {
2 | "genres" : [ "acoustic", "alt-rock", "alternative", "ambient", "bluegrass", "blues", "chill", "classical", "country", "dance", "deep-house", "disco", "drum-and-bass", "dubstep", "edm", "electro", "electronic", "folk", "happy", "heavy-metal", "hip-hop", "indie", "indie-pop", "jazz", "latin", "metal", "minimal-techno", "piano", "pop", "progressive-house", "punk", "r-n-b", "reggae", "reggaeton", "road-trip", "rock", "rock-n-roll", "romance", "sad", "salsa", "show-tunes", "singer-songwriter", "ska", "sleep", "soul", "soundtracks", "spanish", "study", "summer", "synth-pop", "techno", "trance", "work-out" ]
3 | }
--------------------------------------------------------------------------------
/src/app/components/Slider/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Capitalize first letter of string
3 | * @private
4 | * @param {string} - String
5 | * @return {string} - String with first letter capitalized
6 | */
7 | export function capitalize (str) {
8 | return str.charAt(0).toUpperCase() + str.substr(1)
9 | }
10 |
11 | /**
12 | * Clamp position between a range
13 | * @param {number} - Value to be clamped
14 | * @param {number} - Minimum value in range
15 | * @param {number} - Maximum value in range
16 | * @return {number} - Clamped value
17 | */
18 | export function clamp (value, min, max) {
19 | return Math.min(Math.max(value, min), max)
20 | }
--------------------------------------------------------------------------------
/src/app/components/SongStats/StatGraphRow.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { StatRow, StatTitle, StatValue, StatGraphHolder, StatGraph } from './StatElements';
3 |
4 | class StatGraphRow extends Component{
5 | render() {
6 | const { trackDetails, label, detailName } = this.props;
7 | return (
8 |
9 | {label}
10 | {(trackDetails[detailName] * 100).toFixed(0)}
11 |
12 |
13 |
14 |
15 | )
16 | }
17 | };
18 |
19 | export default StatGraphRow;
--------------------------------------------------------------------------------
/src/app/components/Player/Timestamps.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Timestamps extends Component{
4 | convertTime = (timestamp) => {
5 | let minutes = Math.floor(timestamp / 60);
6 | let seconds = timestamp - (minutes * 60);
7 | if(seconds < 10) {
8 | seconds = '0' + seconds;
9 | }
10 | timestamp = minutes + ':' + seconds;
11 | return timestamp;
12 | };
13 |
14 | render() {
15 | return (
16 |
17 |
{this.convertTime(this.props.currentTime)}
18 |
{this.convertTime(this.props.duration)}
19 |
20 | )
21 | }
22 | };
23 |
24 | export default Timestamps;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Synthetic
2 |
3 |
4 |
5 |
6 |
7 | A client-side React app for discovering music
8 |
9 |
10 |
11 |
12 | ### Setup
13 |
14 | Clone this repo to your desktop and run `npm install` or `yarn install` to install all the dependencies.
15 |
16 | ## Deployment
17 |
18 | Run `npm run build` or `yarn build` to create the build folder for deploys
19 |
20 | You can run the build files on a static server by first installing serve
21 |
22 | ```
23 | npm install -g serve
24 | ```
25 |
26 | and then running:
27 |
28 | ```
29 | npm install -g serve
30 | serve -s build
31 | ```
32 |
33 | ## Built With
34 |
35 | * [React](https://reactjs.org/) - UI and state management
36 | * [Spotify Web API](https://developer.spotify.com/web-api/) - API data
37 | * [Yarn](https://yarnpkg.com/en/) - dependency management
38 |
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "musicvault",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.16.2",
7 | "chart.js": "^2.7.0",
8 | "glamor": "^2.20.40",
9 | "glamorous": "^4.9.7",
10 | "jquery": "^3.2.1",
11 | "prop-types": "^15.6.0",
12 | "react": "^15.6.1",
13 | "react-alert": "^2.4.0",
14 | "react-chartjs-2": "^2.6.4",
15 | "react-dom": "^15.6.1",
16 | "react-ga": "^2.3.5",
17 | "react-icons": "^2.2.7",
18 | "react-rangeslider": "^2.2.0",
19 | "react-scripts": "1.0.13",
20 | "react-select": "^1.0.0-rc.10",
21 | "react-slick": "^0.15.4",
22 | "react-spinkit": "^3.0.0",
23 | "slick-carousel": "^1.8.1",
24 | "spotify-web-api-js": "^0.22.1"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test --env=jsdom",
30 | "eject": "react-scripts eject"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/components/PlaylistDetails/PlaylistDetails.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PlaylistDetailRow from "./PlaylistDetailRow";
3 | import playlistsList from "../PlaylistSelector/playlistsList";
4 |
5 | class PlaylistDetails extends Component {
6 | render() {
7 | let playlistRows = playlistsList.map(playlist => {
8 | return (
9 |
18 | );
19 | });
20 | return (
21 |
24 | );
25 | }
26 | }
27 |
28 | export default PlaylistDetails;
29 |
--------------------------------------------------------------------------------
/src/app/components/Player/Controls.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Controls extends Component{
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | };
26 |
27 | export default Controls;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Kyle Gill
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 |
--------------------------------------------------------------------------------
/src/app/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Avatar from "../Avatar/avatar";
3 | import BigButton from "../Button";
4 | import { spotifyImplicitAuth } from "../../javascripts/helpers.js";
5 | import logo from "./synthetic-logo@2x.png";
6 |
7 | class Header extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 | {this.props.params.access_token ? (
22 |
23 | ) : (
24 |
spotifyImplicitAuth(this.props.params)}
30 | />
31 | )}
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default Header;
39 |
--------------------------------------------------------------------------------
/src/app/styles/playlist-details.css:
--------------------------------------------------------------------------------
1 | .playlist-detail-row {
2 | display: grid;
3 | grid-template-areas: "playlist-art playlist-description";
4 | grid-template-columns: 205px 2fr;
5 | margin: 10px 0px;
6 | }
7 | .playlist-art {
8 | height: 175px;
9 | width: 175px;
10 | }
11 | #playlist-details-section-title {
12 | grid-area: playlist-header;
13 | font-size: 36px;
14 | color: #eee;
15 | text-align: center;
16 | align-self: end;
17 | }
18 | #playlist-subheader {
19 | align-self: center;
20 | }
21 | .playlist-info {
22 | grid-area: playlist-description;
23 | text-align: left;
24 | }
25 | .playlist-name {
26 | color: #ccc;
27 | font-size: 28px;
28 | line-height: 1;
29 | margin-bottom: 10px;
30 | vertical-align: bottom;
31 | }
32 | .playlist-description {
33 | font-size: 18px;
34 | color: #aaa;
35 | margin-bottom: 10px;
36 | }
37 | .playlist-follow {
38 | padding: 10px;
39 | border: 1px solid #666;
40 | height: 20px;
41 | background-color: transparent !important;
42 | }
43 |
44 | @media only screen and (max-width: 768px) {
45 | .playlist-name {
46 | color: #ccc;
47 | font-size: 20px;
48 | line-height: 1;
49 | margin-bottom: 10px;
50 | vertical-align: bottom;
51 | }
52 | .playlist-description {
53 | font-size: 12px;
54 | color: #aaa;
55 | margin-bottom: 10px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/components/Slider/SliderSelector.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import glamorous from 'glamorous';
3 | import Slider from './Slider';
4 |
5 |
6 | const SliderRow = glamorous.div({
7 | maxWidth: 820,
8 | margin: '0 auto',
9 | paddingBottom: 20,
10 | lineHeight: 1.25,
11 | '@media only screen and (max-width: 768px)': {
12 | paddingBottom: 5,
13 | minWidth: 300
14 | }
15 | });
16 |
17 |
18 | class SliderSelector extends Component{
19 |
20 | render() {
21 | const { label, value, filterOn } = this.props;
22 | const RadioSelect = glamorous.span({
23 | color: filterOn ? "#27b7ff" : "#5e5a5a"
24 | });
25 | return (
26 |
27 |
28 |
{label}
29 |
37 |
38 | {value}
39 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | };
48 |
49 | export default SliderSelector;
--------------------------------------------------------------------------------
/src/app/styles/select.css:
--------------------------------------------------------------------------------
1 | /* .Select {
2 | }
3 | .Select-control {
4 | background-color: #191414 !important;
5 | border: 1px solid #e6e6e6;
6 | color: #eee;
7 | }
8 | .Select-placeholder {
9 | color: #eee;
10 | }
11 | .Select-input > input {
12 | color: #eee;
13 | }
14 | .Select-control>.Select-value {
15 | background-color: rgba(153, 153, 153, .0) !important;
16 | border: 1px solid rgba(153, 153, 153, 0.24);
17 | color: #999;
18 | }
19 | .Select-control>.Select-value>.Select-value-icon {
20 | border-right: 1px solid rgba(153, 153, 153, 0.24);
21 | }
22 | .Select-control>.Select-value>.Select-value-label, .Select-value>a {
23 | color: #999;
24 | }
25 | .Select-menu-outer {
26 | background-color: #191414 !important;
27 | }
28 | .Select-option {
29 | background-color: #191414 !important;
30 | color: #eee;
31 | }
32 | .Select-option:hover {
33 | background-color: #1E2028 !important;
34 | color: #ccc;
35 | }
36 | .Select-option:focus {
37 | background-color: #1E2028 !important;
38 | color: #ccc;
39 | }
40 | .Select-option:active {
41 | background-color: #1E2028 !important;
42 | color: #ccc;
43 | }
44 | */
45 |
46 | .Select-control {
47 | background-color: rgb(230, 230, 230) !important;
48 | border-radius: 18px;
49 | color: #eee;
50 | height: 25px !important;
51 | }
52 | .Select-value {
53 | border-radius: 11px !important;
54 | background-color: #eee !important;
55 | }
56 | .Select-value-label {
57 | color: #999;
58 | background-color: none;
59 | }
60 | .Select-value-icon {
61 | color: #999 !important;
62 | border-top-left-radius: 11px !important;
63 | border-bottom-left-radius: 11px !important;
64 | }
65 |
66 | .Select-menu-outer {
67 | border-bottom-left-radius: 18px;
68 | border-bottom-right-radius: 18px;
69 | }
70 |
71 | .Select-menu {
72 | border-bottom-left-radius: 18px;
73 | border-bottom-right-radius: 18px;
74 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
29 | Synthetic - Find Music and Create Playlists
30 |
31 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/app/components/Avatar/avatar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import glamorous from 'glamorous';
3 | import { signOut } from '../../javascripts/helpers';
4 |
5 |
6 | class Avatar extends Component{
7 | render() {
8 | const AvatarContainer = glamorous.div({
9 | display: 'grid',
10 | gridTemplateAreas: `
11 | "text image"
12 | `,
13 | gridTemplateColumns: '1fr 40px'
14 | })
15 | let bgImage = this.props.me.images[0] ? this.props.me.images[0].url : "https://cdn.vox-cdn.com/images/verge/default-avatar.v989902574302a6378709709f7baab789b242ebbb.gif";
16 | const AvatarImg = glamorous.div({
17 | gridArea: 'image',
18 | width: 40,
19 | height: 40,
20 | borderRadius: 20,
21 | background: `url( ${bgImage} )`,
22 | backgroundSize: 'contain',
23 | marginRight: 10
24 | })
25 | const AvatarText = glamorous.div({
26 | textAlign: 'right',
27 | gridArea: 'text',
28 | padding: 5,
29 | lineHeight: 1
30 | })
31 | const AvatarSubText = glamorous.span({
32 | color: '#aaa',
33 | fontSize: 10,
34 | letterSpacing: 2,
35 | cursor: 'pointer'
36 | })
37 | return (
38 |
39 |
40 |
41 | {this.props.me.display_name ? this.props.me.display_name : this.props.me.id}
42 |
43 |
signOut()}>
44 | Sign out
45 |
46 |
47 |
48 |
49 | )
50 | }
51 | }
52 |
53 | Avatar.defaultProps = {
54 | me: {
55 | display_name: "User",
56 | images: [
57 | {
58 | url: "https://cdn.vox-cdn.com/images/verge/default-avatar.v989902574302a6378709709f7baab789b242ebbb.gif"
59 | }
60 | ]
61 | }
62 | }
63 |
64 | export default Avatar;
--------------------------------------------------------------------------------
/src/app/components/PlaylistSelector/PlaylistSelector.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Carousel from "react-slick";
3 | import "slick-carousel/slick/slick.css";
4 | import "slick-carousel/slick/slick-theme.css";
5 |
6 | class PlaylistSelector extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | activeSlide: 1
11 | };
12 | }
13 |
14 | render() {
15 | const settings = {
16 | infinite: true,
17 | speed: 200,
18 | slidesToShow: 1,
19 | slidesToScroll: 1,
20 | centerMode: true,
21 | swipeToSlide: true,
22 | afterChange: this.props.onChange,
23 | variableWidth: false
24 | };
25 | return (
26 |
27 |
28 |
29 |
{this.props.playlists[0].name}
30 |
31 |
32 |
{this.props.playlists[1].name}
33 |
34 |
35 |
{this.props.playlists[2].name}
36 |
37 |
38 |
{this.props.playlists[3].name}
39 |
40 |
41 |
{this.props.playlists[4].name}
42 |
43 |
44 |
{this.props.playlists[5].name}
45 |
46 |
47 |
{this.props.playlists[6].name}
48 |
49 |
50 |
{this.props.playlists[7].name}
51 |
52 |
53 |
{this.props.playlists[8].name}
54 |
55 |
56 |
{this.props.playlists[9].name}
57 |
58 |
59 |
Spotify Library
60 |
61 |
62 |
63 | );
64 | }
65 | }
66 |
67 | export default PlaylistSelector;
68 |
--------------------------------------------------------------------------------
/src/app/styles/how-it-works.css:
--------------------------------------------------------------------------------
1 | .hiw-text {
2 | max-width: 1200px;
3 | }
4 | .blue-gradient {
5 | color: #70d5ff;
6 | }
7 |
8 | .title {
9 | color: #eee;
10 | font-size: 36px;
11 | text-align: center;
12 | line-height: 1;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | }
17 | .title-subtitle {
18 | font-size: 18px;
19 | color: #aaa;
20 | margin-bottom: 15px;
21 | line-height: 1.5;
22 | }
23 | .title-subtext {
24 | font-size: 16px;
25 | text-align: center;
26 | padding: 15px;
27 | }
28 | .subtitle {
29 | color: #aaa;
30 | font-size: 28px;
31 | line-height: 28px;
32 | margin: 0px 20px 0px 20px;
33 | grid-area: subtitle;
34 | }
35 | .subtitle-icon {
36 | grid-area: icon;
37 | font-size: 54px;
38 | color: #70d5ff;
39 | text-align: center;
40 | padding: 15px;
41 | }
42 |
43 | .section-3-col {
44 | display: grid;
45 | grid-template-areas: "section1 section2 section3";
46 | grid-template-columns: 1fr 1fr 1fr;
47 | }
48 | .section {
49 | display: grid;
50 | margin: 20px 0px 20px 0px;
51 | grid-template-areas:
52 | "... icon ..." "... subtitle ..."
53 | "content content content";
54 | grid-template-columns: 1fr 1fr 1fr;
55 | grid-gap: 10px;
56 | align-content: center;
57 | text-align: center;
58 | }
59 | .content {
60 | grid-area: content;
61 | font-size: 20px;
62 | line-height: 24px;
63 | }
64 | .content-step {
65 | margin: 15px;
66 | }
67 | @media only screen and (max-width: 768px) {
68 | .section-3-col {
69 | display: grid;
70 | grid-template-areas: "section1" "section2" "section3";
71 | grid-template-columns: 1fr;
72 | }
73 | }
74 |
75 | .hiw-image {
76 | grid-area: image;
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | }
81 | .hiw-image-icon {
82 | font-size: 160px;
83 | color: #fff;
84 | background: linear-gradient(to top left, #27b7ff, #70d5ff);
85 | border-radius: 125px;
86 | height: 250px;
87 | width: 250px;
88 | justify-content: center;
89 | align-items: center;
90 | display: flex;
91 | }
92 | .hiw-image-icon > svg {
93 | margin: 0px 0px 8px 8px;
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/components/SongStats/StatElements.js:
--------------------------------------------------------------------------------
1 | import glamorous from 'glamorous';
2 |
3 | const KeySignatures = {
4 | 0: 'C',
5 | 1: 'C♯, D♭',
6 | 2: 'D',
7 | 3: 'D♯, E♭',
8 | 4: 'E',
9 | 5: 'F',
10 | 6: 'F♯, G♭',
11 | 7: 'G',
12 | 8: 'G♯, A♭',
13 | 9: 'A',
14 | 10: 'A♯, B♭',
15 | 11: 'B'
16 | }
17 |
18 | const StatRow = glamorous.div({
19 | '@supports (display: grid)': {
20 | display: 'grid',
21 | gridTemplateColumns: '1.5fr 0.5fr 3fr',
22 | gridTemplateAreas: `
23 | "title value graph"
24 | `,
25 | gridGap: '10px',
26 | },
27 | fontSize: 17,
28 | marginBottom: 15
29 | })
30 | const TitleGraph = glamorous.div({
31 | '@supports (display: grid)': {
32 | display: 'grid',
33 | gridTemplateColumns: '2fr 3fr',
34 | gridTemplateAreas: `
35 | "title graph"
36 | `,
37 | gridGap: '20px',
38 | },
39 | fontSize: 17,
40 | marginBottom: 15
41 | })
42 | const StatText = glamorous.div({
43 | '@supports (display: grid)': {
44 | display: 'grid',
45 | gridTemplateColumns: '1.5fr 3.5fr',
46 | gridTemplateAreas: `
47 | "title label"
48 | `,
49 | gridGap: '10px',
50 | },
51 | fontSize: 17,
52 | marginBottom: 15
53 | })
54 | const StatTitle = glamorous.div({
55 | color: '#bbb',
56 | gridArea: 'title',
57 | textAlign: 'left'
58 | })
59 | const StatTag = glamorous.div({
60 | color: '#bbb',
61 | gridArea: 'title',
62 | textAlign: 'center',
63 | border: 'solid 1px #ccc',
64 | borderRadius: 2,
65 | padding: 5
66 | })
67 | const StatValue = glamorous.div({
68 | color: '#777',
69 | gridArea: 'value',
70 | textAlign: 'right'
71 | })
72 | const StatLabel = glamorous.div({
73 | color: '#777',
74 | gridArea: 'label',
75 | textAlign: 'left'
76 | })
77 | const StatGraphHolder = glamorous.div({
78 | gridArea: 'graph',
79 | display: 'flex'
80 | })
81 | const StatGraph = glamorous.div({
82 | color: '#777',
83 | background: 'linear-gradient(to left, #27b7ff, #70D5FF)',
84 | transition: 'width 0.5s ease',
85 | height: 8,
86 | alignSelf: 'center',
87 | borderRadius: '3px'
88 | })
89 |
90 | export { KeySignatures, StatRow, StatTitle, StatTag, StatLabel, StatValue, StatGraphHolder, StatGraph, StatText, TitleGraph }
--------------------------------------------------------------------------------
/src/app/styles/slider.css:
--------------------------------------------------------------------------------
1 | .slider-grid {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | }
6 | .slider-label {
7 | width: 30% !important;
8 | font-size: 20px;
9 | vertical-align: center;
10 | color: #eee;
11 | text-align: right;
12 | }
13 | .slider-value {
14 | display: grid;
15 | grid-template-areas: "number button";
16 | grid-template-columns: 50px 1fr;
17 | width: 30% !important;
18 | font-size: 20px;
19 | color: #eee;
20 | text-align: left;
21 | }
22 |
23 | .rangeslider-horizontal {
24 | width: 300px;
25 | margin: 0 auto;
26 | height: 20px;
27 | border-radius: 20px;
28 | }
29 | @media only screen and (max-width: 768px) {
30 | .slider-grid {
31 | display: grid;
32 | grid-template-areas:
33 | "... slider slider slider ..."
34 | "... label label value ...";
35 | grid-template-columns: 1fr 2fr 2fr 2fr 1fr;
36 | margin: 3px 0;
37 | }
38 | .slider-label {
39 | width: 50% !important;
40 | font-size: 20px;
41 | color: #eee;
42 | text-align: left;
43 | grid-area: label;
44 | }
45 | .slider-value {
46 | width: 50% !important;
47 | font-size: 20px;
48 | color: #eee;
49 | text-align: center;
50 | grid-area: value;
51 | align-self: right;
52 | }
53 | .rangeslider-horizontal {
54 | width: 100%;
55 | margin: 0 auto;
56 | height: 20px;
57 | border-radius: 20px;
58 | grid-area: slider;
59 | }
60 | }
61 |
62 | .slider {
63 | padding: 40px;
64 | }
65 | .rangeslider__handle {
66 | width: 60px !important;
67 | height: 25px !important;
68 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05), 0 1.5px 1.5px rgba(0, 0, 0, 0.12) !important;
69 | }
70 | .rangeslider__handle:focus {
71 | outline: none;
72 | }
73 | .rangeslider__handle::after {
74 | display: none;
75 | }
76 | .rangeslider__fill {
77 | border-radius: 20px !important;
78 | min-width: 50px;
79 | }
80 | .scale-emphasis {
81 | transform: scale(1);
82 | transition: transform 0.2s ease;
83 | cursor: pointer;
84 | }
85 | .scale-emphasis:hover {
86 | transform: scale(1.25);
87 | transition: transform 0.2s ease;
88 | }
89 |
90 | .slick-slide {
91 | color: transparent;
92 | transition: color 0.3s linear;
93 | }
94 | .slick-active {
95 | color: #ccc;
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/styles/buttons.css:
--------------------------------------------------------------------------------
1 | .calculateButton-section {
2 | margin-bottom: 30px;
3 | }
4 | .calculateButton {
5 | border-radius: 38px !important;
6 | height: 60px;
7 | width: 225px;
8 | font-size: 20px !important;
9 | color: #eee !important;
10 | background: linear-gradient(to left, #27b7ff, #70d5ff) !important;
11 | transform: scale(1) !important;
12 | transition: transform 0.15s ease !important;
13 | cursor: pointer;
14 | }
15 | .calculateButton:hover {
16 | background: linear-gradient(to left, #39bdff, #80d9ff) !important;
17 | transform: scale(1.035) !important;
18 | transition: transform 0.2s ease !important;
19 | }
20 | .calculateButton:active {
21 | color: #ccc !important;
22 | transform: scale(0.95) !important;
23 | transition: transform 0.1s ease !important;
24 | }
25 |
26 | .loginButton {
27 | border-radius: 38px !important;
28 | height: 40px;
29 | width: 125px;
30 | font-size: 14px !important;
31 | color: #eee !important;
32 | border: 1.5px solid #aaa !important;
33 | background-color: rgba(0, 0, 0, 0) !important;
34 | transform: scale(1) !important;
35 | transition: transform 0.15s ease !important;
36 | cursor: pointer;
37 | }
38 | .loginButton:hover {
39 | transform: scale(1.035) !important;
40 | transition: transform 0.2s ease !important;
41 | border: 2px solid #aaa !important;
42 | }
43 | .loginButton:active {
44 | color: #ccc !important;
45 | transform: scale(0.95) !important;
46 | transition: transform 0.1s ease !important;
47 | }
48 | @media only screen and (max-width: 768px) {
49 | .loginButton {
50 | border-radius: 38px !important;
51 | height: 35px;
52 | width: 100px;
53 | }
54 | }
55 |
56 | .followButton {
57 | border-radius: 38px !important;
58 | height: 40px;
59 | width: 125px;
60 | font-size: 14px !important;
61 | color: #eee !important;
62 | border: 1.5px solid #aaa !important;
63 | background-color: rgba(0, 0, 0, 0) !important;
64 | transform: scale(1) !important;
65 | transition: transform 0.15s ease !important;
66 | cursor: pointer;
67 | }
68 | .followButton:hover {
69 | transform: scale(1.035) !important;
70 | transition: transform 0.2s ease !important;
71 | border: 2px solid #aaa !important;
72 | }
73 | .followButton:active {
74 | color: #ccc !important;
75 | transform: scale(0.95) !important;
76 | transition: transform 0.1s ease !important;
77 | }
78 | @media only screen and (max-width: 768px) {
79 | .followButton {
80 | border-radius: 38px !important;
81 | height: 35px;
82 | width: 100px;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/content/playlists/playlists.js:
--------------------------------------------------------------------------------
1 | // playlist imports
2 | import sampler from "./sampler.json";
3 | import syntheticLibrary from "./synthetic-library.json";
4 | import indietronic from "./indietronic.json";
5 | import hipsterSynthpop from "./hipster-synthpop.json";
6 | import absoluteFocus from "./absolute-focus.json";
7 | import tropicalVibes from "./tropical-vibes.json";
8 | import chillFolk from "./chill-folk.json";
9 | import epicCinematic from "./epic-cinematic.json";
10 | import experimental from "./experimental.json";
11 | import hybridRap from "./hybrid-rap.json";
12 | import subwoofer from "./subwoofer-sounds.json";
13 | // playlist detail imports
14 | import samplerDetails from "../playlist-details/sampler-details.json";
15 | import syntheticLibraryDetails from "../playlist-details/synthetic-library-details.json";
16 | import indietronicDetails from "../playlist-details/indietronic-details.json";
17 | import hipsterSynthpopDetails from "../playlist-details/hipster-synthpop-details.json";
18 | import absoluteFocusDetails from "../playlist-details/absolute-focus-details.json";
19 | import tropicalVibesDetails from "../playlist-details/tropical-vibes-details.json";
20 | import chillFolkDetails from "../playlist-details/chill-folk-details.json";
21 | import epicCinematicDetails from "../playlist-details/epic-cinematic-details.json";
22 | import experimentalDetails from "../playlist-details/experimental-details.json";
23 | import hybridRapDetails from "../playlist-details/hybrid-rap-details.json";
24 | import subwooferDetails from "../playlist-details/subwoofer-details.json";
25 |
26 | const playlists = [
27 | {
28 | name: "Synthetic Library",
29 | data: syntheticLibrary,
30 | details: syntheticLibraryDetails
31 | },
32 | {
33 | name: "Indietronic",
34 | data: indietronic,
35 | details: indietronicDetails
36 | },
37 | {
38 | name: "Hipster Synthpop",
39 | data: hipsterSynthpop,
40 | details: hipsterSynthpopDetails
41 | },
42 | {
43 | name: "Absolute Focus",
44 | data: absoluteFocus,
45 | details: absoluteFocusDetails
46 | },
47 | {
48 | name: "Tropical Vibes",
49 | data: tropicalVibes,
50 | details: tropicalVibesDetails
51 | },
52 | {
53 | name: "Chill Folk",
54 | data: chillFolk,
55 | details: chillFolkDetails
56 | },
57 | {
58 | name: "Experimental Sounds",
59 | data: experimental,
60 | details: experimentalDetails
61 | },
62 | {
63 | name: "Epic Cinematic",
64 | data: epicCinematic,
65 | details: epicCinematicDetails
66 | },
67 | {
68 | name: "Subwoofer Bass",
69 | data: subwoofer,
70 | details: subwooferDetails
71 | },
72 | {
73 | name: "Hybrid Rap",
74 | data: hybridRap,
75 | details: hybridRapDetails
76 | },
77 | {
78 | name: "Spotify Library",
79 | data: sampler,
80 | details: samplerDetails
81 | }
82 | ];
83 |
84 | export default playlists;
85 |
--------------------------------------------------------------------------------
/src/app/components/Radar/RadarSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Radar } from "react-chartjs-2";
3 | import glamorous from "glamorous";
4 |
5 | const Title = glamorous.div({
6 | color: "#eee",
7 | fontSize: 26,
8 | marginBottom: 25,
9 | textAlign: "left"
10 | });
11 |
12 | class RadarSection extends Component {
13 | render() {
14 | const {
15 | track,
16 | trackDetails,
17 | energyValue,
18 | valenceValue,
19 | acousticValue,
20 | danceValue,
21 | popularityValue,
22 | vocalnessValue
23 | } = this.props;
24 | let data = {
25 | labels: [
26 | "Energy",
27 | "Valence",
28 | "Acoustic",
29 | "Dance",
30 | "Popularity",
31 | "Vocalness"
32 | ],
33 | datasets: [
34 | {
35 | label: "Input",
36 | lineTension: 0.075,
37 | backgroundColor: "rgba(39, 183, 255, 0.25)",
38 | borderColor: "rgba(39, 183, 255, 1)",
39 | borderWidth: 2,
40 | pointRadius: 0,
41 | pointHitRadius: 5,
42 | pointBackgroundColor: "rgba(39, 183, 255, 1)",
43 | pointBorderColor: "#191414",
44 | pointHoverBorderColor: "rrgba(255,255,255, 0.5)",
45 | data: [
46 | energyValue,
47 | valenceValue,
48 | acousticValue,
49 | danceValue,
50 | popularityValue,
51 | vocalnessValue
52 | ]
53 | },
54 | {
55 | label: "Actual",
56 | lineTension: 0.075,
57 | backgroundColor: "rgba(112,213,255, 0.25)",
58 | borderColor: "rgba(255,255,255, 1)",
59 | borderWidth: 2,
60 | pointRadius: 0,
61 | pointHitRadius: 5,
62 | pointBackgroundColor: "rgba(255,255,255, 1)",
63 | pointBorderColor: "#191414",
64 | pointHoverBorderColor: "rgba(255,255,255, 0.5)",
65 | data: [
66 | trackDetails.energy * 100,
67 | trackDetails.valence * 100,
68 | trackDetails.acousticness * 100,
69 | trackDetails.danceability * 100,
70 | track.popularity,
71 | Math.abs(trackDetails.instrumentalness * 100 - 100)
72 | ]
73 | }
74 | ]
75 | };
76 | return (
77 |
78 |
Comparison
79 |
111 |
112 | );
113 | }
114 | }
115 |
116 | export default RadarSection;
117 |
--------------------------------------------------------------------------------
/src/app/components/SongStats/SongStatistics.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import glamorous from "glamorous";
3 | import StatGraphRow from "./StatGraphRow";
4 | import {
5 | KeySignatures,
6 | StatRow,
7 | StatTitle,
8 | StatTag,
9 | StatLabel,
10 | StatValue,
11 | StatGraphHolder,
12 | StatGraph,
13 | StatText
14 | } from "./StatElements";
15 |
16 | const Title = glamorous.div({
17 | color: "#eee",
18 | fontSize: 26,
19 | marginBottom: 25,
20 | textAlign: "left"
21 | });
22 | const Subtitle = glamorous.div({
23 | color: "#eee",
24 | fontSize: 18,
25 | marginTop: 10,
26 | marginBottom: 20,
27 | textAlign: "left"
28 | });
29 |
30 | class SongStatistics extends Component {
31 | render() {
32 | const { track, trackDetails } = this.props;
33 | return (
34 |
35 |
Audio Analysis
36 |
42 |
48 |
54 |
60 |
61 | Popularity
62 | {track.popularity.toFixed(0)}
63 |
64 |
65 |
66 |
67 |
68 | Vocalness
69 |
70 | {Math.abs((trackDetails.instrumentalness * 100).toFixed(0) - 100)}
71 |
72 |
73 |
80 | {" "}
81 |
82 |
83 |
84 |
85 | Composite Score{" "}
86 |
87 | {600 - track.ResultDifference || 0}
88 |
89 |
90 |
91 | {track.explicit ? "EXPLICIT" : "CLEAN"}
92 |
93 |
94 | BPM
95 | {trackDetails.tempo.toFixed(0)}
96 | {/* BPM
97 | {(trackDetails.tempo).toFixed(0)} */}
98 |
99 |
100 | Key
101 | {KeySignatures[trackDetails.key]}
102 |
103 |
104 | * all analyses and metrics are approximations and may not accurately
105 | reflect the true nature of track
106 |
107 |
108 | );
109 | }
110 | }
111 |
112 | export default SongStatistics;
113 |
--------------------------------------------------------------------------------
/src/app/styles/compiled-player.css:
--------------------------------------------------------------------------------
1 | .Player {
2 | grid-area: player;
3 | background: #383838;
4 | overflow: hidden;
5 | box-shadow: 0 5px 10px -5px rgba(18, 18, 18, 1);
6 | height: 475px;
7 | position: relative;
8 | width: 300px;
9 | }
10 | .Player .Background {
11 | width: 150%;
12 | height: 150%;
13 | position: absolute;
14 | top: -25%;
15 | left: -25%;
16 | background-size: cover;
17 | background-position: center center;
18 | opacity: 0.4;
19 | filter: blur(8px);
20 | }
21 | .Player .Artwork {
22 | width: 300px;
23 | height: 300px;
24 | background-size: cover;
25 | background-position: center center;
26 | margin: auto;
27 | box-shadow: 0 5px 10px -5px rgba(18, 18, 18, .25);
28 | position: relative;
29 | }
30 | .Player .TrackInformation {
31 | width: 300px;
32 | margin: 20px auto;
33 | text-align: center;
34 | position: relative;
35 | padding: 0px 20px 0px 20px;
36 | }
37 | .Player .TrackInformation .Name {
38 | font-size: 20px;
39 | margin-bottom: 10px;
40 | font-weight: 300;
41 | }
42 | .Player .TrackInformation .Artist {
43 | font-size: 16px;
44 | font-weight: 500;
45 | }
46 | .Player .TrackInformation .Album {
47 | font-size: 12px;
48 | opacity: 0.5;
49 | }
50 | .Player .Scrubber {
51 | position: absolute;
52 | bottom: 0;
53 | left: 0;
54 | width: 100%;
55 | height: 20%;
56 | opacity: 0.2;
57 | transition: opacity 0.25s ease;
58 | }
59 | .Player .Scrubber .Scrubber-Progress {
60 | background: -moz-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%);
61 | background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(255, 71, 0, 0)), color-stop(100%, #34BAFD));
62 | background: -webkit-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%);
63 | background: -o-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%);
64 | background: -ms-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%);
65 | background: linear-gradient(to bottom, rgba(255, 71, 0, 0) 0%, #34BAFD 100%);
66 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#34BAFD', endColorstr='#34BAFD', GradientType=0);
67 | height: 100%;
68 | width: 0%;
69 | transition: width 0.25s ease;
70 | pointer-events: none;
71 | }
72 | .Player .Timestamps {
73 | display: flex;
74 | justify-content: space-between;
75 | box-sizing: border-box;
76 | padding: 20px;
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | pointer-events: none;
81 | width: 100%;
82 | }
83 | .Player .Timestamps .Time {
84 | font-size: 12px;
85 | }
86 | .Player .Controls {
87 | position: absolute;
88 | top: 10px;
89 | pointer-events: none;
90 | margin: auto;
91 | left: 0;
92 | right: 0;
93 | z-index: 5;
94 | display: flex;
95 | width: 200px;
96 | }
97 | .Player .Controls .Button {
98 | height: 40px;
99 | width: 40px;
100 | box-sizing: border-box;
101 | display: flex;
102 | align-items: center;
103 | justify-content: center;
104 | margin: auto;
105 | pointer-events: all;
106 | cursor: pointer;
107 | }
108 | .Player .Controls .Button:hover {
109 | transform: scale(1.09);
110 | }
111 | .Player .Controls .Button:active {
112 | transform: scale(0.98);
113 | }
114 | .Player .Controls .Button:active .fa {
115 | color: #34BAFD;
116 | opacity: 1;
117 | }
118 | .Player .Controls .Button .fa {
119 | color: #fff;
120 | opacity: 0.5;
121 | font-size: 16px;
122 | }
123 | .Player .Controls .Button .fa.fa-play {
124 | margin-left: 5px;
125 | }
126 | .PlayButton {
127 | border: 2px solid rgba(255, 255, 255, .5);
128 | border-radius: 75px;
129 | height: 40px;
130 | width: 40px;
131 | }
132 | .PlayButton:active {
133 | border: 2px solid #34BAFD;
134 | }
135 |
136 | .EmptyHeader {
137 | height: 60px;
138 | }
139 | #button-outer-left {
140 | text-align: right !important;
141 | margin-left: 5px;
142 | }
143 | #button-outer-right {
144 | text-align: left !important;
145 | margin-right: 5px;
146 | }
--------------------------------------------------------------------------------
/src/app/styles/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | /* background: linear-gradient(to left, #27b7ff, #70D5FF); */
3 | background: #191414;
4 | font-family: "Montserrat", sans-serif;
5 | color: #5e5a5a;
6 | }
7 | h4 {
8 | letter-spacing: 4px;
9 | color: #5e5a5a;
10 | }
11 |
12 | .App {
13 | text-align: center;
14 | }
15 |
16 | .app-header {
17 | background-color: #1e1b1b;
18 | height: 60px;
19 | color: white;
20 | display: grid;
21 | grid-template-areas: "... title title title title login";
22 | grid-template-columns: 1.5fr repeat(4, 2fr) 1.5fr;
23 | text-align: center;
24 | }
25 | .app-header-title {
26 | letter-spacing: 12px;
27 | font-weight: 900;
28 | font-size: 42px;
29 | grid-area: title;
30 | align-self: center;
31 | margin: 0 auto;
32 | }
33 | .playlist-selector {
34 | text-align: center;
35 | margin: 10px;
36 | display: grid;
37 | grid-template-areas: "... carousel ...";
38 | grid-template-columns: 1fr 320px 1fr;
39 | }
40 | .app-footer {
41 | margin-top: 30px;
42 | margin-bottom: 30px;
43 | }
44 |
45 | .login-section {
46 | grid-area: login;
47 | align-self: right;
48 | padding: 10px;
49 | }
50 |
51 | .player-section {
52 | display: grid;
53 | grid-template-areas: "... radar player stats ...";
54 | grid-template-columns: 0.75fr 1fr 300px 1fr 0.75fr;
55 | grid-gap: 20px;
56 | align-items: flex-start;
57 | font-family: "Montserrat", sans-serif;
58 | color: white;
59 | }
60 | .radar-section {
61 | grid-area: radar;
62 | margin-top: 20px;
63 | max-width: 300px;
64 | max-height: 300px;
65 | justify-self: end;
66 | }
67 | .stats-section {
68 | grid-area: stats;
69 | width: 300px;
70 | margin-top: 20px;
71 | }
72 | .promotion-section {
73 | background: #1e1b1b;
74 | margin: 60px 0px 0px 0px;
75 | padding: 60px;
76 | }
77 | .instructions-section {
78 | display: grid;
79 | grid-template-areas: "section1" "section2" "section3";
80 | grid-auto-rows: auto auto 1fr;
81 | grid-gap: 60px;
82 | padding: 60px;
83 | justify-content: center;
84 | }
85 | .genre-section {
86 | display: grid;
87 | grid-template-areas: "... label selector details ...";
88 | grid-template-columns: 2fr 1fr 300px 1fr 2fr;
89 | grid-gap: 15px;
90 | margin-bottom: 20px;
91 | text-align: left;
92 | color: #eee;
93 | }
94 | .selector-label {
95 | grid-area: label;
96 | text-align: right;
97 | font-size: 20px;
98 | align-self: center;
99 | }
100 | .selector-details-label {
101 | grid-area: details;
102 | font-size: 20px;
103 | align-self: center;
104 | }
105 | .playlist-details-section {
106 | display: grid;
107 | grid-template-areas: "playlists";
108 | grid-auto-rows: 1fr;
109 | }
110 |
111 | @media only screen and (max-width: 768px) {
112 | .app-header {
113 | height: inherit;
114 | grid-template-areas: "title login";
115 | grid-template-columns: 1fr 1fr;
116 | grid-gap: 10px;
117 | line-height: 1;
118 | padding: 10px 10px 10px 10px;
119 | }
120 | .app-header-title {
121 | letter-spacing: 4px;
122 | font-weight: 900;
123 | font-size: 36px;
124 | }
125 | .promotion-section {
126 | padding: 20px;
127 | }
128 | .instructions-section {
129 | padding: 20px;
130 | }
131 | .genre-section {
132 | grid-template-areas:
133 | "... selector selector selector ..."
134 | "... label label details ...";
135 | grid-template-columns: 1fr 1fr 2fr 1fr 1fr;
136 | grid-gap: 5px;
137 | }
138 | .selector-label {
139 | text-align: left;
140 | }
141 | .player-section {
142 | grid-template-areas: "... player ..." "... stats ..." "... radar ...";
143 | grid-template-columns: 1fr 300px 1fr;
144 | grid-gap: 10px;
145 | align-items: center;
146 | }
147 | .radar-section {
148 | margin-bottom: 30px;
149 | justify-self: start;
150 | }
151 | .playlist-selector {
152 | grid-template-columns: 1fr 200px 1fr;
153 | margin: 30px;
154 | }
155 | }
156 |
157 | .loading-spinner {
158 | text-align: center;
159 | margin-top: 6px;
160 | }
161 |
--------------------------------------------------------------------------------
/src/app/components/Instructions/Promotion.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "../../styles/how-it-works.css";
3 | import * as Fa from "react-icons/lib/fa";
4 | import logo from "../Header/synthetic-logo@2x.png";
5 |
6 | class Promotion extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
16 |
17 | Spotify powered, data-driven music discovery
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Customize
26 |
27 |
28 |
29 | Select a playlist
30 |
31 |
32 | Adjust filter levels
33 |
34 |
35 | Turn filters on/off
36 |
37 |
38 |
50 |
51 |
52 |
53 |
54 | Calculate
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
Discover
63 |
64 |
65 |
66 | Play/Pause a song
67 |
68 |
69 |
70 | Cycle through songs in result set
71 |
72 |
73 | Add song to your library on Spotify
74 |
75 |
76 | Add top results into a playlist
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
Visualize
85 |
86 |
Easily digest song stats
87 |
88 | Compare your search to actual stats
89 |
90 |
91 | Avoid songs with explicit lyrics
92 |
93 |
94 | Sort instrumental or vocal tracks
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 | }
103 |
104 | export default Promotion;
105 |
--------------------------------------------------------------------------------
/src/app/components/Player/Player.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import glamorous from 'glamorous';
3 | import TrackInformation from './TrackInformation';
4 | import Scrubber from './Scrubber';
5 | import Controls from './Controls';
6 | import Timestamps from './Timestamps';
7 |
8 |
9 | const GreenPlayerDivider = glamorous.div({
10 | borderBottom: '3px solid #34BAFD'
11 | });
12 |
13 | class Player extends Component{
14 | constructor(props) {
15 | super(props);
16 | this.state = {
17 | playStatus: 'play',
18 | currentTime: 0
19 | }
20 | };
21 |
22 | componentDiDMount() {
23 | this.props.ref(this);
24 | this.setState({
25 | currentTime: 0,
26 | playStatus: 'play'
27 | });
28 | }
29 | componentWillUnmount() {
30 | this.loadInterval && clearInterval(this.loadInterval);
31 | this.loadInterval = false;
32 | }
33 | shouldComponentUpdate(nextProps, nextState) {
34 | // set of conditions that check to make sure the Player should actually update UI
35 | // rather than get stuck infintie looping
36 | return (nextProps.track.name !== this.props.track.name) ||
37 | (nextProps.track.source !== this.props.track.source) ||
38 | (this.props.songInLibrary !== nextProps.songInLibrary) ||
39 | (this.props.createdPlaylist !== nextProps.createdPlaylist) ||
40 | (this.state.playStatus !== nextState.playStatus) ||
41 | (this.props.playStatus !== nextProps.playStatus) ||
42 | (this.state.currentTime !== nextState.currentTime);
43 | }
44 |
45 | stopPlayback = () => {
46 | // activated from App.js by higher level functions to stop the player playback
47 | let audio = document.getElementById('audio');
48 | audio.pause();
49 | this.setState({ playStatus: 'play' });
50 | audio.load();
51 | }
52 |
53 | updateTime = (timestamp) => {
54 | timestamp = Math.floor(timestamp);
55 | this.setState({ currentTime: timestamp });
56 | }
57 |
58 | updateScrubber = (percent) => {
59 | // Set scrubber width
60 | let innerScrubber = document.querySelector('.Scrubber-Progress');
61 | if (innerScrubber){innerScrubber.style['width'] = percent;}
62 | }
63 |
64 | togglePlay = () => {
65 | let status = this.state.playStatus;
66 | let audio = document.getElementById('audio');
67 | if(status === 'play') {
68 | status = 'pause';
69 | audio.play();
70 | let that = this;
71 | this.loadInterval = setInterval(function() {
72 | let currentTime = audio.currentTime;
73 | let duration = that.props.track.duration;
74 |
75 | // Calculate percent of song
76 | let percent = (currentTime / duration) * 100 + '%';
77 | that.updateScrubber(percent);
78 | that.updateTime(currentTime);
79 | }, 100);
80 | } else {
81 | status = 'play';
82 | audio.pause();
83 | }
84 | this.setState({ playStatus: status });
85 | };
86 |
87 | render() {
88 | return (
89 |
90 |
91 |
92 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 | };
115 |
116 | Player.defaultProps = {
117 | track: {
118 | name: 'We Were Young',
119 | artist: 'Odesza',
120 | album: 'Summers Gone',
121 | artwork: 'https://funkadelphia.files.wordpress.com/2012/09/odesza-summers-gone-lp.jpg',
122 | duration: 192,
123 | source: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/557257/wwy.mp3'
124 | }
125 | };
126 |
127 | export default Player;
--------------------------------------------------------------------------------
/src/app/components/PlaylistDetails/PlaylistDetailRow.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "../../styles/playlist-details.css";
3 | import Button from "../Button";
4 | import Spotify from "spotify-web-api-js";
5 |
6 | class PlaylistDetailRow extends Component {
7 | constructor(props, context) {
8 | super(props, context);
9 | this.state = {
10 | following: false,
11 | beenChanged: false
12 | };
13 | }
14 |
15 | shouldComponentUpdate(nextProps, nextState) {
16 | // easier to read conditions for whether the row should update to show a change in the following button
17 | let shouldUpdate = true;
18 | if (nextProps.userId === null) {
19 | shouldUpdate = false;
20 | }
21 | if (nextProps.loggedIn === false) {
22 | shouldUpdate = false;
23 | }
24 | if (nextProps.initialLoad === false) {
25 | shouldUpdate = false;
26 | }
27 | if (nextState.following === true && this.state.following === true) {
28 | shouldUpdate = false;
29 | }
30 | return shouldUpdate;
31 | }
32 |
33 | onClickFollow() {
34 | // follow playlist
35 | if (!this.props.loggedIn) {
36 | this.props.showFollowAlert();
37 | } else if (!this.state.following) {
38 | const spotifyApi = new Spotify();
39 | spotifyApi.setAccessToken(this.props.accessToken);
40 | spotifyApi
41 | .followPlaylist(this.props.playlist.owner.id, this.props.playlist.id)
42 | .then(response => {
43 | if (response[0] == true) {
44 | this.setState({
45 | following: true
46 | });
47 | }
48 | })
49 | .catch(function(error) {
50 | console.error(error);
51 | });
52 | } else if (this.state.following) {
53 | // unfollow playlist
54 | const spotifyApi = new Spotify();
55 | spotifyApi.setAccessToken(this.props.accessToken);
56 | spotifyApi
57 | .unfollowPlaylist(this.props.playlist.owner.id, this.props.playlist.id)
58 | .then(response => {
59 | if (response[0] == true) {
60 | this.setState({
61 | following: false
62 | });
63 | }
64 | })
65 | .catch(function(error) {
66 | console.error(error);
67 | });
68 | }
69 | this.setState({
70 | following: !this.state.following,
71 | beenChanged: true
72 | });
73 | }
74 |
75 | render() {
76 | //console.log(this.props);
77 | if (this.props.loggedIn && !this.state.beenChanged) {
78 | const spotifyApi = new Spotify();
79 | spotifyApi.setAccessToken(this.props.accessToken);
80 | spotifyApi
81 | .areFollowingPlaylist(
82 | this.props.playlist.owner.id,
83 | this.props.playlist.id,
84 | [this.props.userId]
85 | )
86 | .then(response => {
87 | if (response[0] == true) {
88 | this.setState({
89 | following: true
90 | });
91 | }
92 | })
93 | .catch(function(error) {
94 | console.error(error);
95 | });
96 | }
97 |
98 | return (
99 |
100 |
101 |
106 |
107 |
108 |
{this.props.playlist.name}
109 |
110 | {this.props.playlist.description || "description"}
111 |
112 |
113 | Following
119 | ) : (
120 | Follow
121 | )
122 | }
123 | onClick={() => this.onClickFollow()}
124 | >
125 | Follow
126 |
127 |
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | export default PlaylistDetailRow;
135 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/app/components/GenreSelector/GenreSelector.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import glamorous from 'glamorous';
3 | import Select from 'react-select';
4 | import 'react-select/dist/react-select.css';
5 | import '../../styles/select.css';
6 |
7 | const options = [
8 | { value: 'acoustic', label: 'acoustic'},
9 | { value: 'alt-rock', label: 'alt-rock'},
10 | { value: 'alternative', label: 'alternative'},
11 | { value: 'ambient', label: 'ambient'},
12 | { value: 'bluegrass', label: 'bluegrass'},
13 | { value: 'blues', label: 'blues'},
14 | { value: 'chill', label: 'chill'},
15 | { value: 'classical', label: 'classical'},
16 | { value: 'country', label: 'country'},
17 | { value: 'dance', label: 'dance'},
18 | { value: 'deep-house', label: 'deep-house'},
19 | { value: 'disco', label: 'disco'},
20 | { value: 'drum-and-bass', label: 'drum-and-bass'},
21 | { value: 'dubstep', label: 'dubstep'},
22 | { value: 'edm', label: 'edm'},
23 | { value: 'electro', label: 'electro'},
24 | { value: 'electronic', label: 'electronic'},
25 | { value: 'folk', label: 'folk'},
26 | { value: 'happy', label: 'happy'},
27 | { value: 'heavy-metal', label: 'heavy-metal'},
28 | { value: 'hip-hop', label: 'hip-hop'},
29 | { value: 'indie', label: 'indie'},
30 | { value: 'indie-pop', label: 'indie-pop'},
31 | { value: 'jazz', label: 'jazz'},
32 | { value: 'latin', label: 'latin'},
33 | { value: 'metal', label: 'metal'},
34 | { value: 'minimal-techno', label: 'minimal-techno'},
35 | { value: 'piano', label: 'piano'},
36 | { value: 'pop', label: 'pop'},
37 | { value: 'progressive-house', label: 'progressive-house'},
38 | { value: 'punk', label: 'punk'},
39 | { value: 'r-n-b', label: 'r-n-b'},
40 | { value: 'reggae', label: 'reggae'},
41 | { value: 'reggaeton', label: 'reggaeton'},
42 | { value: 'road-trip', label: 'road-trip'},
43 | { value: 'rock', label: 'rock'},
44 | { value: 'rock-n-roll', label: 'rock-n-roll'},
45 | { value: 'romance', label: 'romance'},
46 | { value: 'sad', label: 'sad'},
47 | { value: 'salsa', label: 'salsa'},
48 | { value: 'show-tunes', label: 'show-tunes'},
49 | { value: 'singer-songwriter', label: 'singer-songwriter'},
50 | { value: 'ska', label: 'ska'},
51 | { value: 'sleep', label: 'sleep'},
52 | { value: 'soul', label: 'soul'},
53 | { value: 'soundtracks', label: 'soundtracks'},
54 | { value: 'spanish', label: 'spanish'},
55 | { value: 'study', label: 'study'},
56 | { value: 'summer', label: 'summer'},
57 | { value: 'synth-pop', label: 'synth-pop'},
58 | { value: 'techno', label: 'techno'},
59 | { value: 'trance', label: 'trance'},
60 | { value: 'work-out', label: 'work-out'}
61 | ];
62 |
63 | class GenreSelector extends Component{
64 | constructor(props) {
65 | super(props);
66 | this.state = {
67 | stayOpen: false
68 | }
69 | }
70 |
71 | render() {
72 | const { stayOpen, disabled } = this.state;
73 | const { filterOn, seed_genres, onChange } = this.props;
74 | const RadioSelect = glamorous.span({
75 | color: filterOn ? "#27b7ff" : "#5e5a5a"
76 | });
77 | // if (disabled) {
78 | // let genreInput = document.querySelector('.Select-control');
79 | // genreInput.setAttribute('style', 'background-color: #191414 !important;');
80 | // } else {
81 | // let genreInput = document.querySelector('.Select-control');
82 | // genreInput.setAttribute('style', 'background-color: rgb(230,230,230) !important;');
83 | // };
84 |
85 | return (
86 |
87 |
88 | GENRES
89 |
90 |
91 |
102 |
103 |
104 |
105 | {seed_genres === '' ? 0 : seed_genres.split(',').length}
106 |
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 | };
115 |
116 | export default GenreSelector
--------------------------------------------------------------------------------
/src/app/javascripts/helpers.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 |
3 | // generate random string to prevent Cross Browser Script threats
4 | const generateRandomString = length => {
5 | var text = "";
6 | var possible =
7 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
8 |
9 | for (var i = 0; i < length; i++) {
10 | text += possible.charAt(Math.floor(Math.random() * possible.length));
11 | }
12 | return text;
13 | };
14 |
15 | // get object of query string hash params
16 | const getHashParams = () => {
17 | let hashParams = {};
18 | let e,
19 | r = /([^&;=]+)=?([^&;]*)/g,
20 | q = window.location.hash.substring(1);
21 | while ((e = r.exec(q))) {
22 | hashParams[e[1]] = decodeURIComponent(e[2]);
23 | }
24 | return hashParams;
25 | };
26 |
27 | const signOut = () => {
28 | window.location = "/";
29 | };
30 |
31 | const setLoginEventListener = () => {
32 | document.getElementById("login-button").addEventListener(
33 | "click",
34 | function() {
35 | const stateKey = "spotify_auth_state";
36 | const client_id = "c3ac28c1b26941b5a09beaa1d33240bd"; // Your client id
37 | let redirect_uri = "https://synthetic.netlify.com"; // Your redirect uri
38 |
39 | let state = generateRandomString();
40 |
41 | localStorage.setItem(stateKey, state);
42 | let scope = `user-read-private user-read-email user-library-read user-library-modify playlist-read-private playlist-read-collaborative playlist-modify-public playlist-modify-private`;
43 |
44 | let url = "https://accounts.spotify.com/authorize";
45 | url += "?response_type=token";
46 | url += "&client_id=" + encodeURIComponent(client_id);
47 | url += "&scope=" + encodeURIComponent(scope);
48 | url += "&redirect_uri=" + encodeURIComponent(redirect_uri);
49 | url += "&state=" + encodeURIComponent(state);
50 |
51 | window.location = url;
52 | },
53 | false
54 | );
55 | };
56 |
57 | const spotifyImplicitAuth = stateParameters => {
58 | var stateKey = "spotify_auth_state";
59 |
60 | var params = getHashParams();
61 |
62 | var access_token = params.access_token,
63 | state = params.state,
64 | storedState = localStorage.getItem(stateKey);
65 | if (
66 | access_token &&
67 | (state == null || state !== storedState) &&
68 | stateParameters.access_token
69 | ) {
70 | alert(
71 | "There was an error during the authentication, you may already be authenticated"
72 | );
73 | } else {
74 | localStorage.removeItem(stateKey);
75 | if (access_token) {
76 | $.ajax({
77 | url: "https://api.spotify.com/v1/me",
78 | headers: {
79 | Authorization: "Bearer " + access_token
80 | },
81 | success: function(response) {
82 | // userProfilePlaceholder.innerHTML = userProfileTemplate(response);
83 |
84 | $("#login").hide();
85 | $("#loggedin").show();
86 | }
87 | });
88 | } else {
89 | $("#login").show();
90 | $("#loggedin").hide();
91 | }
92 | }
93 | };
94 |
95 | const calcAndSort = (data, dataDetails, state) => {
96 | /* big huge function start */
97 | let calculatedData = [];
98 | let trackIds = [];
99 |
100 | // enter entire dataset loop for each song
101 | for (let i = 0; i < data.length; i++) {
102 | let songObj = data[i].track;
103 | let songDetails = dataDetails.find(songDetailSet => {
104 | return songDetailSet.id === songObj.id;
105 | });
106 |
107 | let trackId = songObj.id;
108 | trackIds.push(trackId);
109 |
110 | let trackDetails = songDetails;
111 |
112 | let trackEnergy = Math.round(trackDetails.energy * 100) || 0;
113 | let trackValence = Math.round(trackDetails.valence * 100) || 0;
114 | let trackAcousticness = Math.round(trackDetails.acousticness * 100) || 0;
115 | let trackDance = Math.round(trackDetails.danceability * 100) || 0;
116 | let trackVocalness =
117 | Math.abs(Math.round(trackDetails.instrumentalness * 100 - 100)) || 0;
118 | let trackPopularity = Math.abs(Math.round(songObj.popularity));
119 |
120 | let differenceEnergy = 0;
121 | let differenceValence = 0;
122 | let differenceAcousticness = 0;
123 | let differenceDance = 0;
124 | let differenceVocalness = 0;
125 | let differencePopularity = 0;
126 | if (state.filterBy.energy) {
127 | differenceEnergy = Math.abs(trackEnergy - state.energyValue);
128 | }
129 | if (state.filterBy.valence) {
130 | differenceValence = Math.abs(trackValence - state.valenceValue);
131 | }
132 | if (state.filterBy.acoustic) {
133 | differenceAcousticness = Math.abs(
134 | trackAcousticness - state.acousticValue
135 | );
136 | }
137 | if (state.filterBy.dance) {
138 | differenceDance = Math.abs(trackDance - state.danceValue);
139 | }
140 | if (state.filterBy.vocalness) {
141 | differenceVocalness = Math.abs(trackVocalness - state.vocalnessValue);
142 | }
143 | if (state.filterBy.popularity) {
144 | differencePopularity = Math.abs(trackPopularity - state.popularityValue);
145 | }
146 | let totalDifference =
147 | differenceEnergy +
148 | differenceValence +
149 | differenceAcousticness +
150 | differenceDance +
151 | differenceVocalness +
152 | differencePopularity;
153 | songObj["ResultDifference"] = totalDifference;
154 | if (songObj.preview_url !== null) {
155 | calculatedData.push(songObj);
156 | }
157 | }
158 |
159 | console.log(calculatedData);
160 | // sort by the absolute value of the subtracted entered user amount for each value and resort by that value
161 | calculatedData.sort(function(a, b) {
162 | return a.ResultDifference - b.ResultDifference;
163 | });
164 | console.log(calculatedData);
165 |
166 | return calculatedData;
167 | };
168 |
169 | export {
170 | calcAndSort,
171 | generateRandomString,
172 | getHashParams,
173 | signOut,
174 | setLoginEventListener,
175 | spotifyImplicitAuth
176 | };
177 |
--------------------------------------------------------------------------------
/src/app/components/Instructions/HowItWorks.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import glamorous from "glamorous";
3 | import "../../styles/how-it-works.css";
4 | import PlaylistDetails from "../PlaylistDetails/PlaylistDetails";
5 |
6 | const BulletPoint = glamorous.span({
7 | color: "#ccc",
8 | lineHeight: 1.5
9 | });
10 | const BulletLine = glamorous.div({
11 | marginTop: 10,
12 | textAlign: "center"
13 | });
14 |
15 | class HowItWorks extends Component {
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | How it Works
26 |
27 |
28 |
29 | Spotify analyzes every song in its library and uses that data to
30 | give better recommendations to you. Synthetic gives you a finer
31 | tuned control over just what you find based on song features you
32 | like. Each song is scored from 0-600 (600 being the highest) and
33 | the most closely related songs are returned in 30 second previews
34 | so you can go through more songs faster.
35 |
36 |
37 | Adjust the levels of filters on different musical attributes from
38 | a specified musical selection. Calculated results are sorted and
39 | queued in the player based off of the difference between the input
40 | values and each track's actual aggregate statistics. Preview 30
41 | second snippets from the result set and add them to your Spotify
42 | library by logging in. Filter by one or more of these metrics:
43 |
44 |
45 | Energy a measure of intensity/activity
46 | based off of dynamic range, loudness, timbre, etc.
47 |
48 |
49 | Valence the musical positiveness
50 | conveyed by a track, higher valence tracks sound happier
51 |
52 |
53 | Acousticness a confidence measure of
54 | acousticness of a track, a higher number represents a higher
55 | confidence
56 |
57 |
58 | Danceability how suitable a track is
59 | for dancing, based off of tempo, rhythm stability, and beat
60 | strength
61 |
62 |
63 | Popularity popularity of a track, based
64 | off the total number of plays and how recent those plays are
65 |
66 |
67 | Vocalness how present vocals are in a
68 | track, a lower level represents a track that is more instrumental
69 |
70 |
71 | Genre only available after logging in
72 | and selecting the Spotify Library, choose 1-3 genres to find songs
73 | that correspond
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | Tips for Usage
84 |
85 |
86 |
87 | Experiment with different sets of filters on various playlists.
88 | Use fewer filters to get more defined results from the playlist
89 | you are searching. Extend the music you find by searching by genre
90 | across Spotify's entire library with the Spotify Library selection
91 | in the arrow menu. Try out some of these ideas to get started:
92 |
93 |
94 | Upbeat Hipster cycle through the
95 | playlists to find the Hipster Synthpop playlist, and calculate
96 | using the energy filter at a value of 100 and turn off all other
97 | filters by clicking the blue buttons next to each filter's row.
98 |
99 |
100 | Bass Heavy Dance use the Subwoofer
101 | Soundbombs playlist and set the acoustic filter at 0, and the
102 | dance filter at 100. Turn off all other filters.
103 |
104 |
105 | Underground Folk use the Chill Folk
106 | playlist and turn down popularity below 20 and mix in other
107 | filters for your own style of soft songs.
108 |
109 |
110 | Intense Work Out login with a Spotify
111 | account, find the Spotify Library selection in the arrow menu and
112 | filter by high energy and vocalness values. Choose work-out from
113 | the genre dropdown and preview the results.
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | Feature Playlists and Libraries
124 |
125 |
126 |
127 | Find music from one of two libraries (Spotify Library: 30+ million
128 | songs, Synthetic Libray: 600+ songs) or one of many featured
129 | playlists (50-100 songs), from genres the playlists are named
130 | after. Choose from one of the following:
131 |
132 |
133 | Playlist curated lists of specific
134 | moods and genres for more narrowed music discovery. Playlists are
135 | updated regularly and contain a variety of recently released
136 | handselected songs.
137 |
138 |
139 | Library contain exponentially more
140 | songs for larger result sets and greater scope, available
141 | libraries are the Synthetic library and the Spotify library. To
142 | facilitate searching the Spotify Library you can filter by genre
143 | after logging in.
144 |
145 |
146 |
153 |
154 | {/*
*/}
159 |
160 | );
161 | }
162 | }
163 |
164 | export default HowItWorks;
165 |
--------------------------------------------------------------------------------
/src/app/components/Slider/Slider.js:
--------------------------------------------------------------------------------
1 | /* eslint no-debugger: "warn" */
2 | import cx from "classnames";
3 | import React, { Component } from "react";
4 | import PropTypes from "prop-types";
5 | import ResizeObserver from "resize-observer-polyfill";
6 | import { capitalize, clamp } from "./utils";
7 |
8 | /**
9 | * Predefined constants
10 | * @type {Object}
11 | */
12 | const constants = {
13 | orientation: {
14 | horizontal: {
15 | dimension: "width",
16 | direction: "left",
17 | reverseDirection: "right",
18 | coordinate: "x"
19 | },
20 | vertical: {
21 | dimension: "height",
22 | direction: "top",
23 | reverseDirection: "bottom",
24 | coordinate: "y"
25 | }
26 | }
27 | };
28 |
29 | class Slider extends Component {
30 | static propTypes = {
31 | min: PropTypes.number,
32 | max: PropTypes.number,
33 | step: PropTypes.number,
34 | value: PropTypes.number,
35 | orientation: PropTypes.string,
36 | tooltip: PropTypes.bool,
37 | reverse: PropTypes.bool,
38 | labels: PropTypes.object,
39 | handleLabel: PropTypes.string,
40 | format: PropTypes.func,
41 | onChangeStart: PropTypes.func,
42 | onChange: PropTypes.func,
43 | onChangeComplete: PropTypes.func
44 | };
45 |
46 | static defaultProps = {
47 | min: 0,
48 | max: 100,
49 | step: 1,
50 | value: 0,
51 | orientation: "horizontal",
52 | tooltip: true,
53 | reverse: false,
54 | labels: {},
55 | handleLabel: ""
56 | };
57 |
58 | constructor(props, context) {
59 | super(props, context);
60 |
61 | this.state = {
62 | active: false,
63 | limit: 0,
64 | grab: 0
65 | };
66 | }
67 |
68 | componentDidMount() {
69 | this.handleUpdate();
70 | const resizeObserver = new ResizeObserver(this.handleUpdate);
71 | resizeObserver.observe(this.slider);
72 | }
73 |
74 | /**
75 | * Format label/tooltip value
76 | * @param {Number} - value
77 | * @return {Formatted Number}
78 | */
79 | handleFormat = value => {
80 | const { format } = this.props;
81 | return format ? format(value) : value;
82 | };
83 |
84 | /**
85 | * Update slider state on change
86 | * @return {void}
87 | */
88 | handleUpdate = () => {
89 | if (!this.slider) {
90 | // for shallow rendering
91 | return;
92 | }
93 | const { orientation } = this.props;
94 | const dimension = capitalize(constants.orientation[orientation].dimension);
95 | const sliderPos = this.slider[`offset${dimension}`];
96 | const handlePos = this.handle[`offset${dimension}`];
97 |
98 | this.setState({
99 | limit: sliderPos - handlePos,
100 | grab: handlePos / 2
101 | });
102 | };
103 |
104 | /**
105 | * Attach event listeners to mousemove/mouseup events
106 | * @return {void}
107 | */
108 | handleStart = e => {
109 | const { onChangeStart } = this.props;
110 | document.addEventListener("mousemove", this.handleDrag);
111 | document.addEventListener("mouseup", this.handleEnd);
112 | this.setState(
113 | {
114 | active: true
115 | },
116 | () => {
117 | onChangeStart && onChangeStart(e);
118 | }
119 | );
120 | };
121 |
122 | /**
123 | * Handle drag/mousemove event
124 | * @param {Object} e - Event object
125 | * @return {void}
126 | */
127 | handleDrag = e => {
128 | e.stopPropagation();
129 | const { onChange } = this.props;
130 | const { target: { className, classList, dataset } } = e;
131 | if (!onChange || className === "rangeslider__labels") return;
132 |
133 | let value = this.position(e);
134 |
135 | if (
136 | classList &&
137 | classList.contains("rangeslider__label-item") &&
138 | dataset.value
139 | ) {
140 | value = parseFloat(dataset.value);
141 | }
142 |
143 | onChange && onChange(value, e);
144 | };
145 |
146 | /**
147 | * Detach event listeners to mousemove/mouseup events
148 | * @return {void}
149 | */
150 | handleEnd = e => {
151 | const { onChangeComplete } = this.props;
152 | this.setState(
153 | {
154 | active: false
155 | },
156 | () => {
157 | onChangeComplete && onChangeComplete(e);
158 | }
159 | );
160 | document.removeEventListener("mousemove", this.handleDrag);
161 | document.removeEventListener("mouseup", this.handleEnd);
162 | };
163 |
164 | /**
165 | * Support for key events on the slider handle
166 | * @param {Object} e - Event object
167 | * @return {void}
168 | */
169 | handleKeyDown = e => {
170 | e.preventDefault();
171 | const { keyCode } = e;
172 | const { value, min, max, step, onChange } = this.props;
173 | let sliderValue;
174 |
175 | switch (keyCode) {
176 | case 38:
177 | case 39:
178 | sliderValue = value + step > max ? max : value + step;
179 | onChange && onChange(sliderValue, e);
180 | break;
181 | case 37:
182 | case 40:
183 | sliderValue = value - step < min ? min : value - step;
184 | onChange && onChange(sliderValue, e);
185 | break;
186 | default:
187 | break;
188 | }
189 | };
190 |
191 | /**
192 | * Calculate position of slider based on its value
193 | * @param {number} value - Current value of slider
194 | * @return {position} pos - Calculated position of slider based on value
195 | */
196 | getPositionFromValue = value => {
197 | const { limit } = this.state;
198 | const { min, max } = this.props;
199 | const diffMaxMin = max - min;
200 | const diffValMin = value - min;
201 | const percentage = diffValMin / diffMaxMin;
202 | const pos = Math.round(percentage * limit);
203 |
204 | return pos;
205 | };
206 |
207 | /**
208 | * Translate position of slider to slider value
209 | * @param {number} pos - Current position/coordinates of slider
210 | * @return {number} value - Slider value
211 | */
212 | getValueFromPosition = pos => {
213 | const { limit } = this.state;
214 | const { orientation, min, max, step } = this.props;
215 | const percentage = clamp(pos, 0, limit) / (limit || 1);
216 | const baseVal = step * Math.round(percentage * (max - min) / step);
217 | const value = orientation === "horizontal" ? baseVal + min : max - baseVal;
218 |
219 | return clamp(value, min, max);
220 | };
221 |
222 | /**
223 | * Calculate position of slider based on value
224 | * @param {Object} e - Event object
225 | * @return {number} value - Slider value
226 | */
227 | position = e => {
228 | const { grab } = this.state;
229 | const { orientation, reverse } = this.props;
230 |
231 | const node = this.slider;
232 | const coordinateStyle = constants.orientation[orientation].coordinate;
233 | const directionStyle = reverse
234 | ? constants.orientation[orientation].reverseDirection
235 | : constants.orientation[orientation].direction;
236 | const clientCoordinateStyle = `client${capitalize(coordinateStyle)}`;
237 | const coordinate = !e.touches
238 | ? e[clientCoordinateStyle]
239 | : e.touches[0][clientCoordinateStyle];
240 | const direction = node.getBoundingClientRect()[directionStyle];
241 | const pos = reverse
242 | ? direction - coordinate - grab
243 | : coordinate - direction - grab;
244 | const value = this.getValueFromPosition(pos);
245 |
246 | return value;
247 | };
248 |
249 | /**
250 | * Grab coordinates of slider
251 | * @param {Object} pos - Position object
252 | * @return {Object} - Slider fill/handle coordinates
253 | */
254 | coordinates = pos => {
255 | const { limit, grab } = this.state;
256 | const { orientation } = this.props;
257 | const value = this.getValueFromPosition(pos);
258 | const position = this.getPositionFromValue(value);
259 | const handlePos = orientation === "horizontal" ? position + grab : position;
260 | const fillPos =
261 | orientation === "horizontal" ? handlePos : limit - handlePos;
262 |
263 | return {
264 | fill: fillPos,
265 | handle: handlePos,
266 | label: handlePos
267 | };
268 | };
269 |
270 | renderLabels = labels => (
271 | {
273 | this.labels = sl;
274 | }}
275 | className={cx("rangeslider__labels")}
276 | >
277 | {labels}
278 |
279 | );
280 |
281 | render() {
282 | const dontShowToolTip = false; // change to true to re-enable tooltip
283 | const {
284 | value,
285 | orientation,
286 | className,
287 | tooltip,
288 | reverse,
289 | labels,
290 | min,
291 | max,
292 | handleLabel
293 | } = this.props;
294 | const { active } = this.state;
295 | const dimension = constants.orientation[orientation].dimension;
296 | const direction = reverse
297 | ? constants.orientation[orientation].reverseDirection
298 | : constants.orientation[orientation].direction;
299 | const position = this.getPositionFromValue(value);
300 | const coords = this.coordinates(position);
301 | const fillStyle = {
302 | [dimension]: `${coords.fill}px`,
303 | background: this.props.filterOn
304 | ? "linear-gradient(to left, #27b7ff, #70D5FF)"
305 | : "linear-gradient(to left, #777, #6a6a6a)",
306 | filter: `brightness(${value * 0.2 + 80}%) opacity(${value * 0.25 + 75}%)`
307 | };
308 | const handleStyle = { [direction]: `${coords.handle}px` };
309 | let showTooltip = tooltip && active;
310 |
311 | let labelItems = [];
312 | let labelKeys = Object.keys(labels);
313 |
314 | if (labelKeys.length > 0) {
315 | labelKeys = labelKeys.sort((a, b) => (reverse ? a - b : b - a));
316 |
317 | for (let key of labelKeys) {
318 | const labelPosition = this.getPositionFromValue(key);
319 | const labelCoords = this.coordinates(labelPosition);
320 | const labelStyle = { [direction]: `${labelCoords.label}px` };
321 |
322 | labelItems.push(
323 |
332 | {this.props.labels[key]}
333 |
334 | );
335 | }
336 | }
337 |
338 | return (
339 | {
341 | this.slider = s;
342 | }}
343 | className={cx(
344 | "rangeslider",
345 | `rangeslider-${orientation}`,
346 | { "rangeslider-reverse": reverse },
347 | className
348 | )}
349 | onMouseDown={this.handleDrag}
350 | onMouseUp={this.handleEnd}
351 | onTouchStart={this.handleStart}
352 | onTouchEnd={this.handleEnd}
353 | aria-valuemin={min}
354 | aria-valuemax={max}
355 | aria-valuenow={value}
356 | aria-orientation={orientation}
357 | >
358 |
359 |
{
361 | this.handle = sh;
362 | }}
363 | className="rangeslider__handle"
364 | onMouseDown={this.handleStart}
365 | onTouchMove={this.handleDrag}
366 | onTouchEnd={this.handleEnd}
367 | onKeyDown={this.handleKeyDown}
368 | style={handleStyle}
369 | tabIndex={0}
370 | >
371 | {showTooltip && dontShowToolTip ? (
372 |
{
374 | this.tooltip = st;
375 | }}
376 | className="rangeslider__handle-tooltip"
377 | >
378 | {this.handleFormat(value)}
379 |
380 | ) : null}
381 |
{handleLabel}
382 |
383 | {labels ? this.renderLabels(labelItems) : null}
384 |
385 | );
386 | }
387 | }
388 |
389 | export default Slider;
390 |
--------------------------------------------------------------------------------
/src/app/components/PlaylistSelector/playlistsList.js:
--------------------------------------------------------------------------------
1 | const playlists = [
2 | {
3 | collaborative: false,
4 | external_urls: {
5 | spotify:
6 | "http://open.spotify.com/user/12791885/playlist/6EPVmZXNHsih4SLfL1v1id"
7 | },
8 | href:
9 | "https://api.spotify.com/v1/users/12791885/playlists/6EPVmZXNHsih4SLfL1v1id",
10 | id: "6EPVmZXNHsih4SLfL1v1id",
11 | images: [
12 | {
13 | height: null,
14 | url:
15 | "https://pl.scdn.co/images/pl/default/d09ecae36e5508c79cd5af04d0a07e3918a5e89e",
16 | width: null
17 | }
18 | ],
19 | name: "Synthetic Library",
20 | owner: {
21 | display_name: "Kyle Gill",
22 | external_urls: {
23 | spotify: "http://open.spotify.com/user/12791885"
24 | },
25 | href: "https://api.spotify.com/v1/users/12791885",
26 | id: "12791885",
27 | type: "user",
28 | uri: "spotify:user:12791885"
29 | },
30 | public: true,
31 | description:
32 | "Around 600 songs representing all songs in playlists featured in Synthetic's music selection. ",
33 | snapshot_id:
34 | "s+ROshtOXkjk43dNo4y/t69EFo6SGHbnHPn3MnG8cOMnJ96J9ffYYna2gsvWrE2H",
35 | tracks: {
36 | href:
37 | "https://api.spotify.com/v1/users/12791885/playlists/6EPVmZXNHsih4SLfL1v1id/tracks",
38 | total: 601
39 | },
40 | type: "playlist",
41 | uri: "spotify:user:12791885:playlist:6EPVmZXNHsih4SLfL1v1id"
42 | },
43 | {
44 | collaborative: false,
45 | external_urls: {
46 | spotify:
47 | "http://open.spotify.com/user/12791885/playlist/74Q0BTo3bq2Q9OFbrNZzgd"
48 | },
49 | href:
50 | "https://api.spotify.com/v1/users/12791885/playlists/74Q0BTo3bq2Q9OFbrNZzgd",
51 | id: "74Q0BTo3bq2Q9OFbrNZzgd",
52 | images: [
53 | {
54 | height: null,
55 | url:
56 | "https://pl.scdn.co/images/pl/default/6989f373947e28a95f3feebe27cfe4cef122b0e7",
57 | width: null
58 | }
59 | ],
60 | name: "Indietronic",
61 | owner: {
62 | display_name: "Kyle Gill",
63 | external_urls: {
64 | spotify: "http://open.spotify.com/user/12791885"
65 | },
66 | href: "https://api.spotify.com/v1/users/12791885",
67 | id: "12791885",
68 | type: "user",
69 | uri: "spotify:user:12791885"
70 | },
71 | public: true,
72 | description:
73 | "Alternative indie style music with electronic sounds and vocals.",
74 | snapshot_id:
75 | "dxGb2p0Z8Urnuur9PEJ5Xnhc5WZnpijoCbSzgoL13e1ppKsDP93OXzRFZVo+SQvf",
76 | tracks: {
77 | href:
78 | "https://api.spotify.com/v1/users/12791885/playlists/74Q0BTo3bq2Q9OFbrNZzgd/tracks",
79 | total: 83
80 | },
81 | type: "playlist",
82 | uri: "spotify:user:12791885:playlist:74Q0BTo3bq2Q9OFbrNZzgd"
83 | },
84 | {
85 | collaborative: false,
86 | external_urls: {
87 | spotify:
88 | "http://open.spotify.com/user/12791885/playlist/7EchTyXNRFZ0tDnKK4hheM"
89 | },
90 | href:
91 | "https://api.spotify.com/v1/users/12791885/playlists/7EchTyXNRFZ0tDnKK4hheM",
92 | id: "7EchTyXNRFZ0tDnKK4hheM",
93 | images: [
94 | {
95 | height: null,
96 | url:
97 | "https://pl.scdn.co/images/pl/default/0db53f9796758315d22dd41549f94519c54bbfde",
98 | width: null
99 | }
100 | ],
101 | name: "Hipster Synthpop",
102 | owner: {
103 | display_name: "Kyle Gill",
104 | external_urls: {
105 | spotify: "http://open.spotify.com/user/12791885"
106 | },
107 | href: "https://api.spotify.com/v1/users/12791885",
108 | id: "12791885",
109 | type: "user",
110 | uri: "spotify:user:12791885"
111 | },
112 | public: true,
113 | description:
114 | "Upbeat, poppy synthesizers laid over vocals from that one band you've probably never heard of.",
115 | snapshot_id:
116 | "CEzUxhwl/g9d2hU8VTVUEcMB7K2PYUerVVskF50mxmR5977imSwDKwJw/8k3wVSk",
117 | tracks: {
118 | href:
119 | "https://api.spotify.com/v1/users/12791885/playlists/7EchTyXNRFZ0tDnKK4hheM/tracks",
120 | total: 57
121 | },
122 | type: "playlist",
123 | uri: "spotify:user:12791885:playlist:7EchTyXNRFZ0tDnKK4hheM"
124 | },
125 | {
126 | collaborative: false,
127 | external_urls: {
128 | spotify:
129 | "http://open.spotify.com/user/12791885/playlist/6uMZnO749RnThgbW0EoxJI"
130 | },
131 | href:
132 | "https://api.spotify.com/v1/users/12791885/playlists/6uMZnO749RnThgbW0EoxJI",
133 | id: "6uMZnO749RnThgbW0EoxJI",
134 | images: [
135 | {
136 | height: null,
137 | url:
138 | "https://pl.scdn.co/images/pl/default/036129a69afc7628e7f9c03b7e99efc6f1b1a61e",
139 | width: null
140 | }
141 | ],
142 | name: "Absolute Focus",
143 | owner: {
144 | display_name: "Kyle Gill",
145 | external_urls: {
146 | spotify: "http://open.spotify.com/user/12791885"
147 | },
148 | href: "https://api.spotify.com/v1/users/12791885",
149 | id: "12791885",
150 | type: "user",
151 | uri: "spotify:user:12791885"
152 | },
153 | public: true,
154 | description:
155 | "Music to study, think, or unwind to. Just fast enough to keep you moving, and slow enough to keep you from getting distracted.",
156 | snapshot_id:
157 | "03OfB5md6CkA1iyjajbWgTlmDCkKP8KTmCsLrNmXg0LTnf9NDi/8bOkQ3BlzOA0L",
158 | tracks: {
159 | href:
160 | "https://api.spotify.com/v1/users/12791885/playlists/6uMZnO749RnThgbW0EoxJI/tracks",
161 | total: 97
162 | },
163 | type: "playlist",
164 | uri: "spotify:user:12791885:playlist:6uMZnO749RnThgbW0EoxJI"
165 | },
166 | {
167 | collaborative: false,
168 | external_urls: {
169 | spotify:
170 | "http://open.spotify.com/user/12791885/playlist/1R78QE70QDwK6LGH69yQQ3"
171 | },
172 | href:
173 | "https://api.spotify.com/v1/users/12791885/playlists/1R78QE70QDwK6LGH69yQQ3",
174 | id: "1R78QE70QDwK6LGH69yQQ3",
175 | images: [
176 | {
177 | height: null,
178 | url:
179 | "https://pl.scdn.co/images/pl/default/bef2eebcce27e9778777ccf1a612997dc87c397c",
180 | width: null
181 | }
182 | ],
183 | name: "Tropical Vibes",
184 | owner: {
185 | display_name: "Kyle Gill",
186 | external_urls: {
187 | spotify: "http://open.spotify.com/user/12791885"
188 | },
189 | href: "https://api.spotify.com/v1/users/12791885",
190 | id: "12791885",
191 | type: "user",
192 | uri: "spotify:user:12791885"
193 | },
194 | public: true,
195 | description:
196 | "Poolside tunes with pan flutes and poppy synths and pianos for summer days at the beach.",
197 | snapshot_id:
198 | "e2ZjrWV4XOL4R1uU+zTeMxaIXhTSSbSZ7FwuaX6pNyB/0o3crD1IRlzNg1WUNDHT",
199 | tracks: {
200 | href:
201 | "https://api.spotify.com/v1/users/12791885/playlists/1R78QE70QDwK6LGH69yQQ3/tracks",
202 | total: 54
203 | },
204 | type: "playlist",
205 | uri: "spotify:user:12791885:playlist:1R78QE70QDwK6LGH69yQQ3"
206 | },
207 | {
208 | collaborative: false,
209 | external_urls: {
210 | spotify:
211 | "http://open.spotify.com/user/12791885/playlist/5iwozGT2hvU6DpriMZf6v1"
212 | },
213 | href:
214 | "https://api.spotify.com/v1/users/12791885/playlists/5iwozGT2hvU6DpriMZf6v1",
215 | id: "5iwozGT2hvU6DpriMZf6v1",
216 | images: [
217 | {
218 | height: null,
219 | url:
220 | "https://pl.scdn.co/images/pl/default/0425bd5770595ffa855d76d85ee08be428c3532c",
221 | width: null
222 | }
223 | ],
224 | name: "Experimental Sounds",
225 | owner: {
226 | display_name: "Kyle Gill",
227 | external_urls: {
228 | spotify: "http://open.spotify.com/user/12791885"
229 | },
230 | href: "https://api.spotify.com/v1/users/12791885",
231 | id: "12791885",
232 | type: "user",
233 | uri: "spotify:user:12791885"
234 | },
235 | public: true,
236 | description:
237 | "Avant garde experimentations in sound design or musical style.",
238 | snapshot_id:
239 | "i8uae8T6pM0BUKuR7vxqu6JYT51lfu+L1/Ppyx1BHldfE8KBIUBw90O/6cXSs1Yj",
240 | tracks: {
241 | href:
242 | "https://api.spotify.com/v1/users/12791885/playlists/5iwozGT2hvU6DpriMZf6v1/tracks",
243 | total: 51
244 | },
245 | type: "playlist",
246 | uri: "spotify:user:12791885:playlist:5iwozGT2hvU6DpriMZf6v1"
247 | },
248 | {
249 | collaborative: false,
250 | external_urls: {
251 | spotify:
252 | "http://open.spotify.com/user/12791885/playlist/2rT1j9dETap8pXT0TCZLXO"
253 | },
254 | href:
255 | "https://api.spotify.com/v1/users/12791885/playlists/2rT1j9dETap8pXT0TCZLXO",
256 | id: "2rT1j9dETap8pXT0TCZLXO",
257 | images: [
258 | {
259 | height: null,
260 | url:
261 | "https://pl.scdn.co/images/pl/default/66ff3f9f4319d74594ac8313b60a37832b327cc0",
262 | width: null
263 | }
264 | ],
265 | name: "Chill Acoustic",
266 | owner: {
267 | display_name: "Kyle Gill",
268 | external_urls: {
269 | spotify: "http://open.spotify.com/user/12791885"
270 | },
271 | href: "https://api.spotify.com/v1/users/12791885",
272 | id: "12791885",
273 | type: "user",
274 | uri: "spotify:user:12791885"
275 | },
276 | public: true,
277 | description:
278 | "Soft guitars, light vocals, or easy-going folk music for a mellower mood.",
279 | snapshot_id:
280 | "pCmg5tK287cmhA1SsfqRc64ZqwcBma5JVTD39y4EBxwrHON6Y2Rz9SCEsgj/X6OX",
281 | tracks: {
282 | href:
283 | "https://api.spotify.com/v1/users/12791885/playlists/2rT1j9dETap8pXT0TCZLXO/tracks",
284 | total: 58
285 | },
286 | type: "playlist",
287 | uri: "spotify:user:12791885:playlist:2rT1j9dETap8pXT0TCZLXO"
288 | },
289 | {
290 | collaborative: false,
291 | external_urls: {
292 | spotify:
293 | "http://open.spotify.com/user/12791885/playlist/15TJo6ExKMFSua5fovNrLx"
294 | },
295 | href:
296 | "https://api.spotify.com/v1/users/12791885/playlists/15TJo6ExKMFSua5fovNrLx",
297 | id: "15TJo6ExKMFSua5fovNrLx",
298 | images: [
299 | {
300 | height: null,
301 | url:
302 | "https://pl.scdn.co/images/pl/default/fa24cd761cfb6d84f428c176806c803dafd7b343",
303 | width: null
304 | }
305 | ],
306 | name: "Subwoofer Soundbombs",
307 | owner: {
308 | display_name: "Kyle Gill",
309 | external_urls: {
310 | spotify: "http://open.spotify.com/user/12791885"
311 | },
312 | href: "https://api.spotify.com/v1/users/12791885",
313 | id: "12791885",
314 | type: "user",
315 | uri: "spotify:user:12791885"
316 | },
317 | public: true,
318 | description:
319 | "Songs that have at least one sound to totally blow up your speakers, settling in the low end (50-250 Hz).",
320 | snapshot_id:
321 | "SN4oatClysxh+e1hHtSykA3Z5nf/oZrDEb/9rTSu9AxwAZKD0gLP2c0rLVbciTWO",
322 | tracks: {
323 | href:
324 | "https://api.spotify.com/v1/users/12791885/playlists/15TJo6ExKMFSua5fovNrLx/tracks",
325 | total: 71
326 | },
327 | type: "playlist",
328 | uri: "spotify:user:12791885:playlist:15TJo6ExKMFSua5fovNrLx"
329 | },
330 | {
331 | collaborative: false,
332 | external_urls: {
333 | spotify:
334 | "http://open.spotify.com/user/12791885/playlist/0AErb4bqbLBqYQwssGxdRX"
335 | },
336 | href:
337 | "https://api.spotify.com/v1/users/12791885/playlists/0AErb4bqbLBqYQwssGxdRX",
338 | id: "0AErb4bqbLBqYQwssGxdRX",
339 | images: [
340 | {
341 | height: null,
342 | url:
343 | "https://pl.scdn.co/images/pl/default/1a6873ae592ca0692a2e98010ba6329618b04916",
344 | width: null
345 | }
346 | ],
347 | name: "Epic Cinematic",
348 | owner: {
349 | display_name: "Kyle Gill",
350 | external_urls: {
351 | spotify: "http://open.spotify.com/user/12791885"
352 | },
353 | href: "https://api.spotify.com/v1/users/12791885",
354 | id: "12791885",
355 | type: "user",
356 | uri: "spotify:user:12791885"
357 | },
358 | public: true,
359 | description:
360 | "Dramatic soundtracks and pulse-pounding songs that sound wide and powerful.",
361 | snapshot_id:
362 | "J/k5/x3RhBi8glqmmIF9sLjhwgS+E6NFgPu0zF4QrNfQKdMPJ05rXygp7ad7+ygR",
363 | tracks: {
364 | href:
365 | "https://api.spotify.com/v1/users/12791885/playlists/0AErb4bqbLBqYQwssGxdRX/tracks",
366 | total: 62
367 | },
368 | type: "playlist",
369 | uri: "spotify:user:12791885:playlist:0AErb4bqbLBqYQwssGxdRX"
370 | },
371 | {
372 | collaborative: false,
373 | external_urls: {
374 | spotify:
375 | "http://open.spotify.com/user/12791885/playlist/0woQ1bUsluJ9DGkI7sFTyN"
376 | },
377 | href:
378 | "https://api.spotify.com/v1/users/12791885/playlists/0woQ1bUsluJ9DGkI7sFTyN",
379 | id: "0woQ1bUsluJ9DGkI7sFTyN",
380 | images: [
381 | {
382 | height: null,
383 | url:
384 | "https://pl.scdn.co/images/pl/default/28ef5ec0a0da2479cc8a2e55195633d41d52dfce",
385 | width: null
386 | }
387 | ],
388 | name: "Hybrid Rap",
389 | owner: {
390 | display_name: "Kyle Gill",
391 | external_urls: {
392 | spotify: "http://open.spotify.com/user/12791885"
393 | },
394 | href: "https://api.spotify.com/v1/users/12791885",
395 | id: "12791885",
396 | type: "user",
397 | uri: "spotify:user:12791885"
398 | },
399 | public: true,
400 | description:
401 | "Crossover hip-hop with different producers that have glitchy beats and solid raps.",
402 | snapshot_id:
403 | "zT5CbqH2PYnivseZ61pJUIw+Pnp6j8mk1r0R3Q1n2tZ6SfIJzLhQ6grFosUE5T9a",
404 | tracks: {
405 | href:
406 | "https://api.spotify.com/v1/users/12791885/playlists/0woQ1bUsluJ9DGkI7sFTyN/tracks",
407 | total: 34
408 | },
409 | type: "playlist",
410 | uri: "spotify:user:12791885:playlist:0woQ1bUsluJ9DGkI7sFTyN"
411 | }
412 | ];
413 |
414 | export default playlists;
415 |
--------------------------------------------------------------------------------
/src/app/content/playlist-details/hybrid-rap-details.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "danceability": 0.807,
4 | "energy": 0.774,
5 | "key": 6,
6 | "loudness": -4.099,
7 | "mode": 0,
8 | "speechiness": 0.0878,
9 | "acousticness": 0.00452,
10 | "instrumentalness": 0.0111,
11 | "liveness": 0.173,
12 | "valence": 0.614,
13 | "tempo": 98.99,
14 | "type": "audio_features",
15 | "id": "56Fr00rfuNzcmv8Z0ccIz5",
16 | "uri": "spotify:track:56Fr00rfuNzcmv8Z0ccIz5",
17 | "track_href": "https://api.spotify.com/v1/tracks/56Fr00rfuNzcmv8Z0ccIz5",
18 | "analysis_url":
19 | "https://api.spotify.com/v1/audio-analysis/56Fr00rfuNzcmv8Z0ccIz5",
20 | "duration_ms": 201943,
21 | "time_signature": 4
22 | },
23 | {
24 | "danceability": 0.383,
25 | "energy": 0.621,
26 | "key": 6,
27 | "loudness": -5.878,
28 | "mode": 0,
29 | "speechiness": 0.113,
30 | "acousticness": 0.0272,
31 | "instrumentalness": 0,
32 | "liveness": 0.427,
33 | "valence": 0.213,
34 | "tempo": 80.051,
35 | "type": "audio_features",
36 | "id": "2oJInAFXiQYLgz0jh9MrXf",
37 | "uri": "spotify:track:2oJInAFXiQYLgz0jh9MrXf",
38 | "track_href": "https://api.spotify.com/v1/tracks/2oJInAFXiQYLgz0jh9MrXf",
39 | "analysis_url":
40 | "https://api.spotify.com/v1/audio-analysis/2oJInAFXiQYLgz0jh9MrXf",
41 | "duration_ms": 181907,
42 | "time_signature": 4
43 | },
44 | {
45 | "danceability": 0.312,
46 | "energy": 0.796,
47 | "key": 11,
48 | "loudness": -5.8,
49 | "mode": 1,
50 | "speechiness": 0.0945,
51 | "acousticness": 0.0255,
52 | "instrumentalness": 0.00111,
53 | "liveness": 0.185,
54 | "valence": 0.28,
55 | "tempo": 106.221,
56 | "type": "audio_features",
57 | "id": "1elrRWRsxxN4vdOQXcP7Wz",
58 | "uri": "spotify:track:1elrRWRsxxN4vdOQXcP7Wz",
59 | "track_href": "https://api.spotify.com/v1/tracks/1elrRWRsxxN4vdOQXcP7Wz",
60 | "analysis_url":
61 | "https://api.spotify.com/v1/audio-analysis/1elrRWRsxxN4vdOQXcP7Wz",
62 | "duration_ms": 191144,
63 | "time_signature": 4
64 | },
65 | {
66 | "danceability": 0.754,
67 | "energy": 0.927,
68 | "key": 11,
69 | "loudness": -4.818,
70 | "mode": 0,
71 | "speechiness": 0.534,
72 | "acousticness": 0.0234,
73 | "instrumentalness": 0.00000417,
74 | "liveness": 0.103,
75 | "valence": 0.187,
76 | "tempo": 192.121,
77 | "type": "audio_features",
78 | "id": "6up8VZjgXqptykibKod32m",
79 | "uri": "spotify:track:6up8VZjgXqptykibKod32m",
80 | "track_href": "https://api.spotify.com/v1/tracks/6up8VZjgXqptykibKod32m",
81 | "analysis_url":
82 | "https://api.spotify.com/v1/audio-analysis/6up8VZjgXqptykibKod32m",
83 | "duration_ms": 140991,
84 | "time_signature": 4
85 | },
86 | {
87 | "danceability": 0.743,
88 | "energy": 0.574,
89 | "key": 8,
90 | "loudness": -6.028,
91 | "mode": 1,
92 | "speechiness": 0.346,
93 | "acousticness": 0.164,
94 | "instrumentalness": 0,
95 | "liveness": 0.0922,
96 | "valence": 0.403,
97 | "tempo": 120.877,
98 | "type": "audio_features",
99 | "id": "7gL2cayNSvJkCJM5dAVOVO",
100 | "uri": "spotify:track:7gL2cayNSvJkCJM5dAVOVO",
101 | "track_href": "https://api.spotify.com/v1/tracks/7gL2cayNSvJkCJM5dAVOVO",
102 | "analysis_url":
103 | "https://api.spotify.com/v1/audio-analysis/7gL2cayNSvJkCJM5dAVOVO",
104 | "duration_ms": 241282,
105 | "time_signature": 4
106 | },
107 | {
108 | "danceability": 0.518,
109 | "energy": 0.749,
110 | "key": 7,
111 | "loudness": -7.134,
112 | "mode": 0,
113 | "speechiness": 0.115,
114 | "acousticness": 0.43,
115 | "instrumentalness": 0,
116 | "liveness": 0.23,
117 | "valence": 0.175,
118 | "tempo": 82.221,
119 | "type": "audio_features",
120 | "id": "2SK8bg3M1Z2yukycOG0JiH",
121 | "uri": "spotify:track:2SK8bg3M1Z2yukycOG0JiH",
122 | "track_href": "https://api.spotify.com/v1/tracks/2SK8bg3M1Z2yukycOG0JiH",
123 | "analysis_url":
124 | "https://api.spotify.com/v1/audio-analysis/2SK8bg3M1Z2yukycOG0JiH",
125 | "duration_ms": 234146,
126 | "time_signature": 4
127 | },
128 | {
129 | "danceability": 0.528,
130 | "energy": 0.757,
131 | "key": 6,
132 | "loudness": -5.423,
133 | "mode": 1,
134 | "speechiness": 0.212,
135 | "acousticness": 0.00957,
136 | "instrumentalness": 0.0000553,
137 | "liveness": 0.145,
138 | "valence": 0.304,
139 | "tempo": 93.007,
140 | "type": "audio_features",
141 | "id": "2Y0B6Aih3MhE4UqleLcIhM",
142 | "uri": "spotify:track:2Y0B6Aih3MhE4UqleLcIhM",
143 | "track_href": "https://api.spotify.com/v1/tracks/2Y0B6Aih3MhE4UqleLcIhM",
144 | "analysis_url":
145 | "https://api.spotify.com/v1/audio-analysis/2Y0B6Aih3MhE4UqleLcIhM",
146 | "duration_ms": 231106,
147 | "time_signature": 4
148 | },
149 | {
150 | "danceability": 0.726,
151 | "energy": 0.583,
152 | "key": 0,
153 | "loudness": -6.434,
154 | "mode": 0,
155 | "speechiness": 0.23,
156 | "acousticness": 0.334,
157 | "instrumentalness": 0.00135,
158 | "liveness": 0.122,
159 | "valence": 0.491,
160 | "tempo": 140.943,
161 | "type": "audio_features",
162 | "id": "1E0EOstQRm7YMhFpmDJzaB",
163 | "uri": "spotify:track:1E0EOstQRm7YMhFpmDJzaB",
164 | "track_href": "https://api.spotify.com/v1/tracks/1E0EOstQRm7YMhFpmDJzaB",
165 | "analysis_url":
166 | "https://api.spotify.com/v1/audio-analysis/1E0EOstQRm7YMhFpmDJzaB",
167 | "duration_ms": 221787,
168 | "time_signature": 4
169 | },
170 | {
171 | "danceability": 0.503,
172 | "energy": 0.665,
173 | "key": 1,
174 | "loudness": -4.569,
175 | "mode": 0,
176 | "speechiness": 0.275,
177 | "acousticness": 0.00841,
178 | "instrumentalness": 0,
179 | "liveness": 0.167,
180 | "valence": 0.261,
181 | "tempo": 79.088,
182 | "type": "audio_features",
183 | "id": "3YQXkAqmNFWfUFlL3HgMbS",
184 | "uri": "spotify:track:3YQXkAqmNFWfUFlL3HgMbS",
185 | "track_href": "https://api.spotify.com/v1/tracks/3YQXkAqmNFWfUFlL3HgMbS",
186 | "analysis_url":
187 | "https://api.spotify.com/v1/audio-analysis/3YQXkAqmNFWfUFlL3HgMbS",
188 | "duration_ms": 267862,
189 | "time_signature": 4
190 | },
191 | {
192 | "danceability": 0.371,
193 | "energy": 0.629,
194 | "key": 5,
195 | "loudness": -4.882,
196 | "mode": 1,
197 | "speechiness": 0.0677,
198 | "acousticness": 0.413,
199 | "instrumentalness": 0,
200 | "liveness": 0.0769,
201 | "valence": 0.411,
202 | "tempo": 139.388,
203 | "type": "audio_features",
204 | "id": "6dVcavGKwvUjk998frTljW",
205 | "uri": "spotify:track:6dVcavGKwvUjk998frTljW",
206 | "track_href": "https://api.spotify.com/v1/tracks/6dVcavGKwvUjk998frTljW",
207 | "analysis_url":
208 | "https://api.spotify.com/v1/audio-analysis/6dVcavGKwvUjk998frTljW",
209 | "duration_ms": 269971,
210 | "time_signature": 3
211 | },
212 | {
213 | "danceability": 0.534,
214 | "energy": 0.869,
215 | "key": 7,
216 | "loudness": -3.437,
217 | "mode": 0,
218 | "speechiness": 0.0536,
219 | "acousticness": 0.1,
220 | "instrumentalness": 0,
221 | "liveness": 0.202,
222 | "valence": 0.427,
223 | "tempo": 99.961,
224 | "type": "audio_features",
225 | "id": "1Bh8jtOXIBIRUUghbrwUTX",
226 | "uri": "spotify:track:1Bh8jtOXIBIRUUghbrwUTX",
227 | "track_href": "https://api.spotify.com/v1/tracks/1Bh8jtOXIBIRUUghbrwUTX",
228 | "analysis_url":
229 | "https://api.spotify.com/v1/audio-analysis/1Bh8jtOXIBIRUUghbrwUTX",
230 | "duration_ms": 242213,
231 | "time_signature": 4
232 | },
233 | {
234 | "danceability": 0.583,
235 | "energy": 0.841,
236 | "key": 2,
237 | "loudness": -3.665,
238 | "mode": 1,
239 | "speechiness": 0.19,
240 | "acousticness": 0.0186,
241 | "instrumentalness": 0,
242 | "liveness": 0.105,
243 | "valence": 0.495,
244 | "tempo": 90.646,
245 | "type": "audio_features",
246 | "id": "5LDGG8mzg9k4Nhi8HavI6s",
247 | "uri": "spotify:track:5LDGG8mzg9k4Nhi8HavI6s",
248 | "track_href": "https://api.spotify.com/v1/tracks/5LDGG8mzg9k4Nhi8HavI6s",
249 | "analysis_url":
250 | "https://api.spotify.com/v1/audio-analysis/5LDGG8mzg9k4Nhi8HavI6s",
251 | "duration_ms": 180000,
252 | "time_signature": 4
253 | },
254 | {
255 | "danceability": 0.707,
256 | "energy": 0.615,
257 | "key": 2,
258 | "loudness": -5.75,
259 | "mode": 1,
260 | "speechiness": 0.0324,
261 | "acousticness": 0.00134,
262 | "instrumentalness": 0.0406,
263 | "liveness": 0.561,
264 | "valence": 0.113,
265 | "tempo": 89.994,
266 | "type": "audio_features",
267 | "id": "7zbq8RT5Kd3ExOGVTiUQbR",
268 | "uri": "spotify:track:7zbq8RT5Kd3ExOGVTiUQbR",
269 | "track_href": "https://api.spotify.com/v1/tracks/7zbq8RT5Kd3ExOGVTiUQbR",
270 | "analysis_url":
271 | "https://api.spotify.com/v1/audio-analysis/7zbq8RT5Kd3ExOGVTiUQbR",
272 | "duration_ms": 421332,
273 | "time_signature": 4
274 | },
275 | {
276 | "danceability": 0.582,
277 | "energy": 0.899,
278 | "key": 10,
279 | "loudness": -3.784,
280 | "mode": 0,
281 | "speechiness": 0.194,
282 | "acousticness": 0.0287,
283 | "instrumentalness": 0.00229,
284 | "liveness": 0.368,
285 | "valence": 0.684,
286 | "tempo": 85.449,
287 | "type": "audio_features",
288 | "id": "2KA6O5BzPP0ASVMoIxN8Or",
289 | "uri": "spotify:track:2KA6O5BzPP0ASVMoIxN8Or",
290 | "track_href": "https://api.spotify.com/v1/tracks/2KA6O5BzPP0ASVMoIxN8Or",
291 | "analysis_url":
292 | "https://api.spotify.com/v1/audio-analysis/2KA6O5BzPP0ASVMoIxN8Or",
293 | "duration_ms": 253549,
294 | "time_signature": 4
295 | },
296 | {
297 | "danceability": 0.754,
298 | "energy": 0.853,
299 | "key": 6,
300 | "loudness": -4.609,
301 | "mode": 0,
302 | "speechiness": 0.188,
303 | "acousticness": 0.0084,
304 | "instrumentalness": 0.0497,
305 | "liveness": 0.363,
306 | "valence": 0.575,
307 | "tempo": 84.985,
308 | "type": "audio_features",
309 | "id": "64sWFVn4OwvOo6Uz2UR3rA",
310 | "uri": "spotify:track:64sWFVn4OwvOo6Uz2UR3rA",
311 | "track_href": "https://api.spotify.com/v1/tracks/64sWFVn4OwvOo6Uz2UR3rA",
312 | "analysis_url":
313 | "https://api.spotify.com/v1/audio-analysis/64sWFVn4OwvOo6Uz2UR3rA",
314 | "duration_ms": 210172,
315 | "time_signature": 4
316 | },
317 | {
318 | "danceability": 0.8,
319 | "energy": 0.586,
320 | "key": 0,
321 | "loudness": -7.344,
322 | "mode": 1,
323 | "speechiness": 0.176,
324 | "acousticness": 0.037,
325 | "instrumentalness": 0,
326 | "liveness": 0.074,
327 | "valence": 0.749,
328 | "tempo": 104.002,
329 | "type": "audio_features",
330 | "id": "0lpRp9KzzMbC0w1uiL7H7f",
331 | "uri": "spotify:track:0lpRp9KzzMbC0w1uiL7H7f",
332 | "track_href": "https://api.spotify.com/v1/tracks/0lpRp9KzzMbC0w1uiL7H7f",
333 | "analysis_url":
334 | "https://api.spotify.com/v1/audio-analysis/0lpRp9KzzMbC0w1uiL7H7f",
335 | "duration_ms": 221623,
336 | "time_signature": 4
337 | },
338 | {
339 | "danceability": 0.714,
340 | "energy": 0.527,
341 | "key": 1,
342 | "loudness": -12.159,
343 | "mode": 1,
344 | "speechiness": 0.396,
345 | "acousticness": 0.501,
346 | "instrumentalness": 0.00000197,
347 | "liveness": 0.141,
348 | "valence": 0.415,
349 | "tempo": 124.896,
350 | "type": "audio_features",
351 | "id": "24ATSFqwDM8Ajg4tIu4I3e",
352 | "uri": "spotify:track:24ATSFqwDM8Ajg4tIu4I3e",
353 | "track_href": "https://api.spotify.com/v1/tracks/24ATSFqwDM8Ajg4tIu4I3e",
354 | "analysis_url":
355 | "https://api.spotify.com/v1/audio-analysis/24ATSFqwDM8Ajg4tIu4I3e",
356 | "duration_ms": 193920,
357 | "time_signature": 4
358 | },
359 | {
360 | "danceability": 0.628,
361 | "energy": 0.877,
362 | "key": 10,
363 | "loudness": -3.016,
364 | "mode": 0,
365 | "speechiness": 0.041,
366 | "acousticness": 0.00189,
367 | "instrumentalness": 0,
368 | "liveness": 0.228,
369 | "valence": 0.199,
370 | "tempo": 142.034,
371 | "type": "audio_features",
372 | "id": "2M71Inc9camxaIGtmQ6I87",
373 | "uri": "spotify:track:2M71Inc9camxaIGtmQ6I87",
374 | "track_href": "https://api.spotify.com/v1/tracks/2M71Inc9camxaIGtmQ6I87",
375 | "analysis_url":
376 | "https://api.spotify.com/v1/audio-analysis/2M71Inc9camxaIGtmQ6I87",
377 | "duration_ms": 228169,
378 | "time_signature": 4
379 | },
380 | {
381 | "danceability": 0.769,
382 | "energy": 0.751,
383 | "key": 2,
384 | "loudness": -5.785,
385 | "mode": 0,
386 | "speechiness": 0.0731,
387 | "acousticness": 0.0262,
388 | "instrumentalness": 0.246,
389 | "liveness": 0.0985,
390 | "valence": 0.276,
391 | "tempo": 99.983,
392 | "type": "audio_features",
393 | "id": "20bXCMaPupQ3ZQlURtRR9D",
394 | "uri": "spotify:track:20bXCMaPupQ3ZQlURtRR9D",
395 | "track_href": "https://api.spotify.com/v1/tracks/20bXCMaPupQ3ZQlURtRR9D",
396 | "analysis_url":
397 | "https://api.spotify.com/v1/audio-analysis/20bXCMaPupQ3ZQlURtRR9D",
398 | "duration_ms": 232783,
399 | "time_signature": 4
400 | },
401 | {
402 | "danceability": 0.549,
403 | "energy": 0.862,
404 | "key": 1,
405 | "loudness": -5.066,
406 | "mode": 0,
407 | "speechiness": 0.437,
408 | "acousticness": 0.289,
409 | "instrumentalness": 0,
410 | "liveness": 0.107,
411 | "valence": 0.47,
412 | "tempo": 152.744,
413 | "type": "audio_features",
414 | "id": "0TscVQ3lOYPag7WvEDkS8K",
415 | "uri": "spotify:track:0TscVQ3lOYPag7WvEDkS8K",
416 | "track_href": "https://api.spotify.com/v1/tracks/0TscVQ3lOYPag7WvEDkS8K",
417 | "analysis_url":
418 | "https://api.spotify.com/v1/audio-analysis/0TscVQ3lOYPag7WvEDkS8K",
419 | "duration_ms": 253453,
420 | "time_signature": 4
421 | },
422 | {
423 | "danceability": 0.69,
424 | "energy": 0.525,
425 | "key": 0,
426 | "loudness": -5.828,
427 | "mode": 1,
428 | "speechiness": 0.122,
429 | "acousticness": 0.123,
430 | "instrumentalness": 0.0000499,
431 | "liveness": 0.11,
432 | "valence": 0.179,
433 | "tempo": 94.99,
434 | "type": "audio_features",
435 | "id": "2q7kYnQgPNJ9MxRTgj88mx",
436 | "uri": "spotify:track:2q7kYnQgPNJ9MxRTgj88mx",
437 | "track_href": "https://api.spotify.com/v1/tracks/2q7kYnQgPNJ9MxRTgj88mx",
438 | "analysis_url":
439 | "https://api.spotify.com/v1/audio-analysis/2q7kYnQgPNJ9MxRTgj88mx",
440 | "duration_ms": 229761,
441 | "time_signature": 4
442 | },
443 | {
444 | "danceability": 0.894,
445 | "energy": 0.55,
446 | "key": 3,
447 | "loudness": -5.905,
448 | "mode": 0,
449 | "speechiness": 0.316,
450 | "acousticness": 0.472,
451 | "instrumentalness": 0.0000189,
452 | "liveness": 0.311,
453 | "valence": 0.713,
454 | "tempo": 99.997,
455 | "type": "audio_features",
456 | "id": "6eL1ncVDtQHgSm3E288TS7",
457 | "uri": "spotify:track:6eL1ncVDtQHgSm3E288TS7",
458 | "track_href": "https://api.spotify.com/v1/tracks/6eL1ncVDtQHgSm3E288TS7",
459 | "analysis_url":
460 | "https://api.spotify.com/v1/audio-analysis/6eL1ncVDtQHgSm3E288TS7",
461 | "duration_ms": 198133,
462 | "time_signature": 4
463 | },
464 | {
465 | "danceability": 0.738,
466 | "energy": 0.481,
467 | "key": 1,
468 | "loudness": -8.812,
469 | "mode": 1,
470 | "speechiness": 0.0869,
471 | "acousticness": 0.0182,
472 | "instrumentalness": 0,
473 | "liveness": 0.389,
474 | "valence": 0.248,
475 | "tempo": 118.06,
476 | "type": "audio_features",
477 | "id": "35tWhD29yvWwB0IDRr6zsL",
478 | "uri": "spotify:track:35tWhD29yvWwB0IDRr6zsL",
479 | "track_href": "https://api.spotify.com/v1/tracks/35tWhD29yvWwB0IDRr6zsL",
480 | "analysis_url":
481 | "https://api.spotify.com/v1/audio-analysis/35tWhD29yvWwB0IDRr6zsL",
482 | "duration_ms": 188947,
483 | "time_signature": 4
484 | },
485 | {
486 | "danceability": 0.626,
487 | "energy": 0.747,
488 | "key": 11,
489 | "loudness": -7.517,
490 | "mode": 0,
491 | "speechiness": 0.132,
492 | "acousticness": 0.194,
493 | "instrumentalness": 0.0041,
494 | "liveness": 0.0901,
495 | "valence": 0.0726,
496 | "tempo": 131.007,
497 | "type": "audio_features",
498 | "id": "0RxSL5kjZXO45l38vVerNO",
499 | "uri": "spotify:track:0RxSL5kjZXO45l38vVerNO",
500 | "track_href": "https://api.spotify.com/v1/tracks/0RxSL5kjZXO45l38vVerNO",
501 | "analysis_url":
502 | "https://api.spotify.com/v1/audio-analysis/0RxSL5kjZXO45l38vVerNO",
503 | "duration_ms": 197800,
504 | "time_signature": 4
505 | },
506 | {
507 | "danceability": 0.775,
508 | "energy": 0.529,
509 | "key": 2,
510 | "loudness": -6.87,
511 | "mode": 0,
512 | "speechiness": 0.245,
513 | "acousticness": 0.214,
514 | "instrumentalness": 0,
515 | "liveness": 0.105,
516 | "valence": 0.106,
517 | "tempo": 139.784,
518 | "type": "audio_features",
519 | "id": "2QM7WsnRreCQZKPh8mdVp5",
520 | "uri": "spotify:track:2QM7WsnRreCQZKPh8mdVp5",
521 | "track_href": "https://api.spotify.com/v1/tracks/2QM7WsnRreCQZKPh8mdVp5",
522 | "analysis_url":
523 | "https://api.spotify.com/v1/audio-analysis/2QM7WsnRreCQZKPh8mdVp5",
524 | "duration_ms": 264739,
525 | "time_signature": 4
526 | },
527 | {
528 | "danceability": 0.73,
529 | "energy": 0.718,
530 | "key": 7,
531 | "loudness": -6.185,
532 | "mode": 1,
533 | "speechiness": 0.204,
534 | "acousticness": 0.121,
535 | "instrumentalness": 0.0000459,
536 | "liveness": 0.166,
537 | "valence": 0.488,
538 | "tempo": 100.059,
539 | "type": "audio_features",
540 | "id": "7M9MujbMTIix2IO3v7IjaK",
541 | "uri": "spotify:track:7M9MujbMTIix2IO3v7IjaK",
542 | "track_href": "https://api.spotify.com/v1/tracks/7M9MujbMTIix2IO3v7IjaK",
543 | "analysis_url":
544 | "https://api.spotify.com/v1/audio-analysis/7M9MujbMTIix2IO3v7IjaK",
545 | "duration_ms": 251320,
546 | "time_signature": 4
547 | },
548 | {
549 | "danceability": 0.679,
550 | "energy": 0.74,
551 | "key": 1,
552 | "loudness": -7.928,
553 | "mode": 1,
554 | "speechiness": 0.268,
555 | "acousticness": 0.365,
556 | "instrumentalness": 0,
557 | "liveness": 0.14,
558 | "valence": 0.339,
559 | "tempo": 88.698,
560 | "type": "audio_features",
561 | "id": "4ixDAEYW8uW2ccT2BqMqwK",
562 | "uri": "spotify:track:4ixDAEYW8uW2ccT2BqMqwK",
563 | "track_href": "https://api.spotify.com/v1/tracks/4ixDAEYW8uW2ccT2BqMqwK",
564 | "analysis_url":
565 | "https://api.spotify.com/v1/audio-analysis/4ixDAEYW8uW2ccT2BqMqwK",
566 | "duration_ms": 130787,
567 | "time_signature": 4
568 | },
569 | {
570 | "danceability": 0.566,
571 | "energy": 0.678,
572 | "key": 2,
573 | "loudness": -6.333,
574 | "mode": 1,
575 | "speechiness": 0.304,
576 | "acousticness": 0.454,
577 | "instrumentalness": 0,
578 | "liveness": 0.29,
579 | "valence": 0.36,
580 | "tempo": 89.963,
581 | "type": "audio_features",
582 | "id": "4pAzikd44RA4ndgbPZC9QE",
583 | "uri": "spotify:track:4pAzikd44RA4ndgbPZC9QE",
584 | "track_href": "https://api.spotify.com/v1/tracks/4pAzikd44RA4ndgbPZC9QE",
585 | "analysis_url":
586 | "https://api.spotify.com/v1/audio-analysis/4pAzikd44RA4ndgbPZC9QE",
587 | "duration_ms": 251733,
588 | "time_signature": 4
589 | },
590 | {
591 | "danceability": 0.552,
592 | "energy": 0.503,
593 | "key": 5,
594 | "loudness": -6.759,
595 | "mode": 0,
596 | "speechiness": 0.145,
597 | "acousticness": 0.138,
598 | "instrumentalness": 0.000555,
599 | "liveness": 0.143,
600 | "valence": 0.141,
601 | "tempo": 159.706,
602 | "type": "audio_features",
603 | "id": "29VZKgZvAjFRxxBnctjqOn",
604 | "uri": "spotify:track:29VZKgZvAjFRxxBnctjqOn",
605 | "track_href": "https://api.spotify.com/v1/tracks/29VZKgZvAjFRxxBnctjqOn",
606 | "analysis_url":
607 | "https://api.spotify.com/v1/audio-analysis/29VZKgZvAjFRxxBnctjqOn",
608 | "duration_ms": 218480,
609 | "time_signature": 4
610 | },
611 | {
612 | "danceability": 0.499,
613 | "energy": 0.798,
614 | "key": 9,
615 | "loudness": -4.494,
616 | "mode": 1,
617 | "speechiness": 0.3,
618 | "acousticness": 0.0123,
619 | "instrumentalness": 0,
620 | "liveness": 0.821,
621 | "valence": 0.423,
622 | "tempo": 164.018,
623 | "type": "audio_features",
624 | "id": "5g8CUSqonc6mukd4xntKTb",
625 | "uri": "spotify:track:5g8CUSqonc6mukd4xntKTb",
626 | "track_href": "https://api.spotify.com/v1/tracks/5g8CUSqonc6mukd4xntKTb",
627 | "analysis_url":
628 | "https://api.spotify.com/v1/audio-analysis/5g8CUSqonc6mukd4xntKTb",
629 | "duration_ms": 192125,
630 | "time_signature": 4
631 | },
632 | {
633 | "danceability": 0.839,
634 | "energy": 0.879,
635 | "key": 1,
636 | "loudness": -6.833,
637 | "mode": 1,
638 | "speechiness": 0.0663,
639 | "acousticness": 0.0523,
640 | "instrumentalness": 0.683,
641 | "liveness": 0.0784,
642 | "valence": 0.087,
643 | "tempo": 109.99,
644 | "type": "audio_features",
645 | "id": "4teFVCNjN9l8NyQObbTnQN",
646 | "uri": "spotify:track:4teFVCNjN9l8NyQObbTnQN",
647 | "track_href": "https://api.spotify.com/v1/tracks/4teFVCNjN9l8NyQObbTnQN",
648 | "analysis_url":
649 | "https://api.spotify.com/v1/audio-analysis/4teFVCNjN9l8NyQObbTnQN",
650 | "duration_ms": 248499,
651 | "time_signature": 4
652 | },
653 | {
654 | "danceability": 0.768,
655 | "energy": 0.605,
656 | "key": 1,
657 | "loudness": -6.749,
658 | "mode": 1,
659 | "speechiness": 0.36,
660 | "acousticness": 0.0353,
661 | "instrumentalness": 0,
662 | "liveness": 0.241,
663 | "valence": 0.85,
664 | "tempo": 109.85,
665 | "type": "audio_features",
666 | "id": "6glTh9W17TDwrXrLNcm1fg",
667 | "uri": "spotify:track:6glTh9W17TDwrXrLNcm1fg",
668 | "track_href": "https://api.spotify.com/v1/tracks/6glTh9W17TDwrXrLNcm1fg",
669 | "analysis_url":
670 | "https://api.spotify.com/v1/audio-analysis/6glTh9W17TDwrXrLNcm1fg",
671 | "duration_ms": 238863,
672 | "time_signature": 4
673 | },
674 | {
675 | "danceability": 0.798,
676 | "energy": 0.692,
677 | "key": 1,
678 | "loudness": -6.623,
679 | "mode": 1,
680 | "speechiness": 0.0926,
681 | "acousticness": 0.0989,
682 | "instrumentalness": 0.000011,
683 | "liveness": 0.0941,
684 | "valence": 0.547,
685 | "tempo": 103.981,
686 | "type": "audio_features",
687 | "id": "5d0XP1EN2vZdcro17F8hW6",
688 | "uri": "spotify:track:5d0XP1EN2vZdcro17F8hW6",
689 | "track_href": "https://api.spotify.com/v1/tracks/5d0XP1EN2vZdcro17F8hW6",
690 | "analysis_url":
691 | "https://api.spotify.com/v1/audio-analysis/5d0XP1EN2vZdcro17F8hW6",
692 | "duration_ms": 201184,
693 | "time_signature": 4
694 | },
695 | {
696 | "danceability": 0.613,
697 | "energy": 0.879,
698 | "key": 6,
699 | "loudness": -4.649,
700 | "mode": 1,
701 | "speechiness": 0.0729,
702 | "acousticness": 0.00908,
703 | "instrumentalness": 0.00000166,
704 | "liveness": 0.108,
705 | "valence": 0.497,
706 | "tempo": 76.966,
707 | "type": "audio_features",
708 | "id": "6GAhe3wXCDJP1RK5lZLyjX",
709 | "uri": "spotify:track:6GAhe3wXCDJP1RK5lZLyjX",
710 | "track_href": "https://api.spotify.com/v1/tracks/6GAhe3wXCDJP1RK5lZLyjX",
711 | "analysis_url":
712 | "https://api.spotify.com/v1/audio-analysis/6GAhe3wXCDJP1RK5lZLyjX",
713 | "duration_ms": 214929,
714 | "time_signature": 4
715 | }
716 | ]
717 |
--------------------------------------------------------------------------------
/src/app/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Spotify from "spotify-web-api-js";
3 | import Spinner from "react-spinkit";
4 | import AlertContainer from "react-alert";
5 | import ReactGA from "react-ga";
6 |
7 | // component imports
8 | import Header from "./components/Header/Header";
9 | import BigButton from "./components/Button";
10 | import PlaylistSelector from "./components/PlaylistSelector/PlaylistSelector";
11 | import SliderSelector from "./components/Slider/SliderSelector";
12 | import GenreSelector from "./components/GenreSelector/GenreSelector";
13 | import Player from "./components/Player/Player";
14 | import SongStatistics from "./components/SongStats/SongStatistics";
15 | import RadarSection from "./components/Radar/RadarSection";
16 | import HowItWorks from "./components/Instructions/HowItWorks";
17 | import Promotion from "./components/Instructions/Promotion";
18 |
19 | // css imports
20 | import "./styles/buttons.css";
21 | import "./styles/compiled-player.css";
22 | import "./styles/details.css";
23 | import "./styles/main.css";
24 | import "./styles/slider.css";
25 |
26 | import playlists from "./content/playlists/playlists";
27 | import check from "./content/alert-icons/check.png";
28 | import exclamation from "./content/alert-icons/exclamation.png";
29 |
30 | // helper function imports
31 | import {
32 | calcAndSort,
33 | getHashParams,
34 | setLoginEventListener
35 | } from "./javascripts/helpers";
36 |
37 | const calcQueue = playlistNumber => {
38 | let queue = [];
39 | let data = playlists[playlistNumber].data.items;
40 | for (let i = 0; i < data.length; i++) {
41 | queue.push(data[i].track);
42 | }
43 | return queue;
44 | };
45 |
46 | class App extends Component {
47 | constructor(props, context) {
48 | super(props, context);
49 | this.state = {
50 | energyValue: 50,
51 | valenceValue: 50,
52 | acousticValue: 50,
53 | danceValue: 50,
54 | vocalnessValue: 50,
55 | popularityValue: 50,
56 | filterBy: {
57 | energy: true,
58 | valence: true,
59 | acoustic: true,
60 | dance: true,
61 | vocalness: true,
62 | popularity: true,
63 | genre: true
64 | },
65 | songRecommendation: playlists[0].data.items[0].track,
66 | params: {},
67 | loading: false,
68 | initialLoad: false,
69 | songInLibrary: false,
70 | queue: calcQueue(0),
71 | queueDetails: playlists[0].details,
72 | queuePosition: 0,
73 | createdPlaylist: false,
74 | seed_genres: "",
75 | selectedPlaylist: 0,
76 | calculations:
77 | "Create your own at synthetic.netlify.com | Your generated playlist with music from the Track Sampler selection. Filters - Energy: 50, Valence: 50, Acoustic: 50, Dance: 50, Popularity: 50"
78 | };
79 | this.nextSong = this.nextSong.bind(this);
80 | this.prevSong = this.prevSong.bind(this);
81 | this.addSong = this.addSong.bind(this);
82 | }
83 |
84 | componentWillMount() {}
85 |
86 | componentDidMount() {
87 | // GA tracking
88 | ReactGA.initialize("UA-108999356-1");
89 | ReactGA.set({ page: window.location.pathname });
90 | ReactGA.pageview(window.location.pathname);
91 |
92 | this.setState(
93 | {
94 | params: getHashParams()
95 | },
96 | () => {
97 | if (this.state.params.access_token) {
98 | const spotifyApi = new Spotify();
99 | spotifyApi.setAccessToken(this.state.params.access_token);
100 | spotifyApi
101 | .containsMySavedTracks([this.state.songRecommendation.id])
102 | .then(response => {
103 | this.setState({
104 | songInLibrary: response[0]
105 | });
106 | })
107 | .catch(function(error) {
108 | console.error(error);
109 | });
110 | spotifyApi
111 | .getMe()
112 | .then(response => {
113 | this.setState({
114 | me: response,
115 | initialLoad: true
116 | });
117 | })
118 | .catch(function(error) {
119 | console.error(error);
120 | });
121 | }
122 |
123 | // // get track details DEV only ----
124 | // let dataDetails;
125 | // const s = new Spotify();
126 | // s.setAccessToken(this.state.params.access_token);
127 | // //get static details
128 | // let data = playlists[0].data.items;
129 | // let ddd = [];
130 | // // get song ids to request details
131 | // for (let j = 0; j < data.length; j++) {
132 | // ddd.push(data[j].track.id);
133 | // }
134 | // s.getAudioFeaturesForTracks(ddd, (error, response) => {
135 | // // override details with new recommendations
136 | // console.log(response);
137 | // dataDetails = response.audio_features;
138 | // });
139 | // console.log(dataDetails);
140 | // // --------------------------------
141 | }
142 | );
143 | setLoginEventListener();
144 | setTimeout(() => {
145 | this.setState({ params: {} });
146 | window.location = "/";
147 | this.showSessionTimeout();
148 | }, 1000 * 60 * 60);
149 | }
150 | // handle playlist change
151 | handlePlaylistChange = value => {
152 | this.setState({
153 | selectedPlaylist: value,
154 | queue: calcQueue(value),
155 | queueDetails: playlists[value].details,
156 | songRecommendation: playlists[value].data.items[0].track,
157 | queuePosition: 0,
158 | seed_genres: ""
159 | });
160 | if (!this.state.params.access_token && value == 10) {
161 | this.showLoginNotification();
162 | }
163 | this.child.stopPlayback();
164 | let audio = document.getElementById("audio");
165 | audio.load();
166 | };
167 | // adjust slider values
168 | handleEnergyChange = value => {
169 | this.setState({ energyValue: value });
170 | };
171 | handleValenceChange = value => {
172 | this.setState({ valenceValue: value });
173 | };
174 | handleAcousticChange = value => {
175 | this.setState({ acousticValue: value });
176 | };
177 | handleDanceChange = value => {
178 | this.setState({ danceValue: value });
179 | };
180 | handleVocalnessChange = value => {
181 | this.setState({ vocalnessValue: value });
182 | };
183 | handlePopularityChange = value => {
184 | this.setState({ popularityValue: value });
185 | };
186 | handleGenreChange = value => {
187 | this.setState({ seed_genres: value });
188 | };
189 | // handle radio buttons to select what to filter by
190 | toggleEnergyFilter = () => {
191 | this.setState({
192 | filterBy: { ...this.state.filterBy, energy: !this.state.filterBy.energy }
193 | });
194 | };
195 | toggleValenceFilter = () => {
196 | this.setState({
197 | filterBy: {
198 | ...this.state.filterBy,
199 | valence: !this.state.filterBy.valence
200 | }
201 | });
202 | };
203 | toggleAcousticFilter = () => {
204 | this.setState({
205 | filterBy: {
206 | ...this.state.filterBy,
207 | acoustic: !this.state.filterBy.acoustic
208 | }
209 | });
210 | };
211 | toggleDanceFilter = () => {
212 | this.setState({
213 | filterBy: { ...this.state.filterBy, dance: !this.state.filterBy.dance }
214 | });
215 | };
216 | toggleVocalnessFilter = () => {
217 | this.setState({
218 | filterBy: {
219 | ...this.state.filterBy,
220 | vocalness: !this.state.filterBy.vocalness
221 | }
222 | });
223 | };
224 | togglePopularityFilter = () => {
225 | this.setState({
226 | filterBy: {
227 | ...this.state.filterBy,
228 | popularity: !this.state.filterBy.popularity
229 | }
230 | });
231 | };
232 | toggleGenreFilter = () => {
233 | this.setState({
234 | filterBy: { ...this.state.filterBy, genre: !this.state.filterBy.genre }
235 | });
236 | };
237 |
238 | // main calculation button
239 | handleClick = () => {
240 | this.child.stopPlayback();
241 | this.setState({ loading: true });
242 | ReactGA.event({
243 | category: "Button",
244 | action: "Click"
245 | });
246 |
247 | const s = new Spotify();
248 | s.setAccessToken(this.state.params.access_token);
249 |
250 | // by default use the sample data
251 | let data = playlists[this.state.selectedPlaylist].data.items;
252 | let dataDetails = playlists[this.state.selectedPlaylist].details;
253 | let recommendationSongIds = [];
254 | let calculatedData = [];
255 |
256 | // use more specific data with API requests
257 | let isSeeds = this.state.seed_genres === "" ? false : true;
258 | let options = {};
259 | if (this.state.filterBy.energy)
260 | options["target_energy"] = this.state.energyValue / 100;
261 | if (this.state.filterBy.valence)
262 | options["target_valence"] = this.state.valenceValue / 100;
263 | if (this.state.filterBy.acoustic)
264 | options["target_acousticness"] = this.state.acousticValue / 100;
265 | if (this.state.filterBy.dance)
266 | options["target_danceability"] = this.state.danceValue / 100;
267 | if (this.state.filterBy.vocalness)
268 | options["target_instrumentalness"] = this.state.vocalnessValue / 100;
269 | if (this.state.filterBy.popularity)
270 | options["target_popularity"] = this.state.popularityValue;
271 | if (this.state.filterBy.genre && this.state.seed_genres !== "")
272 | options["seed_genres"] = this.state.seed_genres;
273 | options["limit"] = 50;
274 |
275 | let formattedCalculations = [];
276 | if (this.state.filterBy.energy)
277 | formattedCalculations.push(`Energy: ${this.state.energyValue}`);
278 | if (this.state.filterBy.valence)
279 | formattedCalculations.push(`Valence: ${this.state.valenceValue}`);
280 | if (this.state.filterBy.acoustic)
281 | formattedCalculations.push(`Acoustic: ${this.state.acousticValue}`);
282 | if (this.state.filterBy.dance)
283 | formattedCalculations.push(`Dance: ${this.state.danceValue}`);
284 | if (this.state.filterBy.vocalness)
285 | formattedCalculations.push(`Vocalness: ${this.state.vocalnessValue}`);
286 | if (this.state.filterBy.popularity)
287 | formattedCalculations.push(`Popularity: ${this.state.popularityValue}`);
288 | if (this.state.filterBy.genre && this.state.seed_genres !== "")
289 | formattedCalculations.push(`Genres: ${this.state.seed_genres}`);
290 | const calculationsString = formattedCalculations.join(", ");
291 |
292 | try {
293 | if (
294 | isSeeds &&
295 | this.state.params.access_token &&
296 | this.state.filterBy.genre
297 | ) {
298 | s.getRecommendations(options)
299 | .then(response => {
300 | // override data with new recommendations
301 | data = response.tracks;
302 | let reformattedData = [];
303 |
304 | // get song ids to request details
305 | for (let j = 0; j < data.length; j++) {
306 | recommendationSongIds.push(data[j].id);
307 | reformattedData.push({ track: data[j] });
308 | }
309 | s.getAudioFeaturesForTracks(
310 | recommendationSongIds,
311 | (error, response) => {
312 | // override details with new recommendations
313 | dataDetails = response.audio_features;
314 |
315 | calculatedData = calcAndSort(
316 | reformattedData,
317 | dataDetails,
318 | this.state
319 | );
320 | s.containsMySavedTracks([calculatedData[0].id])
321 | .then(response => {
322 | this.setState({ songInLibrary: response[0] });
323 | })
324 | .catch(function(error) {
325 | console.error(error);
326 | });
327 | // final assignment of top calculated track, remove loading, and load the queue
328 | this.setState({
329 | songRecommendation: calculatedData[0],
330 | loading: false,
331 | queue: calculatedData,
332 | queueDetails: dataDetails,
333 | queuePosition: 0,
334 | createdPlaylist: false,
335 | calculations: calculationsString
336 | });
337 | let audio = document.getElementById("audio");
338 | audio.load();
339 | }
340 | );
341 | })
342 | .catch(function(error) {
343 | console.error(error);
344 | });
345 | } else {
346 | calculatedData = calcAndSort(data, dataDetails, this.state);
347 | if (this.state.params.access_token) {
348 | s.containsMySavedTracks([calculatedData[0].id])
349 | .then(response => {
350 | this.setState({ songInLibrary: response[0] });
351 | })
352 | .catch(function(error) {
353 | console.error(error);
354 | });
355 | }
356 | // final assignment of top calculated track, remove loading, and load the queue
357 | this.setState({
358 | songRecommendation: calculatedData[0],
359 | loading: false,
360 | queue: calculatedData,
361 | queueDetails: dataDetails,
362 | queuePosition: 0,
363 | createdPlaylist: false,
364 | calculations: calculationsString
365 | });
366 | let audio = document.getElementById("audio");
367 | audio.load();
368 | }
369 | } catch (err) {
370 | this.showError();
371 | console.log(err);
372 | this.setState({ loading: false });
373 | }
374 | // won't get stuck loading but worse UX
375 | // this.setState({ loading: false });
376 | };
377 |
378 | // control button functions (add, play/pause, next)
379 | addSong = () => {
380 | ReactGA.event({
381 | category: "Button",
382 | action: "Add_Song"
383 | });
384 | const s = new Spotify();
385 | s.setAccessToken(this.state.params.access_token);
386 | if (this.state.params.access_token !== undefined) {
387 | s.addToMySavedTracks([this.state.songRecommendation.id], {}).then(() => {
388 | this.setState({ songInLibrary: true });
389 | });
390 | this.showAdded(this.state.songInLibrary);
391 | } else {
392 | this.showAlert();
393 | }
394 | };
395 | prevSong = () => {
396 | this.setState({ loading: true });
397 | let state = this.state;
398 | let newQueuePosition = state.queuePosition - 1;
399 | if (newQueuePosition < 0) {
400 | newQueuePosition = 0;
401 | this.showFirstSong();
402 | }
403 | const spotifyApi = new Spotify();
404 | spotifyApi.setAccessToken(state.params.access_token);
405 |
406 | let that = this;
407 | // check if user has access token before making request
408 | if (this.state.params.access_token !== undefined) {
409 | spotifyApi
410 | .containsMySavedTracks([state.queue[newQueuePosition].id])
411 | .then(response => {
412 | this.setState({
413 | queuePosition: newQueuePosition,
414 | songRecommendation: state.queue[newQueuePosition],
415 | songInLibrary: response[0]
416 | });
417 | let audio = document.getElementById("audio");
418 | audio.load();
419 | this.child.stopPlayback();
420 | this.setState({ loading: false });
421 | })
422 | .catch(function(error) {
423 | console.error(error);
424 | that.showError();
425 | that.setState({ params: {}, loading: false });
426 | setLoginEventListener();
427 | });
428 | } else {
429 | this.setState({
430 | queuePosition: newQueuePosition,
431 | songRecommendation: state.queue[newQueuePosition]
432 | });
433 | let audio = document.getElementById("audio");
434 | audio.load();
435 | this.child.stopPlayback();
436 | this.setState({ loading: false });
437 | }
438 | };
439 | nextSong = () => {
440 | this.setState({ loading: true });
441 | let state = this.state;
442 | let newQueuePosition = state.queuePosition + 1;
443 | if (newQueuePosition >= state.queue.length) {
444 | newQueuePosition = state.queuePosition;
445 | this.showLastSong();
446 | }
447 | const spotifyApi = new Spotify();
448 | spotifyApi.setAccessToken(state.params.access_token);
449 |
450 | let that = this;
451 | // check if user has access token before making request
452 | if (this.state.params.access_token !== undefined) {
453 | spotifyApi
454 | .containsMySavedTracks([state.queue[newQueuePosition].id])
455 | .then(response => {
456 | this.setState({
457 | queuePosition: newQueuePosition,
458 | songRecommendation: state.queue[newQueuePosition],
459 | songInLibrary: response[0]
460 | });
461 | let audio = document.getElementById("audio");
462 | audio.load();
463 | this.child.stopPlayback();
464 | this.setState({ loading: false });
465 | })
466 | .catch(function(error) {
467 | console.error(error);
468 | that.showError();
469 | that.setState({ params: {}, loading: false });
470 | setLoginEventListener();
471 | });
472 | } else {
473 | this.setState({
474 | queuePosition: newQueuePosition,
475 | songRecommendation: state.queue[newQueuePosition]
476 | });
477 | let audio = document.getElementById("audio");
478 | audio.load();
479 | this.child.stopPlayback();
480 | this.setState({ loading: false });
481 | }
482 | };
483 | addPlaylist = () => {
484 | ReactGA.event({
485 | category: "Button",
486 | action: "Add_Playlist"
487 | });
488 | const s = new Spotify();
489 | s.setAccessToken(this.state.params.access_token);
490 | if (this.state.params.access_token !== undefined) {
491 | if (!this.state.createdPlaylist) {
492 | // create blank playlist
493 | s.createPlaylist(this.state.me.id, {
494 | name: `Synthetic - ${playlists[this.state.selectedPlaylist].name}`,
495 | description: `Create your own at synthetic.netlify.com | Your generated playlist with music from the ${
496 | playlists[this.state.selectedPlaylist].name
497 | } selection. Filters - ${JSON.stringify(this.state.calculations)}`
498 | })
499 | .then(response => {
500 | this.setState({ createdPlaylist: true });
501 | let trackURIs = [];
502 | for (let i = 0; i < 25 && i < this.state.queue.length; i++) {
503 | trackURIs.push(this.state.queue[i].uri);
504 | }
505 | s.addTracksToPlaylist(this.state.me.id, response.id, trackURIs);
506 | })
507 | .catch(function(error) {
508 | console.error(error);
509 | });
510 | }
511 |
512 | // show msg whether playlist was created or already generated
513 | this.showCreatedPlaylist(this.state.createdPlaylist);
514 | } else {
515 | this.showAlert();
516 | }
517 | };
518 |
519 | // react alert messages and options
520 | alertOptions = {
521 | offset: 14,
522 | position: "bottom right",
523 | theme: "dark",
524 | time: 5000,
525 | transition: "fade"
526 | };
527 | showAlert = () => {
528 | this.msg.show(
529 | "Login with Spotify to add songs or playlists to your library",
530 | {
531 | time: 4000,
532 | type: "success",
533 | icon: (
534 |
539 | )
540 | }
541 | );
542 | };
543 | showFollowAlert = () => {
544 | this.msg.show("Login with Spotify to follow playlists from Synthetic", {
545 | time: 4000,
546 | type: "success",
547 | icon: (
548 |
553 | )
554 | });
555 | };
556 | showError = () => {
557 | this.msg.show("An error occured, you may need to sign out and/or log in", {
558 | time: 4000,
559 | type: "error",
560 | icon: (
561 |
566 | )
567 | });
568 | };
569 | showAdded = songInLibrary => {
570 | if (songInLibrary) {
571 | this.msg.show("Song has already been added to your library", {
572 | time: 4000,
573 | type: "error",
574 | icon: (
575 |
580 | )
581 | });
582 | } else {
583 | this.msg.show("Song added to your library", {
584 | time: 4000,
585 | type: "success",
586 | icon: (
587 |
588 | )
589 | });
590 | }
591 | };
592 | showCreatedPlaylist = createdPlaylist => {
593 | if (createdPlaylist) {
594 | this.msg.show("Playlist has already been generated with this data", {
595 | time: 4000,
596 | type: "error",
597 | icon: (
598 |
603 | )
604 | });
605 | } else {
606 | this.msg.show("Playlist generated from these recommendations", {
607 | time: 4000,
608 | type: "success",
609 | icon: (
610 |
611 | )
612 | });
613 | }
614 | };
615 | showSessionTimeout = () => {
616 | this.msg.show(
617 | "Your session has expired, login again to access user-specific functions",
618 | {
619 | time: 0,
620 | type: "success",
621 | icon: (
622 |
627 | )
628 | }
629 | );
630 | };
631 | showFirstSong = () => {
632 | this.msg.show(
633 | "First song in results reached, move through songs with the right arrow",
634 | {
635 | time: 4000,
636 | type: "error",
637 | icon: (
638 |
643 | )
644 | }
645 | );
646 | };
647 | showLastSong = () => {
648 | this.msg.show("Last song in results set reached", {
649 | time: 4000,
650 | type: "error",
651 | icon: (
652 |
657 | )
658 | });
659 | };
660 | showLoginNotification = () => {
661 | this.msg.show("Login with Spotify to search Spotify's entire library", {
662 | time: 4000,
663 | type: "error",
664 | icon: (
665 |
670 | )
671 | });
672 | };
673 |
674 | render() {
675 | const {
676 | energyValue,
677 | valenceValue,
678 | acousticValue,
679 | danceValue,
680 | popularityValue,
681 | vocalnessValue,
682 | songRecommendation
683 | } = this.state;
684 | return (
685 |
686 |
687 |
693 |
700 |
707 |
714 |
721 |
728 |
735 | {this.state.params.access_token &&
736 | this.state.selectedPlaylist === 10 ? (
737 |
738 | Experimental Features
739 |
745 |
746 | ) : null}
747 |
748 |
754 |
759 |
760 | ) : (
761 | "Calculate"
762 | )
763 | }
764 | onClick={this.handleClick}
765 | disabled={this.state.loading}
766 | />
767 |
768 |
769 |
770 |
771 |
object.id === songRecommendation.id
782 | )[0]
783 | }
784 | />
785 | (this.child = ref)}
797 | songInLibrary={this.state.songInLibrary}
798 | nextSong={this.nextSong}
799 | addSong={this.addSong}
800 | prevSong={this.prevSong}
801 | addPlaylist={this.addPlaylist}
802 | createdPlaylist={this.state.createdPlaylist}
803 | />
804 | object.id === songRecommendation.id
809 | )[0]
810 | }
811 | />
812 |
813 |
814 |
815 |
816 | this.showFollowAlert()}
822 | />
823 |
824 |
850 |
851 | This site is in no way affiliated with Spotify or its partners
852 |
853 |
854 | (this.msg = a)} {...this.alertOptions} />
855 |
856 | );
857 | }
858 | }
859 |
860 | export default App;
861 |
--------------------------------------------------------------------------------
/src/app/content/playlist-details/experimental-details.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "danceability": 0.576,
4 | "energy": 0.291,
5 | "key": 10,
6 | "loudness": -9.919,
7 | "mode": 0,
8 | "speechiness": 0.108,
9 | "acousticness": 0.773,
10 | "instrumentalness": 0.908,
11 | "liveness": 0.205,
12 | "valence": 0.201,
13 | "tempo": 89.529,
14 | "type": "audio_features",
15 | "id": "59tpgfXjTc3QR7mWJj8Wln",
16 | "uri": "spotify:track:59tpgfXjTc3QR7mWJj8Wln",
17 | "track_href": "https://api.spotify.com/v1/tracks/59tpgfXjTc3QR7mWJj8Wln",
18 | "analysis_url":
19 | "https://api.spotify.com/v1/audio-analysis/59tpgfXjTc3QR7mWJj8Wln",
20 | "duration_ms": 253612,
21 | "time_signature": 4
22 | },
23 | {
24 | "danceability": 0.676,
25 | "energy": 0.703,
26 | "key": 8,
27 | "loudness": -5.815,
28 | "mode": 0,
29 | "speechiness": 0.0302,
30 | "acousticness": 0.0869,
31 | "instrumentalness": 0.000687,
32 | "liveness": 0.0463,
33 | "valence": 0.852,
34 | "tempo": 92.761,
35 | "type": "audio_features",
36 | "id": "1foMv2HQwfQ2vntFf9HFeG",
37 | "uri": "spotify:track:1foMv2HQwfQ2vntFf9HFeG",
38 | "track_href": "https://api.spotify.com/v1/tracks/1foMv2HQwfQ2vntFf9HFeG",
39 | "analysis_url":
40 | "https://api.spotify.com/v1/audio-analysis/1foMv2HQwfQ2vntFf9HFeG",
41 | "duration_ms": 200173,
42 | "time_signature": 4
43 | },
44 | {
45 | "danceability": 0.598,
46 | "energy": 0.802,
47 | "key": 5,
48 | "loudness": -7.059,
49 | "mode": 0,
50 | "speechiness": 0.0261,
51 | "acousticness": 0.00199,
52 | "instrumentalness": 0.342,
53 | "liveness": 0.0634,
54 | "valence": 0.453,
55 | "tempo": 120.03,
56 | "type": "audio_features",
57 | "id": "6afC4rD1MwUsFPLQRoCBa2",
58 | "uri": "spotify:track:6afC4rD1MwUsFPLQRoCBa2",
59 | "track_href": "https://api.spotify.com/v1/tracks/6afC4rD1MwUsFPLQRoCBa2",
60 | "analysis_url":
61 | "https://api.spotify.com/v1/audio-analysis/6afC4rD1MwUsFPLQRoCBa2",
62 | "duration_ms": 216250,
63 | "time_signature": 4
64 | },
65 | {
66 | "danceability": 0.882,
67 | "energy": 0.743,
68 | "key": 7,
69 | "loudness": -6.972,
70 | "mode": 1,
71 | "speechiness": 0.141,
72 | "acousticness": 0.0107,
73 | "instrumentalness": 0.00152,
74 | "liveness": 0.327,
75 | "valence": 0.667,
76 | "tempo": 123.97,
77 | "type": "audio_features",
78 | "id": "7qVLZuvzNjyiy3TVtEBwBE",
79 | "uri": "spotify:track:7qVLZuvzNjyiy3TVtEBwBE",
80 | "track_href": "https://api.spotify.com/v1/tracks/7qVLZuvzNjyiy3TVtEBwBE",
81 | "analysis_url":
82 | "https://api.spotify.com/v1/audio-analysis/7qVLZuvzNjyiy3TVtEBwBE",
83 | "duration_ms": 284027,
84 | "time_signature": 4
85 | },
86 | {
87 | "danceability": 0.663,
88 | "energy": 0.746,
89 | "key": 7,
90 | "loudness": -6.832,
91 | "mode": 1,
92 | "speechiness": 0.0518,
93 | "acousticness": 0.00344,
94 | "instrumentalness": 0.837,
95 | "liveness": 0.376,
96 | "valence": 0.515,
97 | "tempo": 125.014,
98 | "type": "audio_features",
99 | "id": "6K2XPbZ4maj8ud5EeKsYIn",
100 | "uri": "spotify:track:6K2XPbZ4maj8ud5EeKsYIn",
101 | "track_href": "https://api.spotify.com/v1/tracks/6K2XPbZ4maj8ud5EeKsYIn",
102 | "analysis_url":
103 | "https://api.spotify.com/v1/audio-analysis/6K2XPbZ4maj8ud5EeKsYIn",
104 | "duration_ms": 342413,
105 | "time_signature": 4
106 | },
107 | {
108 | "danceability": 0.597,
109 | "energy": 0.917,
110 | "key": 4,
111 | "loudness": -7.912,
112 | "mode": 0,
113 | "speechiness": 0.0363,
114 | "acousticness": 0.0135,
115 | "instrumentalness": 0.309,
116 | "liveness": 0.615,
117 | "valence": 0.342,
118 | "tempo": 139.959,
119 | "type": "audio_features",
120 | "id": "6Q2mJ6UfrGr1Aq9Fpt157c",
121 | "uri": "spotify:track:6Q2mJ6UfrGr1Aq9Fpt157c",
122 | "track_href": "https://api.spotify.com/v1/tracks/6Q2mJ6UfrGr1Aq9Fpt157c",
123 | "analysis_url":
124 | "https://api.spotify.com/v1/audio-analysis/6Q2mJ6UfrGr1Aq9Fpt157c",
125 | "duration_ms": 211320,
126 | "time_signature": 4
127 | },
128 | {
129 | "danceability": 0.603,
130 | "energy": 0.606,
131 | "key": 3,
132 | "loudness": -5.06,
133 | "mode": 0,
134 | "speechiness": 0.0688,
135 | "acousticness": 0.0108,
136 | "instrumentalness": 0.00157,
137 | "liveness": 0.108,
138 | "valence": 0.403,
139 | "tempo": 93.06,
140 | "type": "audio_features",
141 | "id": "6OHWgUuDDedHRVhcg8vlaf",
142 | "uri": "spotify:track:6OHWgUuDDedHRVhcg8vlaf",
143 | "track_href": "https://api.spotify.com/v1/tracks/6OHWgUuDDedHRVhcg8vlaf",
144 | "analysis_url":
145 | "https://api.spotify.com/v1/audio-analysis/6OHWgUuDDedHRVhcg8vlaf",
146 | "duration_ms": 167173,
147 | "time_signature": 4
148 | },
149 | {
150 | "danceability": 0.76,
151 | "energy": 0.944,
152 | "key": 6,
153 | "loudness": -3.793,
154 | "mode": 0,
155 | "speechiness": 0.0733,
156 | "acousticness": 0.0155,
157 | "instrumentalness": 0.85,
158 | "liveness": 0.363,
159 | "valence": 0.277,
160 | "tempo": 123.985,
161 | "type": "audio_features",
162 | "id": "4rFbNHh4gX7kGTbfCGEOMF",
163 | "uri": "spotify:track:4rFbNHh4gX7kGTbfCGEOMF",
164 | "track_href": "https://api.spotify.com/v1/tracks/4rFbNHh4gX7kGTbfCGEOMF",
165 | "analysis_url":
166 | "https://api.spotify.com/v1/audio-analysis/4rFbNHh4gX7kGTbfCGEOMF",
167 | "duration_ms": 285651,
168 | "time_signature": 4
169 | },
170 | {
171 | "danceability": 0.409,
172 | "energy": 0.532,
173 | "key": 2,
174 | "loudness": -6.266,
175 | "mode": 1,
176 | "speechiness": 0.033,
177 | "acousticness": 0.0745,
178 | "instrumentalness": 0.00523,
179 | "liveness": 0.0795,
180 | "valence": 0.357,
181 | "tempo": 99.935,
182 | "type": "audio_features",
183 | "id": "5Tw2MRW274rpVGrqKdScU6",
184 | "uri": "spotify:track:5Tw2MRW274rpVGrqKdScU6",
185 | "track_href": "https://api.spotify.com/v1/tracks/5Tw2MRW274rpVGrqKdScU6",
186 | "analysis_url":
187 | "https://api.spotify.com/v1/audio-analysis/5Tw2MRW274rpVGrqKdScU6",
188 | "duration_ms": 175480,
189 | "time_signature": 4
190 | },
191 | {
192 | "danceability": 0.697,
193 | "energy": 0.57,
194 | "key": 1,
195 | "loudness": -9.404,
196 | "mode": 1,
197 | "speechiness": 0.442,
198 | "acousticness": 0.463,
199 | "instrumentalness": 0.679,
200 | "liveness": 0.0925,
201 | "valence": 0.101,
202 | "tempo": 129.995,
203 | "type": "audio_features",
204 | "id": "5J1HmHmp6zCsSfHGkaeF53",
205 | "uri": "spotify:track:5J1HmHmp6zCsSfHGkaeF53",
206 | "track_href": "https://api.spotify.com/v1/tracks/5J1HmHmp6zCsSfHGkaeF53",
207 | "analysis_url":
208 | "https://api.spotify.com/v1/audio-analysis/5J1HmHmp6zCsSfHGkaeF53",
209 | "duration_ms": 290120,
210 | "time_signature": 4
211 | },
212 | {
213 | "danceability": 0.597,
214 | "energy": 0.558,
215 | "key": 8,
216 | "loudness": -8.47,
217 | "mode": 0,
218 | "speechiness": 0.0984,
219 | "acousticness": 0.631,
220 | "instrumentalness": 0.42,
221 | "liveness": 0.248,
222 | "valence": 0.178,
223 | "tempo": 134.034,
224 | "type": "audio_features",
225 | "id": "3r2CpMRlTWexGhZhQeJtAo",
226 | "uri": "spotify:track:3r2CpMRlTWexGhZhQeJtAo",
227 | "track_href": "https://api.spotify.com/v1/tracks/3r2CpMRlTWexGhZhQeJtAo",
228 | "analysis_url":
229 | "https://api.spotify.com/v1/audio-analysis/3r2CpMRlTWexGhZhQeJtAo",
230 | "duration_ms": 229091,
231 | "time_signature": 4
232 | },
233 | {
234 | "danceability": 0.793,
235 | "energy": 0.962,
236 | "key": 6,
237 | "loudness": -7.485,
238 | "mode": 0,
239 | "speechiness": 0.0613,
240 | "acousticness": 0.0326,
241 | "instrumentalness": 0.894,
242 | "liveness": 0.272,
243 | "valence": 0.405,
244 | "tempo": 140.025,
245 | "type": "audio_features",
246 | "id": "671PkBSdtMxfSxkfkQ6fpx",
247 | "uri": "spotify:track:671PkBSdtMxfSxkfkQ6fpx",
248 | "track_href": "https://api.spotify.com/v1/tracks/671PkBSdtMxfSxkfkQ6fpx",
249 | "analysis_url":
250 | "https://api.spotify.com/v1/audio-analysis/671PkBSdtMxfSxkfkQ6fpx",
251 | "duration_ms": 181714,
252 | "time_signature": 4
253 | },
254 | {
255 | "danceability": 0.565,
256 | "energy": 0.536,
257 | "key": 7,
258 | "loudness": -7.85,
259 | "mode": 1,
260 | "speechiness": 0.386,
261 | "acousticness": 0.191,
262 | "instrumentalness": 0.0000269,
263 | "liveness": 0.0902,
264 | "valence": 0.903,
265 | "tempo": 200.431,
266 | "type": "audio_features",
267 | "id": "21pNGvUyQ8NjcfRrzge25F",
268 | "uri": "spotify:track:21pNGvUyQ8NjcfRrzge25F",
269 | "track_href": "https://api.spotify.com/v1/tracks/21pNGvUyQ8NjcfRrzge25F",
270 | "analysis_url":
271 | "https://api.spotify.com/v1/audio-analysis/21pNGvUyQ8NjcfRrzge25F",
272 | "duration_ms": 195467,
273 | "time_signature": 4
274 | },
275 | {
276 | "danceability": 0.821,
277 | "energy": 0.406,
278 | "key": 1,
279 | "loudness": -9.465,
280 | "mode": 1,
281 | "speechiness": 0.212,
282 | "acousticness": 0.812,
283 | "instrumentalness": 0.162,
284 | "liveness": 0.107,
285 | "valence": 0.459,
286 | "tempo": 176.077,
287 | "type": "audio_features",
288 | "id": "72BoqrR8GoPacbRly7gaZo",
289 | "uri": "spotify:track:72BoqrR8GoPacbRly7gaZo",
290 | "track_href": "https://api.spotify.com/v1/tracks/72BoqrR8GoPacbRly7gaZo",
291 | "analysis_url":
292 | "https://api.spotify.com/v1/audio-analysis/72BoqrR8GoPacbRly7gaZo",
293 | "duration_ms": 245455,
294 | "time_signature": 4
295 | },
296 | {
297 | "danceability": 0.779,
298 | "energy": 0.74,
299 | "key": 7,
300 | "loudness": -9.084,
301 | "mode": 1,
302 | "speechiness": 0.424,
303 | "acousticness": 0.182,
304 | "instrumentalness": 0.806,
305 | "liveness": 0.0877,
306 | "valence": 0.458,
307 | "tempo": 176.061,
308 | "type": "audio_features",
309 | "id": "0ogPvB4xroolQe1FFcYxFt",
310 | "uri": "spotify:track:0ogPvB4xroolQe1FFcYxFt",
311 | "track_href": "https://api.spotify.com/v1/tracks/0ogPvB4xroolQe1FFcYxFt",
312 | "analysis_url":
313 | "https://api.spotify.com/v1/audio-analysis/0ogPvB4xroolQe1FFcYxFt",
314 | "duration_ms": 170455,
315 | "time_signature": 4
316 | },
317 | {
318 | "danceability": 0.633,
319 | "energy": 0.764,
320 | "key": 3,
321 | "loudness": -4.594,
322 | "mode": 1,
323 | "speechiness": 0.376,
324 | "acousticness": 0.779,
325 | "instrumentalness": 0.000188,
326 | "liveness": 0.408,
327 | "valence": 0.374,
328 | "tempo": 75.52,
329 | "type": "audio_features",
330 | "id": "5vG7iwUFxDI8xSDfya1Ist",
331 | "uri": "spotify:track:5vG7iwUFxDI8xSDfya1Ist",
332 | "track_href": "https://api.spotify.com/v1/tracks/5vG7iwUFxDI8xSDfya1Ist",
333 | "analysis_url":
334 | "https://api.spotify.com/v1/audio-analysis/5vG7iwUFxDI8xSDfya1Ist",
335 | "duration_ms": 219474,
336 | "time_signature": 4
337 | },
338 | {
339 | "danceability": 0.746,
340 | "energy": 0.822,
341 | "key": 5,
342 | "loudness": -6.518,
343 | "mode": 1,
344 | "speechiness": 0.0314,
345 | "acousticness": 0.0235,
346 | "instrumentalness": 0.796,
347 | "liveness": 0.407,
348 | "valence": 0.655,
349 | "tempo": 129.722,
350 | "type": "audio_features",
351 | "id": "1jGHmlZRTfy2WgcpskoFqv",
352 | "uri": "spotify:track:1jGHmlZRTfy2WgcpskoFqv",
353 | "track_href": "https://api.spotify.com/v1/tracks/1jGHmlZRTfy2WgcpskoFqv",
354 | "analysis_url":
355 | "https://api.spotify.com/v1/audio-analysis/1jGHmlZRTfy2WgcpskoFqv",
356 | "duration_ms": 188444,
357 | "time_signature": 4
358 | },
359 | {
360 | "danceability": 0.588,
361 | "energy": 0.702,
362 | "key": 7,
363 | "loudness": -9.029,
364 | "mode": 1,
365 | "speechiness": 0.259,
366 | "acousticness": 0.0696,
367 | "instrumentalness": 0.0445,
368 | "liveness": 0.114,
369 | "valence": 0.33,
370 | "tempo": 104.948,
371 | "type": "audio_features",
372 | "id": "4CzXwuzqEdmZp0N9RmZvqE",
373 | "uri": "spotify:track:4CzXwuzqEdmZp0N9RmZvqE",
374 | "track_href": "https://api.spotify.com/v1/tracks/4CzXwuzqEdmZp0N9RmZvqE",
375 | "analysis_url":
376 | "https://api.spotify.com/v1/audio-analysis/4CzXwuzqEdmZp0N9RmZvqE",
377 | "duration_ms": 260617,
378 | "time_signature": 4
379 | },
380 | {
381 | "danceability": 0.732,
382 | "energy": 0.696,
383 | "key": 0,
384 | "loudness": -5.242,
385 | "mode": 0,
386 | "speechiness": 0.0285,
387 | "acousticness": 0.309,
388 | "instrumentalness": 0.000525,
389 | "liveness": 0.188,
390 | "valence": 0.281,
391 | "tempo": 106.994,
392 | "type": "audio_features",
393 | "id": "2znIzTWdQBzkPSLuXWl14Y",
394 | "uri": "spotify:track:2znIzTWdQBzkPSLuXWl14Y",
395 | "track_href": "https://api.spotify.com/v1/tracks/2znIzTWdQBzkPSLuXWl14Y",
396 | "analysis_url":
397 | "https://api.spotify.com/v1/audio-analysis/2znIzTWdQBzkPSLuXWl14Y",
398 | "duration_ms": 218175,
399 | "time_signature": 4
400 | },
401 | {
402 | "danceability": 0.682,
403 | "energy": 0.476,
404 | "key": 8,
405 | "loudness": -10.228,
406 | "mode": 0,
407 | "speechiness": 0.0663,
408 | "acousticness": 0.274,
409 | "instrumentalness": 0.862,
410 | "liveness": 0.131,
411 | "valence": 0.0523,
412 | "tempo": 186.024,
413 | "type": "audio_features",
414 | "id": "0SLH9b0nlK4larScjLxfSn",
415 | "uri": "spotify:track:0SLH9b0nlK4larScjLxfSn",
416 | "track_href": "https://api.spotify.com/v1/tracks/0SLH9b0nlK4larScjLxfSn",
417 | "analysis_url":
418 | "https://api.spotify.com/v1/audio-analysis/0SLH9b0nlK4larScjLxfSn",
419 | "duration_ms": 143256,
420 | "time_signature": 3
421 | },
422 | {
423 | "danceability": 0.754,
424 | "energy": 0.755,
425 | "key": 10,
426 | "loudness": -5.041,
427 | "mode": 0,
428 | "speechiness": 0.303,
429 | "acousticness": 0.0154,
430 | "instrumentalness": 0.0827,
431 | "liveness": 0.158,
432 | "valence": 0.438,
433 | "tempo": 96.941,
434 | "type": "audio_features",
435 | "id": "3j0oWeUZjTA0HDUS5j64Vm",
436 | "uri": "spotify:track:3j0oWeUZjTA0HDUS5j64Vm",
437 | "track_href": "https://api.spotify.com/v1/tracks/3j0oWeUZjTA0HDUS5j64Vm",
438 | "analysis_url":
439 | "https://api.spotify.com/v1/audio-analysis/3j0oWeUZjTA0HDUS5j64Vm",
440 | "duration_ms": 244967,
441 | "time_signature": 4
442 | },
443 | {
444 | "danceability": 0.702,
445 | "energy": 0.621,
446 | "key": 4,
447 | "loudness": -7.567,
448 | "mode": 1,
449 | "speechiness": 0.323,
450 | "acousticness": 0.274,
451 | "instrumentalness": 0.275,
452 | "liveness": 0.359,
453 | "valence": 0.44,
454 | "tempo": 150.025,
455 | "type": "audio_features",
456 | "id": "1n0LXwUr7HnhPzzagofH6M",
457 | "uri": "spotify:track:1n0LXwUr7HnhPzzagofH6M",
458 | "track_href": "https://api.spotify.com/v1/tracks/1n0LXwUr7HnhPzzagofH6M",
459 | "analysis_url":
460 | "https://api.spotify.com/v1/audio-analysis/1n0LXwUr7HnhPzzagofH6M",
461 | "duration_ms": 270400,
462 | "time_signature": 4
463 | },
464 | {
465 | "danceability": 0.689,
466 | "energy": 0.739,
467 | "key": 2,
468 | "loudness": -5.81,
469 | "mode": 1,
470 | "speechiness": 0.026,
471 | "acousticness": 0.0000151,
472 | "instrumentalness": 0.509,
473 | "liveness": 0.064,
474 | "valence": 0.578,
475 | "tempo": 120.423,
476 | "type": "audio_features",
477 | "id": "0q6LuUqGLUiCPP1cbdwFs3",
478 | "uri": "spotify:track:0q6LuUqGLUiCPP1cbdwFs3",
479 | "track_href": "https://api.spotify.com/v1/tracks/0q6LuUqGLUiCPP1cbdwFs3",
480 | "analysis_url":
481 | "https://api.spotify.com/v1/audio-analysis/0q6LuUqGLUiCPP1cbdwFs3",
482 | "duration_ms": 233867,
483 | "time_signature": 4
484 | },
485 | {
486 | "danceability": 0.794,
487 | "energy": 0.476,
488 | "key": 2,
489 | "loudness": -4.337,
490 | "mode": 0,
491 | "speechiness": 0.0558,
492 | "acousticness": 0.354,
493 | "instrumentalness": 0.00307,
494 | "liveness": 0.332,
495 | "valence": 0.471,
496 | "tempo": 129.827,
497 | "type": "audio_features",
498 | "id": "5mQjdE8ujLsXjBrlOkUYBg",
499 | "uri": "spotify:track:5mQjdE8ujLsXjBrlOkUYBg",
500 | "track_href": "https://api.spotify.com/v1/tracks/5mQjdE8ujLsXjBrlOkUYBg",
501 | "analysis_url":
502 | "https://api.spotify.com/v1/audio-analysis/5mQjdE8ujLsXjBrlOkUYBg",
503 | "duration_ms": 179658,
504 | "time_signature": 4
505 | },
506 | {
507 | "danceability": 0.782,
508 | "energy": 0.637,
509 | "key": 4,
510 | "loudness": -9.32,
511 | "mode": 0,
512 | "speechiness": 0.0495,
513 | "acousticness": 0.627,
514 | "instrumentalness": 0.935,
515 | "liveness": 0.643,
516 | "valence": 0.229,
517 | "tempo": 120.003,
518 | "type": "audio_features",
519 | "id": "0Z1JVybyPZLijAHIPdRhj5",
520 | "uri": "spotify:track:0Z1JVybyPZLijAHIPdRhj5",
521 | "track_href": "https://api.spotify.com/v1/tracks/0Z1JVybyPZLijAHIPdRhj5",
522 | "analysis_url":
523 | "https://api.spotify.com/v1/audio-analysis/0Z1JVybyPZLijAHIPdRhj5",
524 | "duration_ms": 524216,
525 | "time_signature": 4
526 | },
527 | {
528 | "danceability": 0.449,
529 | "energy": 0.261,
530 | "key": 8,
531 | "loudness": -14.24,
532 | "mode": 1,
533 | "speechiness": 0.287,
534 | "acousticness": 0.764,
535 | "instrumentalness": 0.365,
536 | "liveness": 0.106,
537 | "valence": 0.273,
538 | "tempo": 167.881,
539 | "type": "audio_features",
540 | "id": "2T3WiD2mYsMxPfyylvh2XB",
541 | "uri": "spotify:track:2T3WiD2mYsMxPfyylvh2XB",
542 | "track_href": "https://api.spotify.com/v1/tracks/2T3WiD2mYsMxPfyylvh2XB",
543 | "analysis_url":
544 | "https://api.spotify.com/v1/audio-analysis/2T3WiD2mYsMxPfyylvh2XB",
545 | "duration_ms": 238080,
546 | "time_signature": 5
547 | },
548 | {
549 | "danceability": 0.881,
550 | "energy": 0.644,
551 | "key": 1,
552 | "loudness": -7.134,
553 | "mode": 0,
554 | "speechiness": 0.103,
555 | "acousticness": 0.644,
556 | "instrumentalness": 0.409,
557 | "liveness": 0.116,
558 | "valence": 0.536,
559 | "tempo": 110.026,
560 | "type": "audio_features",
561 | "id": "2EEZdCeGp095urJDE3wEpS",
562 | "uri": "spotify:track:2EEZdCeGp095urJDE3wEpS",
563 | "track_href": "https://api.spotify.com/v1/tracks/2EEZdCeGp095urJDE3wEpS",
564 | "analysis_url":
565 | "https://api.spotify.com/v1/audio-analysis/2EEZdCeGp095urJDE3wEpS",
566 | "duration_ms": 216033,
567 | "time_signature": 4
568 | },
569 | {
570 | "danceability": 0.561,
571 | "energy": 0.641,
572 | "key": 10,
573 | "loudness": -12.766,
574 | "mode": 1,
575 | "speechiness": 0.0712,
576 | "acousticness": 0.0549,
577 | "instrumentalness": 0.857,
578 | "liveness": 0.361,
579 | "valence": 0.225,
580 | "tempo": 99.91,
581 | "type": "audio_features",
582 | "id": "5yGWnEnLinugMV5TN1udip",
583 | "uri": "spotify:track:5yGWnEnLinugMV5TN1udip",
584 | "track_href": "https://api.spotify.com/v1/tracks/5yGWnEnLinugMV5TN1udip",
585 | "analysis_url":
586 | "https://api.spotify.com/v1/audio-analysis/5yGWnEnLinugMV5TN1udip",
587 | "duration_ms": 232573,
588 | "time_signature": 4
589 | },
590 | {
591 | "danceability": 0.602,
592 | "energy": 0.304,
593 | "key": 2,
594 | "loudness": -9.948,
595 | "mode": 0,
596 | "speechiness": 0.0736,
597 | "acousticness": 0.345,
598 | "instrumentalness": 0.0231,
599 | "liveness": 0.0969,
600 | "valence": 0.156,
601 | "tempo": 73.33,
602 | "type": "audio_features",
603 | "id": "6J6FXmYrMxg93mBxkugUlI",
604 | "uri": "spotify:track:6J6FXmYrMxg93mBxkugUlI",
605 | "track_href": "https://api.spotify.com/v1/tracks/6J6FXmYrMxg93mBxkugUlI",
606 | "analysis_url":
607 | "https://api.spotify.com/v1/audio-analysis/6J6FXmYrMxg93mBxkugUlI",
608 | "duration_ms": 240760,
609 | "time_signature": 4
610 | },
611 | {
612 | "danceability": 0.745,
613 | "energy": 0.764,
614 | "key": 0,
615 | "loudness": -6.736,
616 | "mode": 1,
617 | "speechiness": 0.0407,
618 | "acousticness": 0.253,
619 | "instrumentalness": 0.672,
620 | "liveness": 0.574,
621 | "valence": 0.079,
622 | "tempo": 135.99,
623 | "type": "audio_features",
624 | "id": "2jFiRrskwsTAnyT29sYKcH",
625 | "uri": "spotify:track:2jFiRrskwsTAnyT29sYKcH",
626 | "track_href": "https://api.spotify.com/v1/tracks/2jFiRrskwsTAnyT29sYKcH",
627 | "analysis_url":
628 | "https://api.spotify.com/v1/audio-analysis/2jFiRrskwsTAnyT29sYKcH",
629 | "duration_ms": 225772,
630 | "time_signature": 4
631 | },
632 | {
633 | "danceability": 0.558,
634 | "energy": 0.664,
635 | "key": 4,
636 | "loudness": -12.613,
637 | "mode": 0,
638 | "speechiness": 0.0544,
639 | "acousticness": 0.316,
640 | "instrumentalness": 0.701,
641 | "liveness": 0.118,
642 | "valence": 0.138,
643 | "tempo": 119.994,
644 | "type": "audio_features",
645 | "id": "7koY53OPZkEhwmHG5qmsKg",
646 | "uri": "spotify:track:7koY53OPZkEhwmHG5qmsKg",
647 | "track_href": "https://api.spotify.com/v1/tracks/7koY53OPZkEhwmHG5qmsKg",
648 | "analysis_url":
649 | "https://api.spotify.com/v1/audio-analysis/7koY53OPZkEhwmHG5qmsKg",
650 | "duration_ms": 347440,
651 | "time_signature": 4
652 | },
653 | {
654 | "danceability": 0.904,
655 | "energy": 0.451,
656 | "key": 0,
657 | "loudness": -8.942,
658 | "mode": 0,
659 | "speechiness": 0.112,
660 | "acousticness": 0.000997,
661 | "instrumentalness": 0.00113,
662 | "liveness": 0.107,
663 | "valence": 0.102,
664 | "tempo": 128.012,
665 | "type": "audio_features",
666 | "id": "2kkrHMewtpssENwGI3kTsX",
667 | "uri": "spotify:track:2kkrHMewtpssENwGI3kTsX",
668 | "track_href": "https://api.spotify.com/v1/tracks/2kkrHMewtpssENwGI3kTsX",
669 | "analysis_url":
670 | "https://api.spotify.com/v1/audio-analysis/2kkrHMewtpssENwGI3kTsX",
671 | "duration_ms": 350391,
672 | "time_signature": 4
673 | },
674 | {
675 | "danceability": 0.654,
676 | "energy": 0.994,
677 | "key": 10,
678 | "loudness": -2.392,
679 | "mode": 0,
680 | "speechiness": 0.0584,
681 | "acousticness": 0.00157,
682 | "instrumentalness": 0.883,
683 | "liveness": 0.32,
684 | "valence": 0.314,
685 | "tempo": 127.995,
686 | "type": "audio_features",
687 | "id": "02JrlW4pIfzF6rouxZVFsY",
688 | "uri": "spotify:track:02JrlW4pIfzF6rouxZVFsY",
689 | "track_href": "https://api.spotify.com/v1/tracks/02JrlW4pIfzF6rouxZVFsY",
690 | "analysis_url":
691 | "https://api.spotify.com/v1/audio-analysis/02JrlW4pIfzF6rouxZVFsY",
692 | "duration_ms": 260876,
693 | "time_signature": 4
694 | },
695 | {
696 | "danceability": 0.689,
697 | "energy": 0.575,
698 | "key": 6,
699 | "loudness": -9.085,
700 | "mode": 0,
701 | "speechiness": 0.0883,
702 | "acousticness": 0.733,
703 | "instrumentalness": 0.0005,
704 | "liveness": 0.112,
705 | "valence": 0.249,
706 | "tempo": 127.892,
707 | "type": "audio_features",
708 | "id": "5Ag5Dg9gWQTBVQGjJyXt2l",
709 | "uri": "spotify:track:5Ag5Dg9gWQTBVQGjJyXt2l",
710 | "track_href": "https://api.spotify.com/v1/tracks/5Ag5Dg9gWQTBVQGjJyXt2l",
711 | "analysis_url":
712 | "https://api.spotify.com/v1/audio-analysis/5Ag5Dg9gWQTBVQGjJyXt2l",
713 | "duration_ms": 216099,
714 | "time_signature": 4
715 | },
716 | {
717 | "danceability": 0.673,
718 | "energy": 0.863,
719 | "key": 0,
720 | "loudness": -7.496,
721 | "mode": 0,
722 | "speechiness": 0.0577,
723 | "acousticness": 0.0031,
724 | "instrumentalness": 0.924,
725 | "liveness": 0.164,
726 | "valence": 0.261,
727 | "tempo": 125.981,
728 | "type": "audio_features",
729 | "id": "66hLWtkrv53rFKZseItS8W",
730 | "uri": "spotify:track:66hLWtkrv53rFKZseItS8W",
731 | "track_href": "https://api.spotify.com/v1/tracks/66hLWtkrv53rFKZseItS8W",
732 | "analysis_url":
733 | "https://api.spotify.com/v1/audio-analysis/66hLWtkrv53rFKZseItS8W",
734 | "duration_ms": 500985,
735 | "time_signature": 4
736 | },
737 | {
738 | "danceability": 0.696,
739 | "energy": 0.441,
740 | "key": 2,
741 | "loudness": -6.403,
742 | "mode": 1,
743 | "speechiness": 0.104,
744 | "acousticness": 0.475,
745 | "instrumentalness": 0.000018,
746 | "liveness": 0.106,
747 | "valence": 0.438,
748 | "tempo": 134.903,
749 | "type": "audio_features",
750 | "id": "4edCb2ONTusFsKNQiLYgAW",
751 | "uri": "spotify:track:4edCb2ONTusFsKNQiLYgAW",
752 | "track_href": "https://api.spotify.com/v1/tracks/4edCb2ONTusFsKNQiLYgAW",
753 | "analysis_url":
754 | "https://api.spotify.com/v1/audio-analysis/4edCb2ONTusFsKNQiLYgAW",
755 | "duration_ms": 272525,
756 | "time_signature": 4
757 | },
758 | {
759 | "danceability": 0.383,
760 | "energy": 0.621,
761 | "key": 6,
762 | "loudness": -5.878,
763 | "mode": 0,
764 | "speechiness": 0.113,
765 | "acousticness": 0.0272,
766 | "instrumentalness": 0,
767 | "liveness": 0.427,
768 | "valence": 0.213,
769 | "tempo": 80.051,
770 | "type": "audio_features",
771 | "id": "2oJInAFXiQYLgz0jh9MrXf",
772 | "uri": "spotify:track:2oJInAFXiQYLgz0jh9MrXf",
773 | "track_href": "https://api.spotify.com/v1/tracks/2oJInAFXiQYLgz0jh9MrXf",
774 | "analysis_url":
775 | "https://api.spotify.com/v1/audio-analysis/2oJInAFXiQYLgz0jh9MrXf",
776 | "duration_ms": 181907,
777 | "time_signature": 4
778 | },
779 | {
780 | "danceability": 0.551,
781 | "energy": 0.652,
782 | "key": 9,
783 | "loudness": -8.771,
784 | "mode": 0,
785 | "speechiness": 0.144,
786 | "acousticness": 0.144,
787 | "instrumentalness": 0.000277,
788 | "liveness": 0.403,
789 | "valence": 0.558,
790 | "tempo": 96.062,
791 | "type": "audio_features",
792 | "id": "7cOrjHdCqBABTwErAV8z1v",
793 | "uri": "spotify:track:7cOrjHdCqBABTwErAV8z1v",
794 | "track_href": "https://api.spotify.com/v1/tracks/7cOrjHdCqBABTwErAV8z1v",
795 | "analysis_url":
796 | "https://api.spotify.com/v1/audio-analysis/7cOrjHdCqBABTwErAV8z1v",
797 | "duration_ms": 245268,
798 | "time_signature": 4
799 | },
800 | {
801 | "danceability": 0.477,
802 | "energy": 0.329,
803 | "key": 5,
804 | "loudness": -8.981,
805 | "mode": 1,
806 | "speechiness": 0.0331,
807 | "acousticness": 0.878,
808 | "instrumentalness": 0.733,
809 | "liveness": 0.141,
810 | "valence": 0.304,
811 | "tempo": 135.668,
812 | "type": "audio_features",
813 | "id": "3tNTfuiVxvOSerHXbCI7MP",
814 | "uri": "spotify:track:3tNTfuiVxvOSerHXbCI7MP",
815 | "track_href": "https://api.spotify.com/v1/tracks/3tNTfuiVxvOSerHXbCI7MP",
816 | "analysis_url":
817 | "https://api.spotify.com/v1/audio-analysis/3tNTfuiVxvOSerHXbCI7MP",
818 | "duration_ms": 115700,
819 | "time_signature": 4
820 | },
821 | {
822 | "danceability": 0.401,
823 | "energy": 0.51,
824 | "key": 11,
825 | "loudness": -6.284,
826 | "mode": 1,
827 | "speechiness": 0.0785,
828 | "acousticness": 0.343,
829 | "instrumentalness": 0.000225,
830 | "liveness": 0.151,
831 | "valence": 0.348,
832 | "tempo": 139.888,
833 | "type": "audio_features",
834 | "id": "6VDfg9kdGhEb4McPwJpp5t",
835 | "uri": "spotify:track:6VDfg9kdGhEb4McPwJpp5t",
836 | "track_href": "https://api.spotify.com/v1/tracks/6VDfg9kdGhEb4McPwJpp5t",
837 | "analysis_url":
838 | "https://api.spotify.com/v1/audio-analysis/6VDfg9kdGhEb4McPwJpp5t",
839 | "duration_ms": 328631,
840 | "time_signature": 4
841 | },
842 | {
843 | "danceability": 0.395,
844 | "energy": 0.708,
845 | "key": 9,
846 | "loudness": -6.467,
847 | "mode": 0,
848 | "speechiness": 0.1,
849 | "acousticness": 0.2,
850 | "instrumentalness": 0.00654,
851 | "liveness": 0.0909,
852 | "valence": 0.319,
853 | "tempo": 120.402,
854 | "type": "audio_features",
855 | "id": "3YBld9PpvUxYzruAmAj1QF",
856 | "uri": "spotify:track:3YBld9PpvUxYzruAmAj1QF",
857 | "track_href": "https://api.spotify.com/v1/tracks/3YBld9PpvUxYzruAmAj1QF",
858 | "analysis_url":
859 | "https://api.spotify.com/v1/audio-analysis/3YBld9PpvUxYzruAmAj1QF",
860 | "duration_ms": 378477,
861 | "time_signature": 4
862 | },
863 | {
864 | "danceability": 0.652,
865 | "energy": 0.769,
866 | "key": 6,
867 | "loudness": -6.45,
868 | "mode": 1,
869 | "speechiness": 0.0381,
870 | "acousticness": 0.00135,
871 | "instrumentalness": 0.566,
872 | "liveness": 0.107,
873 | "valence": 0.482,
874 | "tempo": 96.982,
875 | "type": "audio_features",
876 | "id": "63OIimPryUsrm8DGaU4O4f",
877 | "uri": "spotify:track:63OIimPryUsrm8DGaU4O4f",
878 | "track_href": "https://api.spotify.com/v1/tracks/63OIimPryUsrm8DGaU4O4f",
879 | "analysis_url":
880 | "https://api.spotify.com/v1/audio-analysis/63OIimPryUsrm8DGaU4O4f",
881 | "duration_ms": 150073,
882 | "time_signature": 4
883 | },
884 | {
885 | "danceability": 0.69,
886 | "energy": 0.614,
887 | "key": 8,
888 | "loudness": -7.016,
889 | "mode": 1,
890 | "speechiness": 0.124,
891 | "acousticness": 0.0543,
892 | "instrumentalness": 0.00637,
893 | "liveness": 0.349,
894 | "valence": 0.206,
895 | "tempo": 98.058,
896 | "type": "audio_features",
897 | "id": "535mcFQXTugp36uiSaTVNk",
898 | "uri": "spotify:track:535mcFQXTugp36uiSaTVNk",
899 | "track_href": "https://api.spotify.com/v1/tracks/535mcFQXTugp36uiSaTVNk",
900 | "analysis_url":
901 | "https://api.spotify.com/v1/audio-analysis/535mcFQXTugp36uiSaTVNk",
902 | "duration_ms": 132544,
903 | "time_signature": 4
904 | },
905 | {
906 | "danceability": 0.643,
907 | "energy": 0.66,
908 | "key": 1,
909 | "loudness": -7.725,
910 | "mode": 1,
911 | "speechiness": 0.0368,
912 | "acousticness": 0.00277,
913 | "instrumentalness": 0.131,
914 | "liveness": 0.382,
915 | "valence": 0.322,
916 | "tempo": 102.001,
917 | "type": "audio_features",
918 | "id": "1ebiFSKKdo5aEa8y2QzQqy",
919 | "uri": "spotify:track:1ebiFSKKdo5aEa8y2QzQqy",
920 | "track_href": "https://api.spotify.com/v1/tracks/1ebiFSKKdo5aEa8y2QzQqy",
921 | "analysis_url":
922 | "https://api.spotify.com/v1/audio-analysis/1ebiFSKKdo5aEa8y2QzQqy",
923 | "duration_ms": 292973,
924 | "time_signature": 4
925 | },
926 | {
927 | "danceability": 0.601,
928 | "energy": 0.883,
929 | "key": 7,
930 | "loudness": -4.396,
931 | "mode": 0,
932 | "speechiness": 0.0574,
933 | "acousticness": 0.0927,
934 | "instrumentalness": 0.909,
935 | "liveness": 0.0933,
936 | "valence": 0.187,
937 | "tempo": 125.97,
938 | "type": "audio_features",
939 | "id": "1yxNmvGetdte8Tqz6LBbfV",
940 | "uri": "spotify:track:1yxNmvGetdte8Tqz6LBbfV",
941 | "track_href": "https://api.spotify.com/v1/tracks/1yxNmvGetdte8Tqz6LBbfV",
942 | "analysis_url":
943 | "https://api.spotify.com/v1/audio-analysis/1yxNmvGetdte8Tqz6LBbfV",
944 | "duration_ms": 219048,
945 | "time_signature": 4
946 | },
947 | {
948 | "danceability": 0.763,
949 | "energy": 0.827,
950 | "key": 11,
951 | "loudness": -1.956,
952 | "mode": 1,
953 | "speechiness": 0.0487,
954 | "acousticness": 0.00499,
955 | "instrumentalness": 0.772,
956 | "liveness": 0.0627,
957 | "valence": 0.227,
958 | "tempo": 125.008,
959 | "type": "audio_features",
960 | "id": "4wtyVaBlDmssq5sj1cwF9S",
961 | "uri": "spotify:track:4wtyVaBlDmssq5sj1cwF9S",
962 | "track_href": "https://api.spotify.com/v1/tracks/4wtyVaBlDmssq5sj1cwF9S",
963 | "analysis_url":
964 | "https://api.spotify.com/v1/audio-analysis/4wtyVaBlDmssq5sj1cwF9S",
965 | "duration_ms": 278400,
966 | "time_signature": 4
967 | },
968 | {
969 | "danceability": 0.576,
970 | "energy": 0.283,
971 | "key": 2,
972 | "loudness": -13.004,
973 | "mode": 1,
974 | "speechiness": 0.039,
975 | "acousticness": 0.897,
976 | "instrumentalness": 0.892,
977 | "liveness": 0.0841,
978 | "valence": 0.252,
979 | "tempo": 119.277,
980 | "type": "audio_features",
981 | "id": "1jJSWxXVeKpW4FtqxLrC5l",
982 | "uri": "spotify:track:1jJSWxXVeKpW4FtqxLrC5l",
983 | "track_href": "https://api.spotify.com/v1/tracks/1jJSWxXVeKpW4FtqxLrC5l",
984 | "analysis_url":
985 | "https://api.spotify.com/v1/audio-analysis/1jJSWxXVeKpW4FtqxLrC5l",
986 | "duration_ms": 208452,
987 | "time_signature": 4
988 | },
989 | {
990 | "danceability": 0.844,
991 | "energy": 0.586,
992 | "key": 8,
993 | "loudness": -12.57,
994 | "mode": 0,
995 | "speechiness": 0.125,
996 | "acousticness": 0.01,
997 | "instrumentalness": 0.924,
998 | "liveness": 0.0872,
999 | "valence": 0.542,
1000 | "tempo": 125.012,
1001 | "type": "audio_features",
1002 | "id": "6l9YOkb1DBafYngYMQXqTJ",
1003 | "uri": "spotify:track:6l9YOkb1DBafYngYMQXqTJ",
1004 | "track_href": "https://api.spotify.com/v1/tracks/6l9YOkb1DBafYngYMQXqTJ",
1005 | "analysis_url":
1006 | "https://api.spotify.com/v1/audio-analysis/6l9YOkb1DBafYngYMQXqTJ",
1007 | "duration_ms": 201398,
1008 | "time_signature": 3
1009 | },
1010 | {
1011 | "danceability": 0.458,
1012 | "energy": 0.789,
1013 | "key": 0,
1014 | "loudness": -5.353,
1015 | "mode": 0,
1016 | "speechiness": 0.143,
1017 | "acousticness": 0.253,
1018 | "instrumentalness": 0.388,
1019 | "liveness": 0.112,
1020 | "valence": 0.123,
1021 | "tempo": 144.085,
1022 | "type": "audio_features",
1023 | "id": "6FJTYiO3CsMzjXZLdyz6vS",
1024 | "uri": "spotify:track:6FJTYiO3CsMzjXZLdyz6vS",
1025 | "track_href": "https://api.spotify.com/v1/tracks/6FJTYiO3CsMzjXZLdyz6vS",
1026 | "analysis_url":
1027 | "https://api.spotify.com/v1/audio-analysis/6FJTYiO3CsMzjXZLdyz6vS",
1028 | "duration_ms": 186658,
1029 | "time_signature": 4
1030 | },
1031 | {
1032 | "danceability": 0.325,
1033 | "energy": 0.578,
1034 | "key": 2,
1035 | "loudness": -6.786,
1036 | "mode": 1,
1037 | "speechiness": 0.0455,
1038 | "acousticness": 0.158,
1039 | "instrumentalness": 0.00479,
1040 | "liveness": 0.575,
1041 | "valence": 0.103,
1042 | "tempo": 144.745,
1043 | "type": "audio_features",
1044 | "id": "1TOKaiIbpxknH9qvEv8F00",
1045 | "uri": "spotify:track:1TOKaiIbpxknH9qvEv8F00",
1046 | "track_href": "https://api.spotify.com/v1/tracks/1TOKaiIbpxknH9qvEv8F00",
1047 | "analysis_url":
1048 | "https://api.spotify.com/v1/audio-analysis/1TOKaiIbpxknH9qvEv8F00",
1049 | "duration_ms": 315724,
1050 | "time_signature": 4
1051 | }
1052 | ]
1053 |
--------------------------------------------------------------------------------