├── .gitattributes ├── resource └── preview.png ├── src ├── settings.json ├── types │ ├── css-modules.d.ts │ └── spicetify.d.ts ├── components │ ├── extractLyrics.js │ └── Lyrics.jsx ├── css │ ├── app.module.scss │ └── icon.svg └── app.tsx ├── manifest.json ├── tsconfig.json ├── dist ├── style.css └── manifest.json ├── package.json ├── install.sh ├── README.md ├── LICENSE ├── install.ps1 ├── .gitignore └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* linguist-vendored -------------------------------------------------------------------------------- /resource/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nuzair46/Lyrixed/HEAD/resource/preview.png -------------------------------------------------------------------------------- /src/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Lyrixed", 3 | "nameId": "lyrixed", 4 | "icon": "css/icon.svg", 5 | "activeIcon": "css/icon.svg" 6 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lyrixed", 3 | "description": "Bringing back lyrics to Spotify free version with Genius.", 4 | "preview": "resource/preview.png", 5 | "readme": "README.md", 6 | "tags": ["lyrics"] 7 | } -------------------------------------------------------------------------------- /src/types/css-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | 6 | declare module '*.module.scss' { 7 | const classes: { [key: string]: string }; 8 | export default classes; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "jsx": "react", 5 | "module": "commonjs", 6 | "resolveJsonModule": true, 7 | "outDir": "dist", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": [ 14 | "./src/**/*.*" 15 | ] 16 | } -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | .app-module__container___thHYo_lyrixed{display:flex;justify-content:center;align-items:center;flex-direction:column;background-color:gray}.app-module__container___thHYo_lyrixed>*{padding:40px 0}.app-module__lyrics_container___BnyjF_lyrixed{top:0;justify-content:center;align-items:center;flex-direction:column;border:1px;border-radius:10px;width:100%;position:absolute;overflow:hidden}.app-module__lyrics___xFuUo_lyrixed{position:relative;color:#f0f8ff;padding:25px 0 0 25px;font-size:48px;line-height:1.5;overflow:scroll!important} -------------------------------------------------------------------------------- /src/components/extractLyrics.js: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio-without-node-native"; 2 | import axios from "axios"; 3 | 4 | const proxyUrl = "https://api.allorigins.win/raw?url="; 5 | 6 | module.exports = async function (url) { 7 | try { 8 | let { data } = await axios.get(`${proxyUrl}${url}`); 9 | const $ = cheerio.load(data); 10 | let lyrics = $("[data-lyrics-container='true']") 11 | .find("br") 12 | .replaceWith("\n") 13 | .end() 14 | .text(); 15 | 16 | if (!lyrics) return null; 17 | return lyrics; 18 | } catch (e) { 19 | throw e; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lyrixed", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "spicetify-creator", 7 | "build-local": "spicetify-creator --out=dist --minify", 8 | "watch": "spicetify-creator --watch" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/react": "^18.2.28", 13 | "@types/react-dom": "^18.2.13", 14 | "spicetify-creator": "^1.0.15" 15 | }, 16 | "dependencies": { 17 | "axios": "^1.5.1", 18 | "cheerio-without-node-native": "0.20.2", 19 | "express": "^4.18.2", 20 | "genius-lyrics-api": "^3.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/css/app.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | 7 | > * { 8 | padding: 40px 0; 9 | } 10 | background-color: gray; 11 | } 12 | 13 | .lyrics_container { 14 | top: 0; 15 | justify-content: center; 16 | align-items: center; 17 | flex-direction: column; 18 | border: 1px; 19 | border-radius: 10px; 20 | width: 100%; 21 | position: absolute; 22 | overflow: hidden; 23 | } 24 | 25 | .lyrics { 26 | position: relative; 27 | color: aliceblue; 28 | padding: 25px 0px 0px 25px; 29 | font-size: 48px; 30 | line-height: 1.5; 31 | overflow: scroll !important; 32 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import styles from './css/app.module.scss' 2 | import Lyrics from './components/Lyrics'; 3 | const react = Spicetify.React; 4 | 5 | function render() { 6 | return react.createElement(App, { title: "Lyrixed" }); 7 | } 8 | 9 | class App extends react.Component { 10 | render() { 11 | return react.createElement( 12 | 'div', 13 | { className: styles.container }, 14 | react.createElement( 15 | 'div', 16 | { className: styles.lyrics_container }, 17 | react.createElement( 18 | 'div', 19 | { className: styles.lyrics }, 20 | react.createElement(Lyrics, null) 21 | ) 22 | ) 23 | ); 24 | } 25 | } 26 | 27 | export default App; -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if command -v spicetify &> /dev/null 4 | then 5 | echo "Spicetify is already installed. Skipping Spicetify installation..." 6 | else 7 | echo "Spicetify not found. Installing Spicetify..." 8 | curl -fsSL https://raw.githubusercontent.com/spicetify/cli/main/install.sh | sh 9 | curl -fsSL https://raw.githubusercontent.com/spicetify/marketplace/main/resources/install.sh | sh 10 | fi 11 | 12 | lyrixedUrl="https://github.com/Nuzair46/Lyrixed/releases/latest/download/lyrixed.zip" 13 | targetDir="$HOME/.spicetify/CustomApps/lyrixed" 14 | 15 | mkdir -p "$targetDir" 16 | 17 | zipFile="$targetDir/lyrixed.zip" 18 | curl -L "$lyrixedUrl" -o "$zipFile" 19 | 20 | unzip -o "$zipFile" -d "$targetDir" 21 | rm "$zipFile" 22 | 23 | echo "Lyrixed has been successfully downloaded and extracted to $targetDir" 24 | 25 | echo "Applying Lyrixed with Spicetify..." 26 | spicetify config custom_apps lyrixed 27 | spicetify apply -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lyrixed 2 | 3 | [![Discord](https://discord.com/api/guilds/807273906872123412/widget.png)](https://discord.gg/eYudMwgYtY) 4 | 5 | ![](resource/preview.png) 6 | 7 | Lyrixed is a Spicetify custom app to bring back the lyrics feature to Spotify desktop app for freemium users. 8 | 9 | ## Installation 10 | 11 | Windows: 12 | - In your Powershell, Run: 13 | ```powershell 14 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-Expression "& { $(Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/Nuzair46/Lyrixed/main/install.ps1') }" 15 | ``` 16 | MacOS/Linux: 17 | - In your terminal, Run: 18 | ```bash 19 | curl -fsSL https://raw.githubusercontent.com/Nuzair46/Lyrixed/main/install.sh | sh 20 | ``` 21 | 22 | ## Uninstallation 23 | - In your terminal, Run: 24 | ``` 25 | spicetify config custom_apps lyrixed- 26 | spicetify apply 27 | ``` 28 | ## Note 29 | 30 | - Feel free to open issue for fixes and features or contact on Discord. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nuzair 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 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | Get-Command spicetify -ErrorAction Stop 3 | Write-Output "Spicetify is already installed. Skipping Spicetify installation..." 4 | } catch { 5 | Write-Output "Spicetify not found. Installing Spicetify..." 6 | iwr -useb https://raw.githubusercontent.com/spicetify/cli/main/install.ps1 | iex 7 | iwr -useb https://raw.githubusercontent.com/spicetify/marketplace/main/resources/install.ps1 | iex 8 | } 9 | 10 | $lyrixedUrl = "https://github.com/Nuzair46/Lyrixed/releases/latest/download/lyrixed.zip" 11 | $targetDir = Join-Path -Path $env:APPDATA -ChildPath "spicetify\CustomApps\lyrixed" 12 | 13 | if (-not (Test-Path -Path $targetDir)) { 14 | New-Item -ItemType Directory -Path $targetDir | Out-Null 15 | } 16 | 17 | $zipFile = Join-Path -Path $targetDir -ChildPath "lyrixed.zip" 18 | 19 | Invoke-WebRequest -Uri $lyrixedUrl -OutFile $zipFile 20 | 21 | Expand-Archive -Path $zipFile -DestinationPath $targetDir -Force 22 | 23 | Remove-Item -Path $zipFile 24 | 25 | Write-Output "Lyrixed has been successfully downloaded and extracted to $targetDir" 26 | 27 | Write-Output "Applying Lyrixed with Spicetify..." 28 | spicetify config custom_apps lyrixed 29 | spicetify apply 30 | -------------------------------------------------------------------------------- /src/components/Lyrics.jsx: -------------------------------------------------------------------------------- 1 | const react = Spicetify.React; 2 | const { useState, useEffect, useCallback } = Spicetify.React; 3 | 4 | import axios from "axios"; 5 | import extractLyrics from "./extractLyrics"; 6 | 7 | const searchUrl = "https://genius.com/api/search/song?"; 8 | const proxyUrl = "https://api.allorigins.win/raw?url="; 9 | 10 | class Lyrics extends react.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | currentTrack: {}, 15 | lyricsData: "", 16 | isLoading: true, 17 | }; 18 | } 19 | 20 | getTitle(title, artist) { 21 | return `${title} ${artist}` 22 | .toLowerCase() 23 | .replace(/ *\([^)]*\) */g, "") 24 | .replace(/ *\[[^\]]*]/, "") 25 | .replace(/feat.|ft./g, "") 26 | .replace(/\s+/g, " ") 27 | .trim(); 28 | } 29 | 30 | async fetchTrackLyrics() { 31 | try { 32 | this.setState({ isLoading: true }); 33 | const track = Spicetify.Player.data.item.metadata; 34 | const { title, artist_name } = track; 35 | const query = new URLSearchParams({ 36 | per_page: 20, 37 | q: `${title} ${artist_name}`, 38 | }); 39 | const url = encodeURIComponent(`${searchUrl}${query}`); 40 | const reqUrl = `${proxyUrl}${url}`; 41 | const trackResponse = await axios.get(reqUrl); 42 | 43 | if (trackResponse.status !== 200) { 44 | return null; 45 | } 46 | const result = trackResponse.data.response.sections[0].hits[0].result; 47 | 48 | if (!result) { 49 | return null; 50 | } 51 | 52 | const lyrics = await extractLyrics(result.url); 53 | 54 | this.setState({ lyricsData: lyrics }); 55 | } catch (e) { 56 | console.error(e); 57 | } finally { 58 | this.setState({ isLoading: false }); 59 | } 60 | } 61 | 62 | componentDidMount() { 63 | Spicetify.Player.addEventListener("songchange", async (e) => { 64 | await this.fetchTrackLyrics(); 65 | }); 66 | this.fetchTrackLyrics(); 67 | } 68 | 69 | render() { 70 | const { isLoading, lyricsData } = this.state; 71 | 72 | if (isLoading) { 73 | return react.createElement( 74 | "div", 75 | null, 76 | "Lyrixed is finding the lyrics. Please wait..." 77 | ); 78 | } 79 | 80 | return lyricsData 81 | ? react.createElement( 82 | "div", 83 | null, 84 | lyricsData 85 | .split("\n") 86 | .map((str) => react.createElement("p", null, str)) 87 | ) 88 | : react.createElement("div", null, "Couldn't find lyrics for this one."); 89 | } 90 | } 91 | 92 | export default Lyrics; 93 | -------------------------------------------------------------------------------- /src/css/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lyrixed", 3 | "icon": "\r\n\r\n", 4 | "active-icon": "\r\n\r\n", 5 | "subfiles": [], 6 | "subfiles_extension": [] 7 | } -------------------------------------------------------------------------------- /src/types/spicetify.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Spicetify { 2 | type Icon = 3 | | "album" 4 | | "artist" 5 | | "block" 6 | | "brightness" 7 | | "car" 8 | | "chart-down" 9 | | "chart-up" 10 | | "check" 11 | | "check-alt-fill" 12 | | "chevron-left" 13 | | "chevron-right" 14 | | "chromecast-disconnected" 15 | | "clock" 16 | | "collaborative" 17 | | "computer" 18 | | "copy" 19 | | "download" 20 | | "downloaded" 21 | | "edit" 22 | | "enhance" 23 | | "exclamation-circle" 24 | | "external-link" 25 | | "facebook" 26 | | "follow" 27 | | "fullscreen" 28 | | "gamepad" 29 | | "grid-view" 30 | | "heart" 31 | | "heart-active" 32 | | "instagram" 33 | | "laptop" 34 | | "library" 35 | | "list-view" 36 | | "location" 37 | | "locked" 38 | | "locked-active" 39 | | "lyrics" 40 | | "menu" 41 | | "minimize" 42 | | "minus" 43 | | "more" 44 | | "new-spotify-connect" 45 | | "offline" 46 | | "pause" 47 | | "phone" 48 | | "play" 49 | | "playlist" 50 | | "playlist-folder" 51 | | "plus-alt" 52 | | "plus2px" 53 | | "podcasts" 54 | | "projector" 55 | | "queue" 56 | | "repeat" 57 | | "repeat-once" 58 | | "search" 59 | | "search-active" 60 | | "shuffle" 61 | | "skip-back" 62 | | "skip-back15" 63 | | "skip-forward" 64 | | "skip-forward15" 65 | | "soundbetter" 66 | | "speaker" 67 | | "spotify" 68 | | "subtitles" 69 | | "tablet" 70 | | "ticket" 71 | | "twitter" 72 | | "visualizer" 73 | | "voice" 74 | | "volume" 75 | | "volume-off" 76 | | "volume-one-wave" 77 | | "volume-two-wave" 78 | | "watch" 79 | | "x"; 80 | type Variant = 81 | | "bass" 82 | | "forte" 83 | | "brio" 84 | | "altoBrio" 85 | | "alto" 86 | | "canon" 87 | | "celloCanon" 88 | | "cello" 89 | | "ballad" 90 | | "balladBold" 91 | | "viola" 92 | | "violaBold" 93 | | "mesto" 94 | | "mestoBold" 95 | | "metronome" 96 | | "finale" 97 | | "finaleBold" 98 | | "minuet" 99 | | "minuetBold"; 100 | type SemanticColor = 101 | | "textBase" 102 | | "textSubdued" 103 | | "textBrightAccent" 104 | | "textNegative" 105 | | "textWarning" 106 | | "textPositive" 107 | | "textAnnouncement" 108 | | "essentialBase" 109 | | "essentialSubdued" 110 | | "essentialBrightAccent" 111 | | "essentialNegative" 112 | | "essentialWarning" 113 | | "essentialPositive" 114 | | "essentialAnnouncement" 115 | | "decorativeBase" 116 | | "decorativeSubdued" 117 | | "backgroundBase" 118 | | "backgroundHighlight" 119 | | "backgroundPress" 120 | | "backgroundElevatedBase" 121 | | "backgroundElevatedHighlight" 122 | | "backgroundElevatedPress" 123 | | "backgroundTintedBase" 124 | | "backgroundTintedHighlight" 125 | | "backgroundTintedPress" 126 | | "backgroundUnsafeForSmallTextBase" 127 | | "backgroundUnsafeForSmallTextHighlight" 128 | | "backgroundUnsafeForSmallTextPress"; 129 | type ColorSet = 130 | | "base" 131 | | "brightAccent" 132 | | "negative" 133 | | "warning" 134 | | "positive" 135 | | "announcement" 136 | | "invertedDark" 137 | | "invertedLight" 138 | | "mutedAccent" 139 | | "overMedia"; 140 | type ColorSetBackgroundColors = { 141 | base: string; 142 | highlight: string; 143 | press: string; 144 | }; 145 | type ColorSetNamespaceColors = { 146 | announcement: string; 147 | base: string; 148 | brightAccent: string; 149 | negative: string; 150 | positive: string; 151 | subdued: string; 152 | warning: string; 153 | }; 154 | type ColorSetBody = { 155 | background: ColorSetBackgroundColors & { 156 | elevated: ColorSetBackgroundColors; 157 | tinted: ColorSetBackgroundColors; 158 | unsafeForSmallText: ColorSetBackgroundColors; 159 | }; 160 | decorative: { 161 | base: string; 162 | subdued: string; 163 | }; 164 | essential: ColorSetNamespaceColors; 165 | text: ColorSetNamespaceColors; 166 | }; 167 | type Metadata = Partial>; 168 | type ContextTrack = { 169 | uri: string; 170 | uid?: string; 171 | metadata?: Metadata; 172 | }; 173 | type PlayerState = { 174 | timestamp: number; 175 | context: PlayerContext; 176 | index: PlayerIndex; 177 | item: PlayerTrack; 178 | shuffle: boolean; 179 | repeat: number; 180 | speed: number; 181 | positionAsOfTimestamp: number; 182 | duration: number; 183 | hasContext: boolean; 184 | isPaused: boolean; 185 | isBuffering: boolean; 186 | restrictions: Restrictions; 187 | previousItems?: PlayerTrack[]; 188 | nextItems?: PlayerTrack[]; 189 | playbackQuality: PlaybackQuality; 190 | playbackId: string; 191 | sessionId: string; 192 | signals?: any[]; 193 | /** 194 | * @deprecated Use `item` instead. This will be removed in the future. 195 | */ 196 | track: PlayerTrack; 197 | }; 198 | type PlayerContext = { 199 | uri: string; 200 | url: string; 201 | metadata: { 202 | "player.arch": string; 203 | }; 204 | }; 205 | type PlayerIndex = { 206 | pageURI?: string | null; 207 | pageIndex: number; 208 | itemIndex: number; 209 | }; 210 | type PlayerTrack = { 211 | type: string; 212 | uri: string; 213 | uid: string; 214 | name: string; 215 | mediaType: string; 216 | duration: { 217 | milliseconds: number; 218 | }; 219 | album: Album; 220 | artists?: ArtistsEntity[]; 221 | isLocal: boolean; 222 | isExplicit: boolean; 223 | is19PlusOnly: boolean; 224 | provider: string; 225 | metadata: TrackMetadata; 226 | images?: ImagesEntity[]; 227 | }; 228 | type TrackMetadata = { 229 | artist_uri: string; 230 | entity_uri: string; 231 | iteration: string; 232 | title: string; 233 | "collection.is_banned": string; 234 | "artist_uri:1": string; 235 | "collection.in_collection": string; 236 | image_small_url: string; 237 | "collection.can_ban": string; 238 | is_explicit: string; 239 | album_disc_number: string; 240 | album_disc_count: string; 241 | track_player: string; 242 | album_title: string; 243 | "collection.can_add": string; 244 | image_large_url: string; 245 | "actions.skipping_prev_past_track": string; 246 | page_instance_id: string; 247 | image_xlarge_url: string; 248 | marked_for_download: string; 249 | "actions.skipping_next_past_track": string; 250 | context_uri: string; 251 | "artist_name:1": string; 252 | has_lyrics: string; 253 | interaction_id: string; 254 | image_url: string; 255 | album_uri: string; 256 | album_artist_name: string; 257 | album_track_number: string; 258 | artist_name: string; 259 | duration: string; 260 | album_track_count: string; 261 | popularity: string; 262 | }; 263 | type Album = { 264 | type: string; 265 | uri: string; 266 | name: string; 267 | images?: ImagesEntity[]; 268 | }; 269 | type ImagesEntity = { 270 | url: string; 271 | label: string; 272 | }; 273 | type ArtistsEntity = { 274 | type: string; 275 | uri: string; 276 | name: string; 277 | }; 278 | type Restrictions = { 279 | canPause: boolean; 280 | canResume: boolean; 281 | canSeek: boolean; 282 | canSkipPrevious: boolean; 283 | canSkipNext: boolean; 284 | canToggleRepeatContext: boolean; 285 | canToggleRepeatTrack: boolean; 286 | canToggleShuffle: boolean; 287 | disallowPausingReasons?: string[]; 288 | disallowResumingReasons?: string[]; 289 | disallowSeekingReasons?: string[]; 290 | disallowSkippingPreviousReasons?: string[]; 291 | disallowSkippingNextReasons?: string[]; 292 | disallowTogglingRepeatContextReasons?: string[]; 293 | disallowTogglingRepeatTrackReasons?: string[]; 294 | disallowTogglingShuffleReasons?: string[]; 295 | disallowTransferringPlaybackReasons?: string[]; 296 | }; 297 | type PlaybackQuality = { 298 | bitrateLevel: number; 299 | strategy: number; 300 | targetBitrateLevel: number; 301 | targetBitrateAvailable: boolean; 302 | hifiStatus: number; 303 | }; 304 | namespace Player { 305 | /** 306 | * Register a listener `type` on Spicetify.Player. 307 | * 308 | * On default, `Spicetify.Player` always dispatch: 309 | * - `songchange` type when player changes track. 310 | * - `onplaypause` type when player plays or pauses. 311 | * - `onprogress` type when track progress changes. 312 | * - `appchange` type when user changes page. 313 | */ 314 | function addEventListener(type: string, callback: (event?: Event) => void): void; 315 | function addEventListener(type: "songchange", callback: (event?: Event & { data: PlayerState }) => void): void; 316 | function addEventListener(type: "onplaypause", callback: (event?: Event & { data: PlayerState }) => void): void; 317 | function addEventListener(type: "onprogress", callback: (event?: Event & { data: number }) => void): void; 318 | function addEventListener( 319 | type: "appchange", 320 | callback: ( 321 | event?: Event & { 322 | data: { 323 | /** 324 | * App href path 325 | */ 326 | path: string; 327 | /** 328 | * App container 329 | */ 330 | container: HTMLElement; 331 | }; 332 | } 333 | ) => void 334 | ): void; 335 | /** 336 | * Skip to previous track. 337 | */ 338 | function back(): void; 339 | /** 340 | * An object contains all information about current track and player. 341 | */ 342 | const data: PlayerState; 343 | /** 344 | * Decrease a small amount of volume. 345 | */ 346 | function decreaseVolume(): void; 347 | /** 348 | * Dispatches an event at `Spicetify.Player`. 349 | * 350 | * On default, `Spicetify.Player` always dispatch 351 | * - `songchange` type when player changes track. 352 | * - `onplaypause` type when player plays or pauses. 353 | * - `onprogress` type when track progress changes. 354 | * - `appchange` type when user changes page. 355 | */ 356 | function dispatchEvent(event: Event): void; 357 | const eventListeners: { 358 | [key: string]: Array<(event?: Event) => void>; 359 | }; 360 | /** 361 | * Convert milisecond to `mm:ss` format 362 | * @param milisecond 363 | */ 364 | function formatTime(milisecond: number): string; 365 | /** 366 | * Return song total duration in milisecond. 367 | */ 368 | function getDuration(): number; 369 | /** 370 | * Return mute state 371 | */ 372 | function getMute(): boolean; 373 | /** 374 | * Return elapsed duration in milisecond. 375 | */ 376 | function getProgress(): number; 377 | /** 378 | * Return elapsed duration in percentage (0 to 1). 379 | */ 380 | function getProgressPercent(): number; 381 | /** 382 | * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). 383 | */ 384 | function getRepeat(): number; 385 | /** 386 | * Return current shuffle state. 387 | */ 388 | function getShuffle(): boolean; 389 | /** 390 | * Return track heart state. 391 | */ 392 | function getHeart(): boolean; 393 | /** 394 | * Return current volume level (0 to 1). 395 | */ 396 | function getVolume(): number; 397 | /** 398 | * Increase a small amount of volume. 399 | */ 400 | function increaseVolume(): void; 401 | /** 402 | * Return a boolean whether player is playing. 403 | */ 404 | function isPlaying(): boolean; 405 | /** 406 | * Skip to next track. 407 | */ 408 | function next(): void; 409 | /** 410 | * Pause track. 411 | */ 412 | function pause(): void; 413 | /** 414 | * Resume track. 415 | */ 416 | function play(): void; 417 | /** 418 | * Play a track, playlist, album, etc. immediately 419 | * @param uri Spotify URI 420 | * @param context 421 | * @param options 422 | */ 423 | function playUri(uri: string, context?: any, options?: any): Promise; 424 | /** 425 | * Unregister added event listener `type`. 426 | * @param type 427 | * @param callback 428 | */ 429 | function removeEventListener(type: string, callback: (event?: Event) => void): void; 430 | /** 431 | * Seek track to position. 432 | * @param position can be in percentage (0 to 1) or in milisecond. 433 | */ 434 | function seek(position: number): void; 435 | /** 436 | * Turn mute on/off 437 | * @param state 438 | */ 439 | function setMute(state: boolean): void; 440 | /** 441 | * Change Repeat mode 442 | * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. 443 | */ 444 | function setRepeat(mode: number): void; 445 | /** 446 | * Turn shuffle on/off. 447 | * @param state 448 | */ 449 | function setShuffle(state: boolean): void; 450 | /** 451 | * Set volume level 452 | * @param level 0 to 1 453 | */ 454 | function setVolume(level: number): void; 455 | /** 456 | * Seek to previous `amount` of milisecond 457 | * @param amount in milisecond. Default: 15000. 458 | */ 459 | function skipBack(amount?: number): void; 460 | /** 461 | * Seek to next `amount` of milisecond 462 | * @param amount in milisecond. Default: 15000. 463 | */ 464 | function skipForward(amount?: number): void; 465 | /** 466 | * Toggle Heart (Favourite) track state. 467 | */ 468 | function toggleHeart(): void; 469 | /** 470 | * Toggle Mute/No mute. 471 | */ 472 | function toggleMute(): void; 473 | /** 474 | * Toggle Play/Pause. 475 | */ 476 | function togglePlay(): void; 477 | /** 478 | * Toggle No repeat/Repeat all/Repeat one. 479 | */ 480 | function toggleRepeat(): void; 481 | /** 482 | * Toggle Shuffle/No shuffle. 483 | */ 484 | function toggleShuffle(): void; 485 | } 486 | /** 487 | * Adds a track or array of tracks to prioritized queue. 488 | */ 489 | function addToQueue(uri: ContextTrack[]): Promise; 490 | /** 491 | * @deprecated 492 | */ 493 | const BridgeAPI: any; 494 | /** 495 | * @deprecated 496 | */ 497 | const CosmosAPI: any; 498 | /** 499 | * Async wrappers of CosmosAPI 500 | */ 501 | namespace CosmosAsync { 502 | type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; 503 | interface Error { 504 | code: number; 505 | error: string; 506 | message: string; 507 | stack?: string; 508 | } 509 | 510 | type Headers = Record; 511 | type Body = Record; 512 | 513 | interface Response { 514 | body: any; 515 | headers: Headers; 516 | status: number; 517 | uri?: string; 518 | } 519 | 520 | function head(url: string, headers?: Headers): Promise; 521 | function get(url: string, body?: Body, headers?: Headers): Promise; 522 | function post(url: string, body?: Body, headers?: Headers): Promise; 523 | function put(url: string, body?: Body, headers?: Headers): Promise; 524 | function del(url: string, body?: Body, headers?: Headers): Promise; 525 | function patch(url: string, body?: Body, headers?: Headers): Promise; 526 | function sub( 527 | url: string, 528 | callback: (b: Response["body"]) => void, 529 | onError?: (e: Error) => void, 530 | body?: Body, 531 | headers?: Headers 532 | ): Promise; 533 | function postSub( 534 | url: string, 535 | body: Body | null, 536 | callback: (b: Response["body"]) => void, 537 | onError?: (e: Error) => void 538 | ): Promise; 539 | function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; 540 | function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; 541 | } 542 | /** 543 | * Fetch interesting colors from URI. 544 | * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) 545 | */ 546 | function colorExtractor(uri: string): Promise<{ 547 | DESATURATED: string; 548 | LIGHT_VIBRANT: string; 549 | PROMINENT: string; 550 | VIBRANT: string; 551 | VIBRANT_NON_ALARMING: string; 552 | }>; 553 | /** 554 | * @deprecated 555 | */ 556 | function getAblumArtColors(): any; 557 | /** 558 | * Fetch track analyzed audio data. 559 | * Beware, not all tracks have audio data. 560 | * @param uri is optional. Leave it blank to get current track 561 | * or specify another track uri. 562 | */ 563 | function getAudioData(uri?: string): Promise; 564 | /** 565 | * Set of APIs method to register, deregister hotkeys/shortcuts 566 | */ 567 | namespace Keyboard { 568 | type ValidKey = 569 | | "BACKSPACE" 570 | | "TAB" 571 | | "ENTER" 572 | | "SHIFT" 573 | | "CTRL" 574 | | "ALT" 575 | | "CAPS" 576 | | "ESCAPE" 577 | | "SPACE" 578 | | "PAGE_UP" 579 | | "PAGE_DOWN" 580 | | "END" 581 | | "HOME" 582 | | "ARROW_LEFT" 583 | | "ARROW_UP" 584 | | "ARROW_RIGHT" 585 | | "ARROW_DOWN" 586 | | "INSERT" 587 | | "DELETE" 588 | | "A" 589 | | "B" 590 | | "C" 591 | | "D" 592 | | "E" 593 | | "F" 594 | | "G" 595 | | "H" 596 | | "I" 597 | | "J" 598 | | "K" 599 | | "L" 600 | | "M" 601 | | "N" 602 | | "O" 603 | | "P" 604 | | "Q" 605 | | "R" 606 | | "S" 607 | | "T" 608 | | "U" 609 | | "V" 610 | | "W" 611 | | "X" 612 | | "Y" 613 | | "Z" 614 | | "WINDOW_LEFT" 615 | | "WINDOW_RIGHT" 616 | | "SELECT" 617 | | "NUMPAD_0" 618 | | "NUMPAD_1" 619 | | "NUMPAD_2" 620 | | "NUMPAD_3" 621 | | "NUMPAD_4" 622 | | "NUMPAD_5" 623 | | "NUMPAD_6" 624 | | "NUMPAD_7" 625 | | "NUMPAD_8" 626 | | "NUMPAD_9" 627 | | "MULTIPLY" 628 | | "ADD" 629 | | "SUBTRACT" 630 | | "DECIMAL_POINT" 631 | | "DIVIDE" 632 | | "F1" 633 | | "F2" 634 | | "F3" 635 | | "F4" 636 | | "F5" 637 | | "F6" 638 | | "F7" 639 | | "F8" 640 | | "F9" 641 | | "F10" 642 | | "F11" 643 | | "F12" 644 | | ";" 645 | | "=" 646 | | " | " 647 | | "-" 648 | | "." 649 | | "/" 650 | | "`" 651 | | "[" 652 | | "\\" 653 | | "]" 654 | | '"' 655 | | "~" 656 | | "!" 657 | | "@" 658 | | "#" 659 | | "$" 660 | | "%" 661 | | "^" 662 | | "&" 663 | | "*" 664 | | "(" 665 | | ")" 666 | | "_" 667 | | "+" 668 | | ":" 669 | | "<" 670 | | ">" 671 | | "?" 672 | | "|"; 673 | type KeysDefine = 674 | | string 675 | | { 676 | key: string; 677 | ctrl?: boolean; 678 | shift?: boolean; 679 | alt?: boolean; 680 | meta?: boolean; 681 | }; 682 | const KEYS: Record; 683 | function registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; 684 | function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; 685 | function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; 686 | function _deregisterShortcut(keys: KeysDefine): void; 687 | function deregisterImportantShortcut(keys: KeysDefine): void; 688 | function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; 689 | } 690 | 691 | /** 692 | * @deprecated 693 | */ 694 | const LiveAPI: any; 695 | 696 | namespace LocalStorage { 697 | /** 698 | * Empties the list associated with the object of all key/value pairs, if there are any. 699 | */ 700 | function clear(): void; 701 | /** 702 | * Get key value 703 | */ 704 | function get(key: string): string | null; 705 | /** 706 | * Delete key 707 | */ 708 | function remove(key: string): void; 709 | /** 710 | * Set new value for key 711 | */ 712 | function set(key: string, value: string): void; 713 | } 714 | /** 715 | * To create and prepend custom menu item in profile menu. 716 | */ 717 | namespace Menu { 718 | /** 719 | * Create a single toggle. 720 | */ 721 | class Item { 722 | constructor(name: string, isEnabled: boolean, onClick: (self: Item) => void, icon?: Icon | string); 723 | name: string; 724 | isEnabled: boolean; 725 | /** 726 | * Change item name 727 | */ 728 | setName(name: string): void; 729 | /** 730 | * Change item enabled state. 731 | * Visually, item would has a tick next to it if its state is enabled. 732 | */ 733 | setState(isEnabled: boolean): void; 734 | /** 735 | * Change icon 736 | */ 737 | setIcon(icon: Icon | string): void; 738 | /** 739 | * Item is only available in Profile menu when method "register" is called. 740 | */ 741 | register(): void; 742 | /** 743 | * Stop item to be prepended into Profile menu. 744 | */ 745 | deregister(): void; 746 | } 747 | 748 | /** 749 | * Create a sub menu to contain Item toggles. 750 | * `Item`s in `subItems` array shouldn't be registered. 751 | */ 752 | class SubMenu { 753 | constructor(name: string, subItems: Item[]); 754 | name: string; 755 | /** 756 | * Change SubMenu name 757 | */ 758 | setName(name: string): void; 759 | /** 760 | * Add an item to sub items list 761 | */ 762 | addItem(item: Item); 763 | /** 764 | * Remove an item from sub items list 765 | */ 766 | removeItem(item: Item); 767 | /** 768 | * SubMenu is only available in Profile menu when method "register" is called. 769 | */ 770 | register(): void; 771 | /** 772 | * Stop SubMenu to be prepended into Profile menu. 773 | */ 774 | deregister(): void; 775 | } 776 | } 777 | 778 | /** 779 | * Keyboard shortcut library 780 | * 781 | * Documentation: https://craig.is/killing/mice v1.6.5 782 | * 783 | * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, 784 | * so new extension should use this library instead. 785 | */ 786 | function Mousetrap(element?: any): void; 787 | 788 | /** 789 | * Contains vast array of internal APIs. 790 | * Please explore in Devtool Console. 791 | */ 792 | const Platform: any; 793 | /** 794 | * Queue object contains list of queuing tracks, 795 | * history of played tracks and current track metadata. 796 | */ 797 | const Queue: { 798 | nextTracks: any[]; 799 | prevTracks: any[]; 800 | queueRevision: string; 801 | track: any; 802 | }; 803 | /** 804 | * Remove a track or array of tracks from current queue. 805 | */ 806 | function removeFromQueue(uri: ContextTrack[]): Promise; 807 | /** 808 | * Display a bubble of notification. Useful for a visual feedback. 809 | * @param message Message to display. Can use inline HTML for styling. 810 | * @param isError If true, bubble will be red. Defaults to false. 811 | * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. 812 | */ 813 | function showNotification(message: React.ReactNode, isError?: boolean, msTimeout?: number): void; 814 | /** 815 | * Set of APIs method to parse and validate URIs. 816 | */ 817 | class URI { 818 | constructor(type: string, props: any); 819 | public type: string; 820 | public hasBase62Id: boolean; 821 | 822 | public id?: string; 823 | public disc?: any; 824 | public args?: any; 825 | public category?: string; 826 | public username?: string; 827 | public track?: string; 828 | public artist?: string; 829 | public album?: string; 830 | public duration?: number; 831 | public query?: string; 832 | public country?: string; 833 | public global?: boolean; 834 | public context?: string | typeof URI | null; 835 | public anchor?: string; 836 | public play?: any; 837 | public toplist?: any; 838 | 839 | /** 840 | * 841 | * @return The URI representation of this uri. 842 | */ 843 | toURI(): string; 844 | 845 | /** 846 | * 847 | * @return The URI representation of this uri. 848 | */ 849 | toString(): string; 850 | 851 | /** 852 | * Get the URL path of this uri. 853 | * 854 | * @param opt_leadingSlash True if a leading slash should be prepended. 855 | * @return The path of this uri. 856 | */ 857 | toURLPath(opt_leadingSlash: boolean): string; 858 | 859 | /** 860 | * 861 | * @param origin The origin to use for the URL. 862 | * @return The URL string for the uri. 863 | */ 864 | toURL(origin?: string): string; 865 | 866 | /** 867 | * Clones a given SpotifyURI instance. 868 | * 869 | * @return An instance of URI. 870 | */ 871 | clone(): URI | null; 872 | 873 | /** 874 | * Gets the path of the URI object by removing all hash and query parameters. 875 | * 876 | * @return The path of the URI object. 877 | */ 878 | getPath(): string; 879 | 880 | /** 881 | * The various URI Types. 882 | * 883 | * Note that some of the types in this enum are not real URI types, but are 884 | * actually URI particles. They are marked so. 885 | * 886 | */ 887 | static Type: { 888 | AD: string; 889 | ALBUM: string; 890 | GENRE: string; 891 | QUEUE: string; 892 | APPLICATION: string; 893 | ARTIST: string; 894 | ARTIST_TOPLIST: string; 895 | ARTIST_CONCERTS: string; 896 | AUDIO_FILE: string; 897 | COLLECTION: string; 898 | COLLECTION_ALBUM: string; 899 | COLLECTION_ARTIST: string; 900 | COLLECTION_MISSING_ALBUM: string; 901 | COLLECTION_TRACK_LIST: string; 902 | CONCERT: string; 903 | CONTEXT_GROUP: string; 904 | DAILY_MIX: string; 905 | EMPTY: string; 906 | EPISODE: string; 907 | /** URI particle; not an actual URI. */ 908 | FACEBOOK: string; 909 | FOLDER: string; 910 | FOLLOWERS: string; 911 | FOLLOWING: string; 912 | IMAGE: string; 913 | INBOX: string; 914 | INTERRUPTION: string; 915 | LIBRARY: string; 916 | LIVE: string; 917 | ROOM: string; 918 | EXPRESSION: string; 919 | LOCAL: string; 920 | LOCAL_TRACK: string; 921 | LOCAL_ALBUM: string; 922 | LOCAL_ARTIST: string; 923 | MERCH: string; 924 | MOSAIC: string; 925 | PLAYLIST: string; 926 | PLAYLIST_V2: string; 927 | PRERELEASE: string; 928 | PROFILE: string; 929 | PUBLISHED_ROOTLIST: string; 930 | RADIO: string; 931 | ROOTLIST: string; 932 | SEARCH: string; 933 | SHOW: string; 934 | SOCIAL_SESSION: string; 935 | SPECIAL: string; 936 | STARRED: string; 937 | STATION: string; 938 | TEMP_PLAYLIST: string; 939 | TOPLIST: string; 940 | TRACK: string; 941 | TRACKSET: string; 942 | USER_TOPLIST: string; 943 | USER_TOP_TRACKS: string; 944 | UNKNOWN: string; 945 | MEDIA: string; 946 | QUESTION: string; 947 | POLL: string; 948 | }; 949 | 950 | /** 951 | * Creates a new URI object from a parsed string argument. 952 | * 953 | * @param str The string that will be parsed into a URI object. 954 | * @throws TypeError If the string argument is not a valid URI, a TypeError will 955 | * be thrown. 956 | * @return The parsed URI object. 957 | */ 958 | static fromString(str: string): URI; 959 | 960 | /** 961 | * Parses a given object into a URI instance. 962 | * 963 | * Unlike URI.fromString, this function could receive any kind of value. If 964 | * the value is already a URI instance, it is simply returned. 965 | * Otherwise the value will be stringified before parsing. 966 | * 967 | * This function also does not throw an error like URI.fromString, but 968 | * instead simply returns null if it can't parse the value. 969 | * 970 | * @param value The value to parse. 971 | * @return The corresponding URI instance, or null if the 972 | * passed value is not a valid value. 973 | */ 974 | static from(value: any): URI | null; 975 | 976 | /** 977 | * Checks whether two URI:s refer to the same thing even though they might 978 | * not necessarily be equal. 979 | * 980 | * These two Playlist URIs, for example, refer to the same playlist: 981 | * 982 | * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 983 | * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 984 | * 985 | * @param baseUri The first URI to compare. 986 | * @param refUri The second URI to compare. 987 | * @return Whether they shared idenitity 988 | */ 989 | static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; 990 | 991 | /** 992 | * Returns the hex representation of a Base62 encoded id. 993 | * 994 | * @param id The base62 encoded id. 995 | * @return The hex representation of the base62 id. 996 | */ 997 | static idToHex(id: string): string; 998 | 999 | /** 1000 | * Returns the base62 representation of a hex encoded id. 1001 | * 1002 | * @param hex The hex encoded id. 1003 | * @return The base62 representation of the id. 1004 | */ 1005 | static hexToId(hex: string): string; 1006 | 1007 | /** 1008 | * Creates a new 'album' type URI. 1009 | * 1010 | * @param id The id of the album. 1011 | * @param disc The disc number of the album. 1012 | * @return The album URI. 1013 | */ 1014 | static albumURI(id: string, disc: number): URI; 1015 | 1016 | /** 1017 | * Creates a new 'application' type URI. 1018 | * 1019 | * @param id The id of the application. 1020 | * @param args An array containing the arguments to the app. 1021 | * @return The application URI. 1022 | */ 1023 | static applicationURI(id: string, args: string[]): URI; 1024 | 1025 | /** 1026 | * Creates a new 'artist' type URI. 1027 | * 1028 | * @param id The id of the artist. 1029 | * @return The artist URI. 1030 | */ 1031 | static artistURI(id: string): URI; 1032 | 1033 | /** 1034 | * Creates a new 'collection' type URI. 1035 | * 1036 | * @param username The non-canonical username of the rootlist owner. 1037 | * @param category The category of the collection. 1038 | * @return The collection URI. 1039 | */ 1040 | static collectionURI(username: string, category: string): URI; 1041 | 1042 | /** 1043 | * Creates a new 'collection-album' type URI. 1044 | * 1045 | * @param username The non-canonical username of the rootlist owner. 1046 | * @param id The id of the album. 1047 | * @return The collection album URI. 1048 | */ 1049 | static collectionAlbumURI(username: string, id: string): URI; 1050 | 1051 | /** 1052 | * Creates a new 'collection-artist' type URI. 1053 | * 1054 | * @param username The non-canonical username of the rootlist owner. 1055 | * @param id The id of the artist. 1056 | * @return The collection artist URI. 1057 | */ 1058 | static collectionAlbumURI(username: string, id: string): URI; 1059 | 1060 | /** 1061 | * Creates a new 'concert' type URI. 1062 | * 1063 | * @param id The id of the concert. 1064 | * @return The concert URI. 1065 | */ 1066 | static concertURI(id: string): URI; 1067 | 1068 | /** 1069 | * Creates a new 'episode' type URI. 1070 | * 1071 | * @param id The id of the episode. 1072 | * @return The episode URI. 1073 | */ 1074 | static episodeURI(id: string): URI; 1075 | 1076 | /** 1077 | * Creates a new 'folder' type URI. 1078 | * 1079 | * @param id The id of the folder. 1080 | * @return The folder URI. 1081 | */ 1082 | static folderURI(id: string): URI; 1083 | 1084 | /** 1085 | * Creates a new 'local-album' type URI. 1086 | * 1087 | * @param artist The artist of the album. 1088 | * @param album The name of the album. 1089 | * @return The local album URI. 1090 | */ 1091 | static localAlbumURI(artist: string, album: string): URI; 1092 | 1093 | /** 1094 | * Creates a new 'local-artist' type URI. 1095 | * 1096 | * @param artist The name of the artist. 1097 | * @return The local artist URI. 1098 | */ 1099 | static localArtistURI(artist: string): URI; 1100 | 1101 | /** 1102 | * Creates a new 'playlist-v2' type URI. 1103 | * 1104 | * @param id The id of the playlist. 1105 | * @return The playlist URI. 1106 | */ 1107 | static playlistV2URI(id: string): URI; 1108 | 1109 | /** 1110 | * Creates a new 'prerelease' type URI. 1111 | * 1112 | * @param id The id of the prerelease. 1113 | * @return The prerelease URI. 1114 | */ 1115 | static prereleaseURI(id: string): URI; 1116 | 1117 | /** 1118 | * Creates a new 'profile' type URI. 1119 | * 1120 | * @param username The non-canonical username of the rootlist owner. 1121 | * @param args A list of arguments. 1122 | * @return The profile URI. 1123 | */ 1124 | static profileURI(username: string, args: string[]): URI; 1125 | 1126 | /** 1127 | * Creates a new 'search' type URI. 1128 | * 1129 | * @param query The unencoded search query. 1130 | * @return The search URI 1131 | */ 1132 | static searchURI(query: string): URI; 1133 | 1134 | /** 1135 | * Creates a new 'show' type URI. 1136 | * 1137 | * @param id The id of the show. 1138 | * @return The show URI. 1139 | */ 1140 | static showURI(id: string): URI; 1141 | 1142 | /** 1143 | * Creates a new 'station' type URI. 1144 | * 1145 | * @param args An array of arguments for the station. 1146 | * @return The station URI. 1147 | */ 1148 | static stationURI(args: string[]): URI; 1149 | 1150 | /** 1151 | * Creates a new 'track' type URI. 1152 | * 1153 | * @param id The id of the track. 1154 | * @param anchor The point in the track formatted as mm:ss 1155 | * @param context An optional context URI 1156 | * @param play Toggles autoplay 1157 | * @return The track URI. 1158 | */ 1159 | static trackURI(id: string, anchor: string, context?: string, play?: boolean): URI; 1160 | 1161 | /** 1162 | * Creates a new 'user-toplist' type URI. 1163 | * 1164 | * @param username The non-canonical username of the toplist owner. 1165 | * @param toplist The toplist type. 1166 | * @return The user-toplist URI. 1167 | */ 1168 | static userToplistURI(username: string, toplist: string): URI; 1169 | 1170 | static isAd(uri: URI | string): boolean; 1171 | static isAlbum(uri: URI | string): boolean; 1172 | static isGenre(uri: URI | string): boolean; 1173 | static isQueue(uri: URI | string): boolean; 1174 | static isApplication(uri: URI | string): boolean; 1175 | static isArtist(uri: URI | string): boolean; 1176 | static isArtistToplist(uri: URI | string): boolean; 1177 | static isArtistConcerts(uri: URI | string): boolean; 1178 | static isAudioFile(uri: URI | string): boolean; 1179 | static isCollection(uri: URI | string): boolean; 1180 | static isCollectionAlbum(uri: URI | string): boolean; 1181 | static isCollectionArtist(uri: URI | string): boolean; 1182 | static isCollectionMissingAlbum(uri: URI | string): boolean; 1183 | static isCollectionTrackList(uri: URI | string): boolean; 1184 | static isConcert(uri: URI | string): boolean; 1185 | static isContextGroup(uri: URI | string): boolean; 1186 | static isDailyMix(uri: URI | string): boolean; 1187 | static isEmpty(uri: URI | string): boolean; 1188 | static isEpisode(uri: URI | string): boolean; 1189 | static isFacebook(uri: URI | string): boolean; 1190 | static isFolder(uri: URI | string): boolean; 1191 | static isFollowers(uri: URI | string): boolean; 1192 | static isFollowing(uri: URI | string): boolean; 1193 | static isImage(uri: URI | string): boolean; 1194 | static isInbox(uri: URI | string): boolean; 1195 | static isInterruption(uri: URI | string): boolean; 1196 | static isLibrary(uri: URI | string): boolean; 1197 | static isLive(uri: URI | string): boolean; 1198 | static isRoom(uri: URI | string): boolean; 1199 | static isExpression(uri: URI | string): boolean; 1200 | static isLocal(uri: URI | string): boolean; 1201 | static isLocalTrack(uri: URI | string): boolean; 1202 | static isLocalAlbum(uri: URI | string): boolean; 1203 | static isLocalArtist(uri: URI | string): boolean; 1204 | static isMerch(uri: URI | string): boolean; 1205 | static isMosaic(uri: URI | string): boolean; 1206 | static isPlaylist(uri: URI | string): boolean; 1207 | static isPlaylistV2(uri: URI | string): boolean; 1208 | static isPrerelease(uri: URI | string): boolean; 1209 | static isProfile(uri: URI | string): boolean; 1210 | static isPublishedRootlist(uri: URI | string): boolean; 1211 | static isRadio(uri: URI | string): boolean; 1212 | static isRootlist(uri: URI | string): boolean; 1213 | static isSearch(uri: URI | string): boolean; 1214 | static isShow(uri: URI | string): boolean; 1215 | static isSocialSession(uri: URI | string): boolean; 1216 | static isSpecial(uri: URI | string): boolean; 1217 | static isStarred(uri: URI | string): boolean; 1218 | static isStation(uri: URI | string): boolean; 1219 | static isTempPlaylist(uri: URI | string): boolean; 1220 | static isToplist(uri: URI | string): boolean; 1221 | static isTrack(uri: URI | string): boolean; 1222 | static isTrackset(uri: URI | string): boolean; 1223 | static isUserToplist(uri: URI | string): boolean; 1224 | static isUserTopTracks(uri: URI | string): boolean; 1225 | static isUnknown(uri: URI | string): boolean; 1226 | static isMedia(uri: URI | string): boolean; 1227 | static isQuestion(uri: URI | string): boolean; 1228 | static isPoll(uri: URI | string): boolean; 1229 | static isPlaylistV1OrV2(uri: URI | string): boolean; 1230 | } 1231 | 1232 | /** 1233 | * Create custom menu item and prepend to right click context menu 1234 | */ 1235 | namespace ContextMenu { 1236 | type OnClickCallback = (uris: string[], uids?: string[], contextUri?: string) => void; 1237 | type ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: string) => boolean; 1238 | 1239 | // Single context menu item 1240 | class Item { 1241 | /** 1242 | * List of valid icons to use. 1243 | */ 1244 | static readonly iconList: Icon[]; 1245 | constructor(name: string, onClick: OnClickCallback, shouldAdd?: ShouldAddCallback, icon?: Icon, disabled?: boolean); 1246 | name: string; 1247 | icon: Icon | string; 1248 | disabled: boolean; 1249 | /** 1250 | * A function returning boolean determines whether item should be prepended. 1251 | */ 1252 | shouldAdd: ShouldAddCallback; 1253 | /** 1254 | * A function to call when item is clicked 1255 | */ 1256 | onClick: OnClickCallback; 1257 | /** 1258 | * Item is only available in Context Menu when method "register" is called. 1259 | */ 1260 | register: () => void; 1261 | /** 1262 | * Stop Item to be prepended into Context Menu. 1263 | */ 1264 | deregister: () => void; 1265 | } 1266 | 1267 | /** 1268 | * Create a sub menu to contain `Item`s. 1269 | * `Item`s in `subItems` array shouldn't be registered. 1270 | */ 1271 | class SubMenu { 1272 | constructor(name: string, subItems: Iterable, shouldAdd?: ShouldAddCallback, disabled?: boolean); 1273 | name: string; 1274 | disabled: boolean; 1275 | /** 1276 | * A function returning boolean determines whether item should be prepended. 1277 | */ 1278 | shouldAdd: ShouldAddCallback; 1279 | addItem: (item: Item) => void; 1280 | removeItem: (item: Item) => void; 1281 | /** 1282 | * SubMenu is only available in Context Menu when method "register" is called. 1283 | */ 1284 | register: () => void; 1285 | /** 1286 | * Stop SubMenu to be prepended into Context Menu. 1287 | */ 1288 | deregister: () => void; 1289 | } 1290 | } 1291 | 1292 | /** 1293 | * Popup Modal 1294 | */ 1295 | namespace PopupModal { 1296 | interface Content { 1297 | title: string; 1298 | /** 1299 | * You can specify a string for simple text display 1300 | * or a HTML element for interactive config/setting menu 1301 | */ 1302 | content: string | Element; 1303 | /** 1304 | * Bigger window 1305 | */ 1306 | isLarge?: boolean; 1307 | } 1308 | 1309 | function display(e: Content): void; 1310 | function hide(): void; 1311 | } 1312 | 1313 | /** React instance to create components */ 1314 | const React: any; 1315 | /** React DOM instance to render and mount components */ 1316 | const ReactDOM: any; 1317 | /** React DOM Server instance to render components to string */ 1318 | const ReactDOMServer: any; 1319 | 1320 | /** Stock React components exposed from Spotify library */ 1321 | namespace ReactComponent { 1322 | type ContextMenuProps = { 1323 | /** 1324 | * Decide whether to use the global singleton context menu (rendered in ) 1325 | * or a new inline context menu (rendered in a sibling 1326 | * element to `children`) 1327 | */ 1328 | renderInline?: boolean; 1329 | /** 1330 | * Determins what will trigger the context menu. For example, a click, or a right-click 1331 | */ 1332 | trigger?: "click" | "right-click"; 1333 | /** 1334 | * Determins is the context menu should open or toggle when triggered 1335 | */ 1336 | action?: "toggle" | "open"; 1337 | /** 1338 | * The preferred placement of the context menu when it opens. 1339 | * Relative to trigger element. 1340 | */ 1341 | placement?: 1342 | | "top" 1343 | | "top-start" 1344 | | "top-end" 1345 | | "right" 1346 | | "right-start" 1347 | | "right-end" 1348 | | "bottom" 1349 | | "bottom-start" 1350 | | "bottom-end" 1351 | | "left" 1352 | | "left-start" 1353 | | "left-end"; 1354 | /** 1355 | * The x and y offset distances at which the context menu should open. 1356 | * Relative to trigger element and `position`. 1357 | */ 1358 | offset?: [number, number]; 1359 | /** 1360 | * Will stop the client from scrolling while the context menu is open 1361 | */ 1362 | preventScrollingWhileOpen?: boolean; 1363 | /** 1364 | * The menu UI to render inside of the context menu. 1365 | */ 1366 | menu: 1367 | | typeof Spicetify.ReactComponent.Menu 1368 | | typeof Spicetify.ReactComponent.AlbumMenu 1369 | | typeof Spicetify.ReactComponent.PodcastShowMenu 1370 | | typeof Spicetify.ReactComponent.ArtistMenu 1371 | | typeof Spicetify.ReactComponent.PlaylistMenu; 1372 | /** 1373 | * A child of the context menu. Should be `