From d20a58dbdb73089007c602c9a6f1d0b7c42aeaa4 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 15:48:32 -0400 Subject: [PATCH 1/6] Get recomendations based on metrics --- package-lock.json | 179 +++++++++++++++++++++++- package.json | 4 +- src/components/NowPlaying.tsx | 29 +++- src/components/RecommendationsModal.tsx | 49 +++++++ src/components/SongMetric.tsx | 5 +- src/css/app.module.scss | 11 ++ src/types/enhancify.d.ts | 4 + src/types/spotify-web-api.d.ts | 100 ++++++------- 8 files changed, 319 insertions(+), 62 deletions(-) create mode 100644 src/components/RecommendationsModal.tsx diff --git a/package-lock.json b/package-lock.json index 5bebc9b..938db6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,13 @@ "@spotify/web-api-ts-sdk": "^1.2.0", "@types/jquery": "^3.5.31", "jquery": "^3.7.1", - "react-circular-progressbar": "^2.1.0" + "react-circular-progressbar": "^2.1.0", + "react-modal": "^3.16.1" }, "devDependencies": { "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-modal": "^3.16.3", "spicetify-creator": "^1.0.17" } }, @@ -81,6 +83,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/sizzle": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", @@ -802,6 +813,11 @@ "node": ">=6" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -1214,8 +1230,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", @@ -1299,7 +1314,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -1420,6 +1434,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1624,6 +1646,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -1653,6 +1685,47 @@ "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1756,6 +1829,15 @@ "dev": true, "optional": true }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -1978,6 +2060,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -2049,6 +2139,15 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/sizzle": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", @@ -2452,6 +2551,11 @@ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -2760,8 +2864,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "peer": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "jsonfile": { "version": "6.1.0", @@ -2825,7 +2928,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -2904,6 +3006,11 @@ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3041,6 +3148,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -3063,6 +3180,37 @@ "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==", "requires": {} }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3144,6 +3292,15 @@ "dev": true, "optional": true }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -3294,6 +3451,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 2eb0d3b..c169767 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "devDependencies": { "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-modal": "^3.16.3", "spicetify-creator": "^1.0.17" }, "dependencies": { "@spotify/web-api-ts-sdk": "^1.2.0", "@types/jquery": "^3.5.31", "jquery": "^3.7.1", - "react-circular-progressbar": "^2.1.0" + "react-circular-progressbar": "^2.1.0", + "react-modal": "^3.16.1" } } diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 7357ebf..89e135f 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -4,14 +4,17 @@ import getAudioFeatures from "../services/nowPlayingService"; import { AudioFeaturesResponse } from "../types/spotify-web-api"; import DynamicRecommendations from "./DynamicRecommendations"; import SongMetric from "./SongMetric"; -import { SongMetricData } from "../types/enhancify"; +import { SelectedMetrics, SongMetricData } from "../types/enhancify"; import { allMetrics, getSongMetrics } from "../services/enhancifyInternalService"; +import RecommendationsModal from "./RecommendationsModal"; class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string, recTarget: string, songMetrics: SongMetricData[], - metricsToDisplay: string[]}> { + metricsToDisplay: string[], + modalIsOpen: boolean, + selectedMetrics: SelectedMetrics}> { state = { audioFeatures: {}, // Features of the currently playing song (name, artist, stats) @@ -19,6 +22,8 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon recTarget: "songs", // Recommendations based on either songs or artist songMetrics: [], // Current song metric information metricsToDisplay: Spicetify.LocalStorage.get("metricsToDisplay") != "" ? Spicetify.LocalStorage.get("metricsToDisplay")?.split(',') || ["Danceability", "Energy", "Acousticness", "Loudness", "Key", "Tempo"] : [], // Current metric information types + modalIsOpen: false, // Whether the modal is currently open + selectedMetrics: {}, } componentDidMount = () => { @@ -26,6 +31,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } setAudioFeatures = () => { + this.setState({selectedMetrics: {}}) // Check if there is no currently playing song or // if the info of the song is currently being displayed @@ -85,6 +91,19 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon }, this.setSongMetrics); } + setModalIsOpen = (value: boolean) => { + this.setState({ + modalIsOpen: value + }); + } + + selectMetric = (metric: string, value: string) => { + (this.state.selectedMetrics as SelectedMetrics)[metric] = value; + this.setState({ + selectedMetrics: this.state.selectedMetrics + }); + } + render() { Spicetify.Player.addEventListener("songchange", this.setAudioFeatures); @@ -164,11 +183,14 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon
{"Song Statistics"}
+
{/* Stats block data */} {this.state.songMetrics.map((songMetric: SongMetricData, i) => { - return ; + return ; })}
@@ -192,6 +214,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon })} + { this.state.modalIsOpen ? : <> } ); } diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx new file mode 100644 index 0000000..9ffa59f --- /dev/null +++ b/src/components/RecommendationsModal.tsx @@ -0,0 +1,49 @@ +import styles from "../css/app.module.scss"; +import React from "react"; +import Modal from 'react-modal'; +import { GetRecommendationsInput, GetRecommendationsResponse, RecommendationsInput } from "../types/spotify-web-api.d"; +import getRecommendations from "../services/dynamicRecommendationsService"; +import { SelectedMetrics } from "../types/enhancify"; +import getID from './../services/common'; + +class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setModalIsOpen: (value: boolean) => void, songURI: string, selectedMetrics: SelectedMetrics}, {recommendations: GetRecommendationsResponse | {}}> { + + state = { + recommendations: {} + }; + + componentDidMount = () => { + this.generateRecommendations(); + } + + // Generate recommendations by sending a request to the spotify API + generateRecommendations = async () => { + + // Prepare the recommendations to send to the server + let apiOptions = new GetRecommendationsInput(); + apiOptions.data.seed_tracks = getID(this.props.songURI); + + for (let key in this.props.selectedMetrics) { + let apiDataKey = "target_" + key.toLowerCase(); + apiOptions.data[apiDataKey as keyof RecommendationsInput] = this.props.selectedMetrics[key]; + } + + // Make the API call + var recommendations = await getRecommendations(apiOptions); + this.setState({ + recommendations: recommendations, + }); + }; + + render() { + return ( + this.props.setModalIsOpen(false)}> + + {JSON.stringify(this.props.selectedMetrics)} + {JSON.stringify(this.state.recommendations)} + + ); + } +} + +export default RecommendationsModal; diff --git a/src/components/SongMetric.tsx b/src/components/SongMetric.tsx index 09e6c5a..ae4b485 100644 --- a/src/components/SongMetric.tsx +++ b/src/components/SongMetric.tsx @@ -7,12 +7,13 @@ import 'react-circular-progressbar/dist/styles.css'; class SongMetric extends React.Component<{floatValue: string, title: string, progressBar: boolean, - label: string}, + label: string, + selectMetric: (metric: string, value: string) => void}, {}> { render() { return ( -
+
this.props.selectMetric(this.props.title, this.props.floatValue)}>
{this.props.title} diff --git a/src/css/app.module.scss b/src/css/app.module.scss index 03cb32e..52f09f8 100644 --- a/src/css/app.module.scss +++ b/src/css/app.module.scss @@ -245,4 +245,15 @@ font-size: larger; font-weight: 500; color: white; +} + +.modal { + background-color: black; + height: 80%; + width: 80%; + margin-right: 50px; + margin-top: 50px; + margin-left: 50px; + margin-bottom: 50px; + text-wrap: wrap; } \ No newline at end of file diff --git a/src/types/enhancify.d.ts b/src/types/enhancify.d.ts index 2ab7859..3df84f1 100644 --- a/src/types/enhancify.d.ts +++ b/src/types/enhancify.d.ts @@ -17,3 +17,7 @@ export type MetricFeatures = { progressbar: Set, label: Labels }; + +export type SelectedMetrics = { + [metric: string]: string +}; diff --git a/src/types/spotify-web-api.d.ts b/src/types/spotify-web-api.d.ts index eb5971e..4e33947 100644 --- a/src/types/spotify-web-api.d.ts +++ b/src/types/spotify-web-api.d.ts @@ -20,55 +20,57 @@ export type AudioFeaturesResponse = { }; export class GetRecommendationsInput { - data = { - limit: "6", - market: "", - seed_artists: "", - seed_genres: "", - seed_tracks: "", - min_acousticness: "", - max_acousticness: "", - target_acousticness: "", - min_danceability: "", - max_danceability: "", - target_danceability: "", - min_duration_ms: "", - max_duration_ms: "", - target_duration_ms: "", - min_energy: "", - max_energy: "", - target_energy: "", - min_instrumentalness: "", - max_instrumentalness: "", - target_instrumentalness: "", - min_key: "", - max_key: "", - target_key: "", - min_liveness: "", - max_liveness: "", - target_liveness: "", - min_loudness: "", - max_loudness: "", - target_loudness: "", - min_mode: "", - max_mode: "", - target_mode: "", - min_popularity: "", - max_popularity: "", - target_popularity: "", - min_speechiness: "", - max_speechiness: "", - target_speechiness: "", - min_tempo: "", - max_tempo: "", - target_tempo: "", - min_time_signature: "", - max_time_signature: "", - target_time_signature: "", - min_valence: "", - max_valence: "", - target_valence: "", - }; + data = new RecommendationsInput(); +}; + +export class RecommendationsInput { + limit = "6"; + market = ""; + seed_artists = ""; + seed_genres = ""; + seed_tracks = ""; + min_acousticness = ""; + max_acousticness = ""; + target_acousticness = ""; + min_danceability = ""; + max_danceability = ""; + target_danceability = ""; + min_duration_ms = ""; + max_duration_ms = ""; + target_duration_ms = ""; + min_energy = ""; + max_energy = ""; + target_energy = ""; + min_instrumentalness = ""; + max_instrumentalness = ""; + target_instrumentalness = ""; + min_key = ""; + max_key = ""; + target_key = ""; + min_liveness = ""; + max_liveness = ""; + target_liveness = ""; + min_loudness = ""; + max_loudness = ""; + target_loudness = ""; + min_mode = ""; + max_mode = ""; + target_mode = ""; + min_popularity = ""; + max_popularity = ""; + target_popularity = ""; + min_speechiness = ""; + max_speechiness = ""; + target_speechiness = ""; + min_tempo = ""; + max_tempo = ""; + target_tempo = ""; + min_time_signature = ""; + max_time_signature = ""; + target_time_signature = ""; + min_valence = ""; + max_valence = ""; + target_valence = ""; }; export type GetRecommendationsResponse = { From b0b2f59be279cfcf43ad5a0ae47fac0657612536 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 16:05:41 -0400 Subject: [PATCH 2/6] Allow removing metrics and use normal recommendations render --- src/components/DynamicRecommendations.tsx | 20 ++------------------ src/components/NowPlaying.tsx | 7 ++++++- src/components/RecommendationsModal.tsx | 4 +++- src/services/enhancifyInternalService.tsx | 23 ++++++++++++++++++++++- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/components/DynamicRecommendations.tsx b/src/components/DynamicRecommendations.tsx index 4b87cc6..8b4c829 100644 --- a/src/components/DynamicRecommendations.tsx +++ b/src/components/DynamicRecommendations.tsx @@ -4,6 +4,7 @@ import getRecommendations from "../services/dynamicRecommendationsService"; import { GetRecommendationsInput, GetRecommendationsResponse } from "../types/spotify-web-api.d"; import getID from './../services/common'; import RecommendedTrack from "./RecommendedTrack"; +import { RecommendationsRender } from "../services/enhancifyInternalService"; class DynamicRecommendations extends React.Component<{recTargetProp : string}, {songQueue: Array, artistQueue: Array, recTarget: string, recommendations: GetRecommendationsResponse | {}}> { @@ -153,24 +154,7 @@ class DynamicRecommendations extends React.Component<{recTargetProp : string}, {
{this.props.recTargetProp}
- {function(recommendations : GetRecommendationsResponse | {}) { - if (Object.keys(recommendations).length == 0) { - return; - } - let recs = (recommendations as GetRecommendationsResponse)["tracks"]; - let recommendedTracksHTML = []; - for (let i = 0; i < 6; i++) { - let recommendedSong = artist.name)} - songURI={recs[i].uri} - key={i}> - ; - recommendedTracksHTML.push(recommendedSong); - } - return recommendedTracksHTML; - }(this.state.recommendations)} + {RecommendationsRender(this.state.recommendations)}
diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 89e135f..2ab6e19 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -98,7 +98,12 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } selectMetric = (metric: string, value: string) => { - (this.state.selectedMetrics as SelectedMetrics)[metric] = value; + if (metric in this.state.selectedMetrics) { + delete (this.state.selectedMetrics as SelectedMetrics)[metric]; + } + else { + (this.state.selectedMetrics as SelectedMetrics)[metric] = value; + } this.setState({ selectedMetrics: this.state.selectedMetrics }); diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx index 9ffa59f..22faa85 100644 --- a/src/components/RecommendationsModal.tsx +++ b/src/components/RecommendationsModal.tsx @@ -5,6 +5,7 @@ import { GetRecommendationsInput, GetRecommendationsResponse, RecommendationsInp import getRecommendations from "../services/dynamicRecommendationsService"; import { SelectedMetrics } from "../types/enhancify"; import getID from './../services/common'; +import { RecommendationsRender } from "../services/enhancifyInternalService"; class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setModalIsOpen: (value: boolean) => void, songURI: string, selectedMetrics: SelectedMetrics}, {recommendations: GetRecommendationsResponse | {}}> { @@ -22,6 +23,7 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod // Prepare the recommendations to send to the server let apiOptions = new GetRecommendationsInput(); apiOptions.data.seed_tracks = getID(this.props.songURI); + apiOptions.data.limit = "10"; for (let key in this.props.selectedMetrics) { let apiDataKey = "target_" + key.toLowerCase(); @@ -40,7 +42,7 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod this.props.setModalIsOpen(false)}> {JSON.stringify(this.props.selectedMetrics)} - {JSON.stringify(this.state.recommendations)} + {RecommendationsRender(this.state.recommendations)} ); } diff --git a/src/services/enhancifyInternalService.tsx b/src/services/enhancifyInternalService.tsx index 16b3e70..fd19150 100644 --- a/src/services/enhancifyInternalService.tsx +++ b/src/services/enhancifyInternalService.tsx @@ -1,5 +1,26 @@ +import React from "react"; +import RecommendedTrack from "../components/RecommendedTrack"; import { Labels, MetricFeatures, SongMetricData } from "../types/enhancify"; -import { AudioFeaturesResponse } from "../types/spotify-web-api"; +import { AudioFeaturesResponse, GetRecommendationsResponse } from "../types/spotify-web-api"; + +export function RecommendationsRender(recommendations : GetRecommendationsResponse | {}) { + if (Object.keys(recommendations).length == 0) { + return; + } + let recs = (recommendations as GetRecommendationsResponse)["tracks"]; + let recommendedTracksHTML = []; + for (let i = 0; i < recs.length; i++) { + let recommendedSong = artist.name)} + songURI={recs[i].uri} + key={i}> + ; + recommendedTracksHTML.push(recommendedSong); + } + return recommendedTracksHTML; +} // Dynamically fills in the song metric information based on the specific metrics that the user wants to display export function getSongMetrics(audioFeatures: AudioFeaturesResponse, metricsToDisplay: string[]): SongMetricData[] { From 40b1a130a76afb3dfad83f836c9144cfad1dff63 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 16:19:28 -0400 Subject: [PATCH 3/6] Add visual representation when a metric is selected --- src/components/NowPlaying.tsx | 2 +- src/components/RecommendationsModal.tsx | 1 - src/components/SongMetric.tsx | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 2ab6e19..778c5c8 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -195,7 +195,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Stats block data */} {this.state.songMetrics.map((songMetric: SongMetricData, i) => { - return ; + return ; })}
diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx index 22faa85..eb9e16a 100644 --- a/src/components/RecommendationsModal.tsx +++ b/src/components/RecommendationsModal.tsx @@ -41,7 +41,6 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod return ( this.props.setModalIsOpen(false)}> - {JSON.stringify(this.props.selectedMetrics)} {RecommendationsRender(this.state.recommendations)} ); diff --git a/src/components/SongMetric.tsx b/src/components/SongMetric.tsx index ae4b485..1d58b2b 100644 --- a/src/components/SongMetric.tsx +++ b/src/components/SongMetric.tsx @@ -8,12 +8,13 @@ class SongMetric extends React.Component<{floatValue: string, title: string, progressBar: boolean, label: string, - selectMetric: (metric: string, value: string) => void}, + selectMetric: (metric: string, value: string) => void + isMetricSelected: boolean}, {}> { render() { return ( -
this.props.selectMetric(this.props.title, this.props.floatValue)}> +
this.props.selectMetric(this.props.title, this.props.floatValue)} style={this.props.isMetricSelected ? {borderWidth: "5px", borderStyle: "solid", borderColor: "white"} : {}}>
{this.props.title} From 8e64cacf794f9fbf9c1f4d07d7ab67103a1efd52 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 16:55:13 -0400 Subject: [PATCH 4/6] Integrate selected metrics with local storage --- src/components/NowPlaying.tsx | 23 ++++++++++++++++------- src/components/RecommendationsModal.tsx | 2 ++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 778c5c8..5468723 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -23,7 +23,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon songMetrics: [], // Current song metric information metricsToDisplay: Spicetify.LocalStorage.get("metricsToDisplay") != "" ? Spicetify.LocalStorage.get("metricsToDisplay")?.split(',') || ["Danceability", "Energy", "Acousticness", "Loudness", "Key", "Tempo"] : [], // Current metric information types modalIsOpen: false, // Whether the modal is currently open - selectedMetrics: {}, + selectedMetrics: JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || "{}"), } componentDidMount = () => { @@ -31,8 +31,6 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } setAudioFeatures = () => { - this.setState({selectedMetrics: {}}) - // Check if there is no currently playing song or // if the info of the song is currently being displayed if (!Spicetify.Player.data || this.state.songURI == Spicetify.Player.data.item.uri) { @@ -79,6 +77,15 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon let newArray = this.state.metricsToDisplay.slice(); if (newArray.includes(metric)) { newArray = newArray.filter((val) => val != metric); + + if (metric in this.state.selectedMetrics) { + let copy: SelectedMetrics = { ...this.state.selectedMetrics }; + delete copy[metric]; + Spicetify.LocalStorage.set("selectedMetrics", JSON.stringify(copy)); + this.setState({ + selectedMetrics: copy + }); + } } else { newArray.push(metric); @@ -98,14 +105,16 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } selectMetric = (metric: string, value: string) => { - if (metric in this.state.selectedMetrics) { - delete (this.state.selectedMetrics as SelectedMetrics)[metric]; + let copy: SelectedMetrics = { ...this.state.selectedMetrics }; + if (metric in copy) { + delete copy[metric]; } else { - (this.state.selectedMetrics as SelectedMetrics)[metric] = value; + copy[metric] = value; } + Spicetify.LocalStorage.set("selectedMetrics", JSON.stringify(copy)); this.setState({ - selectedMetrics: this.state.selectedMetrics + selectedMetrics: copy }); } diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx index eb9e16a..f99f9c5 100644 --- a/src/components/RecommendationsModal.tsx +++ b/src/components/RecommendationsModal.tsx @@ -41,6 +41,8 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod return ( this.props.setModalIsOpen(false)}> + {JSON.stringify(JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || "{}") as SelectedMetrics)} + {JSON.stringify(this.props.selectedMetrics)} {RecommendationsRender(this.state.recommendations)} ); From 57445a54deb01afa1aa9d4090b5d13f6dc332997 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 17:00:31 -0400 Subject: [PATCH 5/6] Update comments --- src/components/NowPlaying.tsx | 5 ++++- src/components/RecommendationsModal.tsx | 4 +--- src/services/enhancifyInternalService.tsx | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 5468723..48c42ff 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -23,7 +23,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon songMetrics: [], // Current song metric information metricsToDisplay: Spicetify.LocalStorage.get("metricsToDisplay") != "" ? Spicetify.LocalStorage.get("metricsToDisplay")?.split(',') || ["Danceability", "Energy", "Acousticness", "Loudness", "Key", "Tempo"] : [], // Current metric information types modalIsOpen: false, // Whether the modal is currently open - selectedMetrics: JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || "{}"), + selectedMetrics: JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || "{}"), // Metrics that have been selected to be fed into the Spotify recommendations endpoint } componentDidMount = () => { @@ -78,6 +78,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon if (newArray.includes(metric)) { newArray = newArray.filter((val) => val != metric); + // If a metric is being hidden from the display, it should not be fed into the recommendations endpoint if (metric in this.state.selectedMetrics) { let copy: SelectedMetrics = { ...this.state.selectedMetrics }; delete copy[metric]; @@ -98,12 +99,14 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon }, this.setSongMetrics); } + // Set whether the modal should be open or closed setModalIsOpen = (value: boolean) => { this.setState({ modalIsOpen: value }); } + // Select a metric to toggle whether they should be included in the recommendations endpoint request or not selectMetric = (metric: string, value: string) => { let copy: SelectedMetrics = { ...this.state.selectedMetrics }; if (metric in copy) { diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx index f99f9c5..26292b8 100644 --- a/src/components/RecommendationsModal.tsx +++ b/src/components/RecommendationsModal.tsx @@ -10,7 +10,7 @@ import { RecommendationsRender } from "../services/enhancifyInternalService"; class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setModalIsOpen: (value: boolean) => void, songURI: string, selectedMetrics: SelectedMetrics}, {recommendations: GetRecommendationsResponse | {}}> { state = { - recommendations: {} + recommendations: {} // Recommendations that show up in the modal view }; componentDidMount = () => { @@ -41,8 +41,6 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod return ( this.props.setModalIsOpen(false)}> - {JSON.stringify(JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || "{}") as SelectedMetrics)} - {JSON.stringify(this.props.selectedMetrics)} {RecommendationsRender(this.state.recommendations)} ); diff --git a/src/services/enhancifyInternalService.tsx b/src/services/enhancifyInternalService.tsx index fd19150..5a0f1c6 100644 --- a/src/services/enhancifyInternalService.tsx +++ b/src/services/enhancifyInternalService.tsx @@ -3,6 +3,7 @@ import RecommendedTrack from "../components/RecommendedTrack"; import { Labels, MetricFeatures, SongMetricData } from "../types/enhancify"; import { AudioFeaturesResponse, GetRecommendationsResponse } from "../types/spotify-web-api"; +// Creates the recommended track view for any response from the Spotify recommendations endpoint export function RecommendationsRender(recommendations : GetRecommendationsResponse | {}) { if (Object.keys(recommendations).length == 0) { return; From 1632d6c0286bbaeef5dae6b487ded070b3ccdd50 Mon Sep 17 00:00:00 2001 From: Parikshith Mohite Date: Tue, 1 Oct 2024 17:12:39 -0400 Subject: [PATCH 6/6] Clean up modal html --- src/components/NowPlaying.tsx | 5 ++++- src/components/RecommendationsModal.tsx | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 48c42ff..f2e42ca 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -7,6 +7,7 @@ import SongMetric from "./SongMetric"; import { SelectedMetrics, SongMetricData } from "../types/enhancify"; import { allMetrics, getSongMetrics } from "../services/enhancifyInternalService"; import RecommendationsModal from "./RecommendationsModal"; +import Modal from 'react-modal'; class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string, @@ -231,7 +232,9 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon })}
- { this.state.modalIsOpen ? : <> } + this.setModalIsOpen(false)}> + + ); } diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx index 26292b8..907230c 100644 --- a/src/components/RecommendationsModal.tsx +++ b/src/components/RecommendationsModal.tsx @@ -1,13 +1,11 @@ -import styles from "../css/app.module.scss"; import React from "react"; -import Modal from 'react-modal'; import { GetRecommendationsInput, GetRecommendationsResponse, RecommendationsInput } from "../types/spotify-web-api.d"; import getRecommendations from "../services/dynamicRecommendationsService"; import { SelectedMetrics } from "../types/enhancify"; import getID from './../services/common'; import { RecommendationsRender } from "../services/enhancifyInternalService"; -class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setModalIsOpen: (value: boolean) => void, songURI: string, selectedMetrics: SelectedMetrics}, {recommendations: GetRecommendationsResponse | {}}> { +class RecommendationsModal extends React.Component<{setModalIsOpen: (value: boolean) => void, songURI: string, selectedMetrics: SelectedMetrics}, {recommendations: GetRecommendationsResponse | {}}> { state = { recommendations: {} // Recommendations that show up in the modal view @@ -39,10 +37,10 @@ class RecommendationsModal extends React.Component<{modalIsOpen: boolean, setMod render() { return ( - this.props.setModalIsOpen(false)}> + <> {RecommendationsRender(this.state.recommendations)} - + ); } }