diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/README.md b/README.md index c612b05..3758aea 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ # SpotifyPlus Spotify with a better interface and a better algorithm + +To create a README that explains how to build the code using the build script located in the `/extension` folder of your repository, follow this template. This guide assumes the users have basic knowledge of using terminal or command prompt, have cloned the repository, and have basic setup requirements met, such as having Git and possibly Node.js (depending on your project's requirements). + +--- + +# Building SpotifyPlus Extension + +Welcome to the SpotifyPlus extension! This guide will walk you through the steps to build and deploy the SpotifyPlus extension from the cloned repository to your local Spotify application. + +## Prerequisites + +Before you begin, ensure you have the following prerequisites installed and set up: + +- **Git**: To clone the repository. +- **Spicetify**: Ensure Spicetify is installed and configured on your system. Spicetify is a tool to customize Spotify. +- **Windows Subsystem for Linux (WSL)** (for Windows users): Ensure WSL is installed and set up if you're on Windows, you cannot use powershell to run this script. + +## Clone the Repository + +If you haven't already cloned the repository, run the following command in your terminal or command prompt: + +```bash +git clone [https://github.com/LeadFreeCandy/SpotifyPlus] +``` + +## Navigate to the Extension Directory + +Change your current directory to the `/extension` folder within the cloned repository: + +```bash +cd SpotifyPlus/extension +``` + +## Running the Build Script + +To execute the build script, use the following command: + +```bash +./build.sh +``` + +### For macOS Users + +The script will automatically detect your operating system. If you're on macOS, it will copy the necessary files to `~/.config/spicetify/CustomApps/SpotifyPlus` and apply the Spicetify changes. + +### For Windows Users with WSL + +If you're using Windows Subsystem for Linux (WSL), the script will place the necessary files in your Windows `%appdata%\spicetify\CustomApps\SpotifyPlus` directory and then apply the Spicetify changes. + +## Troubleshooting + +If you encounter any errors during the build process: + +- Ensure you have all the prerequisites installed. +- Verify that Spicetify is correctly set up and that you have permissions to modify its directories. +- Check the script's error messages for hints on what went wrong and adjust your environment accordingly. diff --git a/spotify_app/build.bat b/spotify_app/build.bat new file mode 100644 index 0000000..2c4623d --- /dev/null +++ b/spotify_app/build.bat @@ -0,0 +1,45 @@ +@echo off +SetLocal EnableDelayedExpansion + +:: Function equivalent in batch for copy_and_apply +:copy_and_apply +mkdir "%~1" 2>nul +copy manifest.json "%~1" >nul +if errorlevel 1 ( + echo Error copying files. Ensure manifest.json and index.js exist in the current directory. + exit /b 1 +) +copy index.js "%~1" >nul +if errorlevel 1 ( + echo Error copying files. Ensure manifest.json and index.js exist in the current directory. + exit /b 1 +) + +:: Attempt to configure spicetify for SpotifyPlus +spicetify config custom_apps SpotifyPlus +if errorlevel 1 ( + echo Failed to configure spicetify for SpotifyPlus. Ensure spicetify is correctly installed. + exit /b 1 +) + +:: Apply spicetify changes +spicetify apply +if errorlevel 1 ( + echo Failed to apply spicetify changes. Check your spicetify installation and configuration. + exit /b 1 +) + +goto :eof + +:: Detect OS and set target path +for /f "tokens=2 delims==" %%i in ('wmic os get caption /value') do set OS=%%i + +if "%OS%" == "Microsoft Windows 10 Pro" ( + :: Assuming this is for demonstration; adjust as necessary for your Windows version + :: Convert %appdata% to a direct path usage in PowerShell/Command Prompt + set TARGET_DIR=%appdata%\spicetify\CustomApps\SpotifyPlus + call :copy_and_apply "%TARGET_DIR%" +) else ( + echo Unsupported operating system. This script is intended for Windows with PowerShell. + exit /b 1 +) diff --git a/spotify_app/build.sh b/spotify_app/build.sh new file mode 100755 index 0000000..eab407b --- /dev/null +++ b/spotify_app/build.sh @@ -0,0 +1,37 @@ +copy_and_apply() { + mkdir -p "$1" 2>/dev/null + if ! cp manifest.json index.js "$1"; then + echo "Error copying files. Ensure manifest.json and index.js exist in the current directory." + exit 1 + fi + + if ! spicetify config custom_apps SpotifyPlus; then + echo "Failed to configure spicetify for SpotifyPlus. Ensure spicetify is correctly installed." + exit 1 + fi + + if ! spicetify apply; then + echo "Failed to apply spicetify changes. Check your spicetify installation and configuration." + exit 1 + fi +} + +# Detect OS and set target path +if [ "$(uname)" = "Darwin" ]; then + # macOS + TARGET_DIR="$HOME/.config/spicetify/CustomApps/SpotifyPlus" + copy_and_apply "$TARGET_DIR" +elif [ "$(expr substr $(uname -s) 1 5)" = "Linux" ]; then + # Assuming WSL is being used on Windows + if [ -n "$WSL_DISTRO_NAME" ]; then + # Convert Windows %appdata% path to WSL path + TARGET_DIR="$(wslpath "$(wslvar APPDATA)")/spicetify/CustomApps/SpotifyPlus" + copy_and_apply "$TARGET_DIR" + else + echo "This script is intended to be run on macOS or within WSL for Windows." + exit 1 + fi +else + echo "Unsupported operating system. This script is intended for macOS or WSL on Windows." + exit 1 +fi diff --git a/spotify_app/index.js b/spotify_app/index.js new file mode 100644 index 0000000..4efbf65 --- /dev/null +++ b/spotify_app/index.js @@ -0,0 +1,654 @@ +// Grab any variables you need + +const react = Spicetify.React; +const reactDOM = Spicetify.ReactDOM; +const { + CosmosAsync, + URI, + React: { useState, useEffect, useCallback }, + Platform: { History }, +} = Spicetify; + +const CONFIG = { + activeTab: "Main", + tabs: ["Main","Recent Songs"] +}; + +// Top Bar Content component +const TopBarContent = (props) => { + return react.createElement("div", { + style: { + display: "flex", + paddingTop: "15px", + justifyContent: "flex-start", // Align tabs to the left + gap: "100px", // Adjust the spacing between tabs + } + }, + react.createElement("div", { + key: "Main", + style: { + position: "relative", + } + }, + react.createElement("a", { + className: 'active', + style: { + padding: "10px 20px", // Adjust the padding of the tabs + borderRadius: "20px", // Add border radius to create rounded edges + border: "1px solid white", // Add border with white color + color: "white", // Set text color + textDecoration: "none", // Remove underline + } + }, "Main"), + ) + ); +}; + +async function getUriFromSongAndArtist(song, artist){ + let trackinfo = await CosmosAsync.get('https://api.spotify.com/v1/search?q=' + artist + '%25' + song + '&type=track'); + let newuri = trackinfo.tracks.items[0].uri; + return newuri; + +} + +async function getSongFromUri(uri){ + let id = uri.split(":")[2] + let temp = await CosmosAsync.get('https://api.spotify.com/v1/tracks/' + id); + let tempname = temp.name; + return tempname; +} + +async function checkIfExplicit(uri){ + let id = uri.split(":")[2] + let temp = await CosmosAsync.get('https://api.spotify.com/v1/tracks/' + id); + let isExplcit = temp.explicit; + return isExplcit; +} + +let x = -1; +let randomIndex = -1; + +async function retrievenext() { + const popSongs = [ + 'Blank+Space', + 'You+Belong+With+Me', + 'Bad+Blood', + 'Space+Cadet', + '心いう名の不可解', + '踊' + ]; + const popArtists = [ + 'Taylor+Swift', + 'Taylor+Swift', + 'Taylor+Swift', + 'Gunna', + 'Ado', + 'Ado' + ]; + while (x == randomIndex){ + randomIndex = Math.floor(Math.random() * popSongs.length); + } + x = randomIndex; + return getUriFromSongAndArtist(popSongs[randomIndex], popArtists[randomIndex]); + }; + +const currenturi = []; +let playlistinfo = ''; +let playlistCreated = false; + +async function createPlaylist(){ + const user = await CosmosAsync.get('https://api.spotify.com/v1/me'); + if (likedlist.length != 0){ + playlistinfo = CosmosAsync.post('https://api.spotify.com/v1/users/' + user.id + '/playlists', { + name: 'SpotifyPlus Playlist' + }); + } + return playlistinfo; +} + + + +async function addtoplaylist(playlistinfo) { + const playlisturi = playlistinfo.uri.split(":")[2] + CosmosAsync.post('https://api.spotify.com/v1/playlists/' + playlisturi + '/tracks', { + uris: likedlist + }); + //createPlaylist(); + return true; +} + +const likedlist = []; +let likedSongs = []; +async function addSongToLiked(song){ + let temp = getSongFromUri(song); + if(!likedlist.includes(song)){ + likedlist.push(song); + likedSongs.push(temp.toString()); + likedSongs.push(", \n"); + + } + +} + + +function send(liked, auri) { + try { + console.log("Retrieving items from local storage:", Spicetify.Platform.LocalStorageAPI.items); + + // Retrieve the value from local storage + let songs = Spicetify.Platform.LocalStorageAPI.getItem(liked); + + // Check if songs is undefined or not an array + if (songs === undefined || !Array.isArray(songs)) { + songs = [auri]; + } else { + if (!songs.includes(auri)){ + songs.push(auri); + } + } + + // Store the updated value in local storage + Spicetify.Platform.LocalStorageAPI.setItem(liked, songs); + } catch (error) { + // Handle the error if any + console.error("Error:", error); + console.log("Failed to update local storage for 'liked' key."); + } +} + + + + +/* +async function clearsong() { + return Spicetify.Platform.LocalStorageAPI.clearItem(this.songname) + .then(() => true) // Resolves to true if the item is successfully cleared + .catch(() => false); // Resolves to false if there's an error while clearing the item +} +/*used like this: +clearsong() + .then(success => { + if (success) { + console.log("Item successfully cleared."); + } else { + console.error("Failed to clear item."); + } + }); +*/ + +async function setAlbumUrl(uri){ + let id = uri.split(':')[2]; // Extract the ID from the URI + try { + const response = await CosmosAsync.get('https://api.spotify.com/v1/tracks/' + id); + const data = await response; // Assuming response.json() returns a promise + // Access the data fetched from the Spotify API here + const albumurl = data.album.images[0].url; + return albumurl; + } catch (error) { + // Handle any errors that occurred during the request + console.error('Img retrieval Error:', error); + } +} +async function nextsong(uri){ + + Spicetify.Player.playUri(uri); +} + +// Example usage: +const trackUri = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; +// +let tempplaylist = ''; +let uri = ''; + +async function handleLike() { + try { + // Assuming send() is an asynchronous function + await send("liked",uri); + } catch (error) { + console.error("Error sending like to LLM:", error); + } + try{ + addSongToLiked(uri); + + //likedSongs.push(getSongFromUri(uri)); + //likedSongs.push("\n"); + } catch (error) { + console.error("Error adding song to liked songs:", error); + } + + +} + +async function handleDislike() { + try { + // Assuming send() is an asynchronous function + await send("disliked",uri); + } catch (error) { + console.error("Error sending like to LLM:", error); + } + +} + +async function handleCreatePlaylist() { + try { + // Assuming send() is an asynchronous function + await send("playlist created"); + } catch (error) { + console.error("Error sending playlist state to LLM:", error); + } + + if (!playlistCreated){ + try { + //playlistCreated = true + tempplaylist = await createPlaylist(); + await addtoplaylist(tempplaylist); + likedlist.length = 0; + likedSongs.length = 0; + tempplaylist = null; + } catch (error) { + console.error("Error creating playlist:", error); + } + } + else { + try { + await addtoplaylist(tempplaylist); + } catch (error) { + console.error("Error adding song to playlist:", error); + } + } +} + +let explicitAllowed = true; +async function handleToggleExplicit() { + try { + // Assuming send() is an asynchronous function + await send("explicit toggled",uri); + + } catch (error) { + console.error("Error sending explicit to LLM:", error); + } + + + if (explicitAllowed){explicitAllowed = false;} + else {explicitAllowed = true;} + +} + + + + +// The main custom app render function. The component returned is what is rendered in Spotify. +function render() { + return react.createElement(Grid, { title: "SpotifyPlus" }); +} + + + +// Our main component +class Grid extends react.Component { + constructor(props) { + super(props); + Object.assign(this, props); + this.state = { + foo: "bar", + data: "etc", + h1: false, + h2: false, + h3: false, + h4: false, + h5: false, + c1: false, + c2: false, + c3: false, + c4: false, + c5: false, + songName: "", + Artist: "", + selectedGenre: "" + //LikedSongs: []; + + + }; + } + + componentDidMount() { + this.addKeyboardShortcutListener(); + } + + componentWillUnmount() { + this.removeKeyboardShortcutListener(); + } + + handleGenreChange = (event) => { + this.setState({ selectedGenre: event.target.value }); + } + + handleKeyDown = (event) => { + // Check if CTRL, ALT, and L are pressed + if (event.ctrlKey && event.altKey && event.key === 'p') { + // Find the button element by ID or other means + const button = document.getElementById('playlist'); + // If the button is found, trigger its click event + if (button) { + button.click(); + } + } + else if (event.ctrlKey && event.altKey && event.key === 'l') { + // Find the button element by ID or other means + const button = document.getElementById('like'); + // If the button is found, trigger its click event + if (button) { + button.click(); + } + } + else if (event.ctrlKey && event.altKey && event.key === 'd') { + // Find the button element by ID or other means + const button = document.getElementById('dislike'); + // If the button is found, trigger its click event + if (button) { + button.click(); + } + } + else if (event.ctrlKey && event.altKey && event.key === 'n') { + // Find the button element by ID or other means + const button = document.getElementById('play-next'); + // If the button is found, trigger its click event + if (button) { + button.click(); + } + } + }; + + addKeyboardShortcutListener() { + document.addEventListener('keydown', this.handleKeyDown); + } + + removeKeyboardShortcutListener() { + document.removeEventListener('keydown', this.handleKeyDown); + } + handlePlayNext = async () => { + try { + // Assuming retrievenext() is an asynchronous function + // uri = await retrievenext(); + + // while (!explicitAllowed && checkIfExplicit(uri)){ + // uri = await retrievenext(); + // } + + do{ + uri = await retrievenext(); + let temp = checkIfExplicit(uri); + } while (!explicitAllowed && temp); + + //testing geturifromsongandartistfunction + //const uri = await getUriFromSongAndArtist("Space+Cadet", "Gunna"); + + currenturi.length = 0; + currenturi.push(uri); + await nextsong(uri); + const imageUrl = await setAlbumUrl(uri); + let id = uri.split(':')[2]; // Extract the ID from the URI + const response = await CosmosAsync.get('https://api.spotify.com/v1/tracks/' + id); + const data = await response; // Assuming response.json() returns a promise + const s = data.name; + const a = " by: " + data.artists[0].name; + // Access the data fetched from the Spotify API here + this.setState({ image: imageUrl, songName: s,Artist: a}); // Update the image state + + + } catch (error) { + console.error("Error handling next song:", error); + } + } + + + render() { + const {h1,h2,h3,h4,h5,c1,c2,c3,c4,songName,Artist } = this.state; + + return react.createElement("section", { + className: "contentSpacing", + }, + react.createElement("div", { + className: "marketplace-header", + }, react.createElement("h1", null, this.props.title), + react.createElement("button", { + //onClick: () => handleDislike(param1, param2), // Call handleDislike with parameters + onClick: () => { + handleCreatePlaylist(); + this.setState({ c4: true }); + setTimeout(() => { + this.setState({ c4: false }); + }, 125); + }, + onMouseOver: () => this.setState({h4: true}), + onMouseOut: () => this.setState({h4: false}), + id: "playlist", + style: { + backgroundColor: this.state.h4 ? "purple": "#8a2be2", + color: "white", // Change the text color of the button + border: this.state.c4 ? "1px solid white":"none", // Remove the border + padding: "10px 20px", // Add padding + borderRadius:"5px", // Add border radius + } + }, "Create Playlist"), + react.createElement("button", { + onClick: () => { + handleToggleExplicit(); + this.setState({ c5: true }); + setTimeout(() => { + this.setState({ c5: false }); + }, 125); + }, + onMouseOver: () => this.setState({h5: true}), + onMouseOut: () => this.setState({h5: false}), + id: "playlist", + style: { + backgroundColor: this.state.h5 ? "Dark Grey": "Black", + color: "white", // Change the text color of the button + border: this.state.c5 ? "1px solid white":"none", // Remove the border + padding: "10px 20px", // Add padding + borderRadius:"5px", // Add border radius + } + }, "Toggle Explicit"), + react.createElement("div",{ + style: { + margin: "8px" + } + }, + react.createElement("div",{ + style:{ + color: "white", + + } + }, explicitAllowed.toString())), + ), + react.createElement("div", { + style: { + flexDirection: "column", + //flexDirection: "row", + textAlign: "center", + display: "flex", + justifyContent: "center", + alignItems: "center", + transform: "scale(2)", // Scale the image to make it 4 times bigger + transformOrigin: "center top", // Set the transform origin to the center top + } + }, + + //genre selection for llm, work in progress + // react.createElement("label", { htmlFor: "genreDropdown" }, "Select Genre: "), + // react.createElement("select", { + // id: "genreDropdown", + // value: selectedGenre, + // onChange: this.handleGenreChange, + // style: { + // marginLeft: "10px" + // } + // }, + // react.createElement("option", { value: "" }, "Select Genre"), + // react.createElement("option", { value: "Pop" }, "Pop"), + // react.createElement("option", { value: "Rock" }, "Rock"), + // react.createElement("option", { value: "Hip Hop" }, "Hip Hop"), + // react.createElement("option", { value: "Electronic" }, "Electronic") + // ) + react.createElement("div",{ + style: { + margin: "8px" + } + }, + react.createElement("div",{ + style:{ + color: "white", + + } + },this.state.songName + this.state.Artist)), + react.createElement("img", { + src: this.state.image, + style: { + maxWidth: 315, + maxHeight: 220, + } + }) + ), + react.createElement("div", { + style: { + flexDirection: "row", + display: "flex", + justifyContent: "flex-start", + alignItems: "left", + transform: "scale(1)", // Scale the image to make it 4 times bigger + transformOrigin: "center top", // Set the transform origin to the center top + } + }, + + react.createElement("img", { + src: "https://cdn.pixabay.com/photo/2020/09/30/07/48/heart-5614865_1280.png", //this.state.image, + style: { + maxWidth: 315, + maxHeight: 220, + } + }) + ), + react.createElement("div", { + style: { + flexDirection: "row", + display: "flex", + justifyContent: "flex-start", + alignItems: "right", + transform: "scale(1)", // Scale the image to make it 4 times bigger + transformOrigin: "center top", // Set the transform origin to the center top + } + }, + + react.createElement("img", { + src: "https://www.freeiconspng.com/thumbs/close-button-png/black-circle-close-button-png-5.png", //this.state.image, + style: { + maxWidth: 315, + maxHeight: 220, + } + }) + ), + + react.createElement("div",{ + style: { + margin: "8px" + } + }, + react.createElement("div",{ + style:{ + color: "white", + + } + }, likedSongs)), + + + react.createElement("footer", { + style: { + margin: "auto", + position: "fixed", + textAlign: "center", + bottom: 120, + left: 310, + width: "80%", // Ensure the footer spans the full width + paddingTop: "20px", // Add padding at the top for spacing + paddingBottom: "20px" + + }, + }, + react.createElement("div", { + style: { + display: "flex", + justifyContent: "space-around", // This will evenly distribute the buttons + + }, + }, + react.createElement("button", { + onClick: () => { + handleDislike(); + this.setState({ c1: true }); + setTimeout(() => { + this.setState({ c1: false }); + }, 125); + }, + onMouseOver: () => this.setState({h1: true}), + onMouseOut: () => this.setState({h1: false}), + id: "dislike", + style: { + backgroundColor: this.state.h1 ? "darkred" : "red", // Change the background color of the button + color: "white", // Change the text color of the button + border: this.state.c1 ? "1px solid white":"none", // Remove the border + padding: "10px 20px", // Add padding + borderRadius: "5px", // Add border radius + // background-image: url('https://www.freeiconspng.com/thumbs/close-button-png/black-circle-close-button-png-5.png'); + // width: 315px; + // height: 220px; + // border: none; + // cursor: pointer; + } + }, "Dislike"), + + react.createElement("button", { + onClick: () => { + this.handlePlayNext(); + this.setState({ c2: true }); + setTimeout(() => { + this.setState({ c2: false }); + }, 125); + }, + onMouseOver: () => this.setState({h2: true}), + onMouseOut: () => this.setState({h2: false}), + id: "play-next", + style: { + backgroundColor: this.state.h2 ? "darkblue": "blue", // Change the background color of the button + color: "white", // Change the text color of the button + border: this.state.c2 ? "1px solid white":"none", // Remove the border + padding: "10px 20px", // Add padding + borderRadius: "5px", // Add border radius + } + }, "Play Next"), + react.createElement("button", { + onClick: () => { + handleLike(); + this.setState({ c3: true }); + setTimeout(() => { + this.setState({ c3: false }); + }, 125); + }, + onMouseOver: () => this.setState({h3: true}), + onMouseOut: () => this.setState({h3: false}), + id: "like", + + style: { + backgroundColor: this.state.h3 ? "darkgreen" : "green", // Change the background color of the button + color: "white", // Change the text color of the button + border: this.state.c3 ? "1px solid white":"none", // Remove the border + padding: "10px 20px", // Add padding + borderRadius:"5px", // Add border radius + } + }, "Like"), + + + )), + ); + } +} diff --git a/spotify_app/manifest.json b/spotify_app/manifest.json new file mode 100644 index 0000000..d13eebc --- /dev/null +++ b/spotify_app/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "SpotifyPlus", + "icon": "", + "active-icon": "", + "subfiles": ["src/app.js"], + "subfiles_extension": [] +}