├── .gitignore ├── extractor.psd ├── firefox ├── icons │ ├── icon32.png │ ├── icon48.png │ ├── icon96.png │ └── icon128.png ├── .web-extension-id ├── manifest.json ├── background.js └── extractor.js ├── chromium ├── icons │ ├── icon128.png │ ├── icon32.png │ ├── icon48.png │ └── icon96.png ├── service.js ├── extractor.js └── manifest.json ├── manifest.json ├── package.json ├── .github └── workflows │ └── release.yml ├── RELEASING.md ├── spicetify ├── extractor.js ├── pnpm-lock.yaml └── globals.d.ts ├── README.md └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.crx 2 | *.pem 3 | *.xpi 4 | *.zip 5 | node_modules 6 | -------------------------------------------------------------------------------- /extractor.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/extractor.psd -------------------------------------------------------------------------------- /firefox/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/firefox/icons/icon32.png -------------------------------------------------------------------------------- /firefox/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/firefox/icons/icon48.png -------------------------------------------------------------------------------- /firefox/icons/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/firefox/icons/icon96.png -------------------------------------------------------------------------------- /chromium/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/chromium/icons/icon128.png -------------------------------------------------------------------------------- /chromium/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/chromium/icons/icon32.png -------------------------------------------------------------------------------- /chromium/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/chromium/icons/icon48.png -------------------------------------------------------------------------------- /chromium/icons/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/chromium/icons/icon96.png -------------------------------------------------------------------------------- /firefox/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afonsojramos/spotify-details-extractor/HEAD/firefox/icons/icon128.png -------------------------------------------------------------------------------- /firefox/.web-extension-id: -------------------------------------------------------------------------------- 1 | # This file was created by https://github.com/mozilla/web-ext 2 | # Your auto-generated extension ID for addons.mozilla.org is: 3 | {8301b4fd-b04f-40f7-be18-55f0da220794} 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Details Extractor", 3 | "description": "Spicetify extension to extract Spotify details from an album page in a specific JSON object.", 4 | "preview": "chromium/icons/icon128.png", 5 | "main": "spicetify/extractor.js", 6 | "readme": "README.md" 7 | } 8 | -------------------------------------------------------------------------------- /chromium/service.js: -------------------------------------------------------------------------------- 1 | chrome.action.onClicked.addListener((tab) => { 2 | if (tab.url.match(/https:\/\/open\.spotify\.com\/\w*\/\w*/)) { 3 | chrome.scripting.executeScript({ 4 | target: { tabId: tab.id }, 5 | files: ['extractor.js'], 6 | }); 7 | } 8 | }); 9 | 10 | chrome.tabs.onActivated.addListener((activeInfo) => { 11 | chrome.tabs.get(activeInfo.tabId, async (tab) => { 12 | if (!tab.url.match(/https:\/\/open\.spotify\.com\/\w*\/\w*/)) chrome.action.disable(tab.id); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Details Extractor", 3 | "description": "Extract the most important details of an album to the desired JSON.", 4 | "manifest_version": 2, 5 | "version": "2.5.0", 6 | "background": { 7 | "scripts": ["background.js"] 8 | }, 9 | "permissions": ["tabs", "*://*.spotify.com/*", "contextMenus"], 10 | "icons": { 11 | "32": "/icons/icon32.png", 12 | "48": "/icons/icon48.png", 13 | "96": "/icons/icon96.png", 14 | "128": "/icons/icon128.png" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chromium/extractor.js: -------------------------------------------------------------------------------- 1 | artists = []; 2 | document 3 | .querySelectorAll("section > div:first-child > div > div span > a") 4 | .forEach((artist) => artists.push(artist.innerHTML)); 5 | 6 | album = { 7 | title: document.querySelector("h1").innerText, 8 | artist: 9 | artists.length === 1 10 | ? artists[0] 11 | : artists.reduce((artist, artistSum) => `${artist}, ${artistSum}`), 12 | image: document.querySelector("section > div > div > div > img").src, 13 | url: window.location.href.match(/https:\/\/open\.spotify\.com\/\w*\/\w*/)[0], 14 | }; 15 | 16 | navigator.clipboard.writeText(JSON.stringify(album)); 17 | -------------------------------------------------------------------------------- /chromium/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Details Extractor", 3 | "action": {}, 4 | "description": "Extract the most important details of an album to the desired JSON.", 5 | "version": "2.5.0", 6 | "manifest_version": 3, 7 | "homepage_url": "https://github.com/afonsojramos/spotify-details-extractor", 8 | "background": { 9 | "service_worker": "service.js" 10 | }, 11 | "permissions": ["scripting", "tabs"], 12 | "host_permissions": ["*://*.spotify.com/*"], 13 | "icons": { 14 | "32": "/icons/icon32.png", 15 | "48": "/icons/icon48.png", 16 | "96": "/icons/icon96.png", 17 | "128": "/icons/icon128.png" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /firefox/background.js: -------------------------------------------------------------------------------- 1 | browser.contextMenus.create({ 2 | id: 'extract-album', 3 | title: 'Extract Album Info', 4 | documentUrlPatterns: ['https://open.spotify.com/*'], 5 | }); 6 | 7 | function messageTab(tabs) { 8 | browser.tabs.sendMessage(tabs[0].id, { 9 | replacement: 'Extracting!', 10 | }); 11 | } 12 | 13 | function onExecuted() { 14 | let querying = browser.tabs.query({ 15 | active: true, 16 | currentWindow: true, 17 | }); 18 | querying.then(messageTab); 19 | } 20 | 21 | browser.contextMenus.onClicked.addListener(function (info, _tab) { 22 | if (info.menuItemId == 'extract-album') { 23 | let executing = browser.tabs.executeScript({ 24 | file: 'extractor.js', 25 | }); 26 | executing.then(onExecuted); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /firefox/extractor.js: -------------------------------------------------------------------------------- 1 | extractorReceiver = () => { 2 | artists = []; 3 | document 4 | .querySelectorAll("section > div:first-child > div > div span > a") 5 | .forEach((artist) => artists.push(artist.innerHTML)); 6 | 7 | album = { 8 | title: document.querySelector("h1").innerText, 9 | artist: 10 | artists.length === 1 11 | ? artists[0] 12 | : artists.reduce((artist, artistSum) => `${artist}, ${artistSum}`), 13 | image: document.querySelector("section > div > div > div > img").src, 14 | url: window.location.href.match( 15 | /https:\/\/open\.spotify\.com\/\w*\/\w*/ 16 | )[0], 17 | }; 18 | 19 | navigator.clipboard.writeText(JSON.stringify(album)); 20 | }; 21 | 22 | browser.runtime.onMessage.addListener(extractorReceiver); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "chromium:build": "zip -r spotify-details-extractor-chromium.zip ./chromium/", 4 | "firefox:version": "sed -i '' -e 's/\"version\": \".*\"/\"version\": \"$(git describe --tags --abbrev=0)\"/' ./firefox/manifest.json", 5 | "firefox:dev": "web-ext run -s firefox", 6 | "firefox:build": "web-ext build -s firefox --overwrite-dest", 7 | "chromium:version": "sed -i '' -e 's/\"version\": \".*\"/\"version\": \"$(git describe --tags --abbrev=0)\"/' ./chromium/manifest.json", 8 | "firefox:release": "web-ext sign -s firefox --api-key=$JWT_ISSUER --api-secret=$JWT_SECRET", 9 | "spicetify": "spicetify enable-devtools & spicetify watch -le", 10 | "dev": "pnpm watch & pnpm spicetify", 11 | "watch": "onchange 'extractor.js' -- pnpm copy", 12 | "copy": "cp extractor.js ~/.spicetify/Extensions", 13 | "setup": "spicetify config extensions extractor.js", 14 | "upgrade": "pnpm chromium:version && pnpm firefox:version" 15 | }, 16 | "dependencies": { 17 | "@types/react": "^18.2.6", 18 | "onchange": "^7.1.0", 19 | "web-ext": "^7.6.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 8 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Build Chromium 26 | run: pnpm run chromium:build 27 | 28 | - name: Build Firefox 29 | run: pnpm run firefox:build 30 | 31 | - name: Release Firefox 32 | run: pnpm run firefox:release 33 | env: 34 | JWT_SECRET: ${{ secrets.JWT_SECRET }} 35 | JWT_ISSUER: ${{ secrets.JWT_ISSUER }} 36 | 37 | - name: Rename files 38 | run: | 39 | mv web-ext-artifacts/spotify_details_extractor-${{ github.ref_name }}.zip spotify-details-extractor-firefox.zip 40 | mv spicetify/extractor.js spotify-details-extractor-spicetify.js 41 | 42 | - name: Upload Linux release assets 43 | env: 44 | GH_TOKEN: ${{ github.token }} 45 | run: | 46 | gh release upload ${{ github.ref_name }} spotify-details-extractor-firefox.zip 47 | gh release upload ${{ github.ref_name }} spotify-details-extractor-spicetify.js 48 | gh release upload ${{ github.ref_name }} spotify-details-extractor-chromium.zip 49 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation and Compilation 3 | 4 | ## Chromium 5 | 6 | ### Development 7 | 8 | Chromium only requires you to go to `chrome://extensions`, activate **Developer Mode** and `Load Unpacked` by selecting the folder that you have the extension on. No need to zip it or package it in any way. More reference on how to manually install Chrome extensions [here](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest). 9 | 10 | ### Release 11 | 12 | In order to release, all you need to do is select `Pack Extension` under `chrome://extensions`, select the folder and that's it! Remember to save the key somewhere safe to generate new versions of the extension. 13 | 14 | Alternatively, you may, using 7-Zip, run `zip -r dist/spotify-details-extractor.zip .\chromium\` (Unix) or `7z a -tzip dist/spotify-details-extractor.zip .\chromium\` (Windows), and then upload the archive to the [Chrome Web Store Dev Console](https://chrome.google.com/webstore/devconsole/). Finally, you will find the `.crx` extension under the Package tab. This one will be signed and will now show any warning when installing. 15 | 16 | ## Firefox 17 | 18 | Firefox add-ons, before generating an installable `.xpi` file, must be digitally signed by Mozilla, which can be a tiny bit tedious. 19 | 20 | First of all, install the `web-ext` tool with the following `npm install -g web-ext`. 21 | 22 | ### Development 23 | 24 | Development is very streamlined with its own self-contained browser session using the following command `web-ext run -s firefox`. 25 | 26 | ### Release 27 | 28 | In order to release, you first need to build the `.zip` file inside a new `/web-ext-artifacts` directory, which can also be loaded as a temporary extension in Firefox through the `about:debugging` page with the following: `web-ext build -s firefox --overwrite-dest`. 29 | 30 | Afterwards, you need to sign the extension. For this you'll need to generate your [addons.mozilla.org credentials](https://addons.mozilla.org/en-GB/developers/addon/api/key/). 31 | 32 | Then, simply run the following command `web-ext sign -s firefox --api-key=JWT_ISSUER --api-secret=JWT_SECRET` with the API key and secret parameters that you generated. The new `.xpi` file can also be found in the `/web-ext-artifacts` directory. -------------------------------------------------------------------------------- /spicetify/extractor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // NAME: Spotify Details Extractor 4 | // AUTHOR: afonsojramos 5 | // DESCRIPTION: Extracts album information from Spotify. 6 | 7 | /// 8 | 9 | (function SpotifyDetailsExtractor() { 10 | if (!Spicetify.CosmosAsync || !Spicetify.Platform) { 11 | setTimeout(SpotifyDetailsExtractor, 1000); 12 | return; 13 | } 14 | 15 | const cntxMenu = new Spicetify.ContextMenu.Item( 16 | "Extract Album Info", 17 | (uris) => { 18 | try { 19 | const artists = []; 20 | document 21 | .querySelectorAll("section > div:first-child > div > div span > a") 22 | .forEach((artist) => artists.push(artist.innerHTML)); 23 | 24 | const album = { 25 | title: document.querySelector("h1")?.innerText || "", 26 | artist: 27 | artists.length === 1 28 | ? artists[0] 29 | : artists.reduce( 30 | (artist, artistSum) => `${artist}, ${artistSum}` 31 | ), 32 | // @ts-ignore 33 | image: document.querySelector("section > div > div > div > img").src, 34 | url: Spicetify.URI.fromString(uris[0]) 35 | .toURL() 36 | .replace("play", "open"), 37 | }; 38 | Spicetify.CosmosAsync.put("sp://desktop/v1/clipboard", album); 39 | success(album.title); 40 | } catch (e) { 41 | console.error(e); 42 | Spicetify.showNotification("Something went wrong. Please try again."); 43 | } 44 | }, 45 | (uris) => { 46 | if (uris.length === 1) { 47 | const uriObj = Spicetify.URI.fromString(uris[0]); 48 | switch (uriObj.type) { 49 | case Spicetify.URI.Type.ALBUM: 50 | case Spicetify.URI.Type.COLLECTION: 51 | return true; 52 | } 53 | return false; 54 | } 55 | // User selects multiple tracks in a list. 56 | return false; 57 | }, 58 | "download" 59 | ); 60 | cntxMenu.register(); 61 | 62 | /** 63 | * Text of notification when information is extracted successfully. 64 | * @param {string} title 65 | */ 66 | function success(title) { 67 | Spicetify.showNotification(`Copied ${title}'s info to the clipboard!`); 68 | } 69 | })(); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Spotify Details Extractor 🎶

3 | 4 | Simple browser extension to extract Spotify details from an album page in a specific JSON object. You can find it in any context menu/extension bar near you! 5 | 6 | **PS**: This extension tries to be the least intrusive possible and will only show up in the context menu when you are in the Spotify Web App in the case of the Firefox browser. On Chromium, the extension will show as disabled on other websites. Additionally, in Firefox, since Spotify captures the user's `Right Click`, you need to press `Shift` + `Right Click` for it to show up. This does not work in Chromium, which is why the implementation is different. 7 | 8 | ## Installation 9 | 10 | 11 | 12 | 15 | 18 | 19 | 20 | 23 | 26 | 27 | 28 | 33 | 36 | 37 | 38 | 45 | 48 | 49 |
13 | Browser Download 14 | 16 | Usage Example 17 |
21 | 22 | 24 | 25 |
29 | 30 | 31 | 32 | 34 | 35 |
39 | 40 | 41 | 42 |
43 | Spicetify Marketplace or manually. 44 |
46 | 47 |
50 | 51 | ## Manual Installation 52 | 53 | ### Firefox 54 | 55 | Navigate to `about:addons`, select **Install Add-on From File...** and choose the `.xpi` extension that you've downloaded from the [GitHub releases page](https://github.com/afonsojramos/spotify-details-extractor/releases/latest). 56 | 57 | ### Chromium 58 | 59 | Navigate to `chrome://extensions` and drag the `.crx` extension that you've downloaded from the [GitHub releases page](https://github.com/afonsojramos/spotify-details-extractor/releases/latest). 60 | 61 | ### Spicetify 62 | 63 | Navigate to `~/.config/spicetify/Extensions` and download `extractor.js` that can be found in the [GitHub releases page](https://github.com/afonsojramos/spotify-details-extractor/releases/latest). Then, using `spicetify config extensions extractor.js`, enable the extension. 64 | 65 | ## Motivation 66 | 67 | Currently, my personal website uses a [JSON Database](https://github.com/afonsojramos/afonsojramos.me/blob/main/data/music.json) to store the details of my favorite albums of the year. 68 | 69 | Each entry is constructed by the following JSON schema: 70 | 71 | ```json 72 | { 73 | "title": "For the first time", 74 | "artist": "Black Country, New Road", 75 | "image": "https://i.scdn.co/image/ab67616d00001e020ffaa4f75b2297d36ff1e0ad", 76 | "url": "https://open.spotify.com/album/2PfgptDcfJTFtoZIS3AukX" 77 | } 78 | ``` 79 | 80 | The resulting page can be seen in [afonsojramos.me/music](afonsojramos.me/music). 81 | 82 |

83 | 84 | However, the process of extracting the details from the album page is quite tedious as I have to **manually** copy the album's URL, and extract the album's title, artist and image URL. All of this requires the opening of the developer's console and makes the process rather slow. 85 | 86 | Therefore, I decided to create a browser extension that will **extract the details** from the album page, store them in the desired JSON object, and **automatically copy it to the clipboard**. 87 | 88 | ## Implementation 89 | 90 | Initially, I was going to create an extension that would create an in-page button that would trigger the events. I was somewhat successful in this (it works perfectly on Spicetify), but on Spotify's Web App things are a bit more complicated as it meant interacting with the page's DOM, which I preferred not to do as it would be more prone to errors. 91 | 92 | With this in mind, `v2` shifted to a simple context menu on Firefox and the extension button on Chromium, due to the latter not supporting context menus. These proved to be way more reliable and faster than the previous approach. 93 | 94 | ## More 95 | 🌟 Like it? Gimme some love! 96 | [![Github Stars badge](https://img.shields.io/github/stars/afonsojramos/spotify-details-extractor?logo=github&style=social)](https://github.com/afonsojramos/spotify-details-extractor/) 97 | 98 | If you find any bugs or places where podcasts are still showing up, please [create a new issue](https://github.com/afonsojramos/spotify-details-extractor/issues/new/choose) on the GitHub repo. 99 | ![https://github.com/afonsojramos/spotify-details-extractor/issues](https://img.shields.io/github/issues/afonsojramos/spotify-details-extractor?logo=github) 100 | -------------------------------------------------------------------------------- /spicetify/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | '@types/react': 5 | specifier: ^18.2.6 6 | version: 18.2.6 7 | onchange: 8 | specifier: ^7.1.0 9 | version: 7.1.0 10 | 11 | packages: 12 | 13 | /@blakeembrey/deque@1.0.5: 14 | resolution: {integrity: sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==} 15 | dev: false 16 | 17 | /@blakeembrey/template@1.1.0: 18 | resolution: {integrity: sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==} 19 | dev: false 20 | 21 | /@types/prop-types@15.7.5: 22 | resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} 23 | dev: false 24 | 25 | /@types/react@18.2.6: 26 | resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==} 27 | dependencies: 28 | '@types/prop-types': 15.7.5 29 | '@types/scheduler': 0.16.3 30 | csstype: 3.1.2 31 | dev: false 32 | 33 | /@types/scheduler@0.16.3: 34 | resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} 35 | dev: false 36 | 37 | /anymatch@3.1.3: 38 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 39 | engines: {node: '>= 8'} 40 | dependencies: 41 | normalize-path: 3.0.0 42 | picomatch: 2.3.1 43 | dev: false 44 | 45 | /arg@4.1.3: 46 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 47 | dev: false 48 | 49 | /binary-extensions@2.2.0: 50 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 51 | engines: {node: '>=8'} 52 | dev: false 53 | 54 | /braces@3.0.2: 55 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 56 | engines: {node: '>=8'} 57 | dependencies: 58 | fill-range: 7.0.1 59 | dev: false 60 | 61 | /chokidar@3.5.3: 62 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 63 | engines: {node: '>= 8.10.0'} 64 | dependencies: 65 | anymatch: 3.1.3 66 | braces: 3.0.2 67 | glob-parent: 5.1.2 68 | is-binary-path: 2.1.0 69 | is-glob: 4.0.3 70 | normalize-path: 3.0.0 71 | readdirp: 3.6.0 72 | optionalDependencies: 73 | fsevents: 2.3.2 74 | dev: false 75 | 76 | /cross-spawn@7.0.3: 77 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 78 | engines: {node: '>= 8'} 79 | dependencies: 80 | path-key: 3.1.1 81 | shebang-command: 2.0.0 82 | which: 2.0.2 83 | dev: false 84 | 85 | /csstype@3.1.2: 86 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 87 | dev: false 88 | 89 | /fill-range@7.0.1: 90 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 91 | engines: {node: '>=8'} 92 | dependencies: 93 | to-regex-range: 5.0.1 94 | dev: false 95 | 96 | /fsevents@2.3.2: 97 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 98 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 99 | os: [darwin] 100 | requiresBuild: true 101 | dev: false 102 | optional: true 103 | 104 | /glob-parent@5.1.2: 105 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 106 | engines: {node: '>= 6'} 107 | dependencies: 108 | is-glob: 4.0.3 109 | dev: false 110 | 111 | /ignore@5.2.4: 112 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 113 | engines: {node: '>= 4'} 114 | dev: false 115 | 116 | /is-binary-path@2.1.0: 117 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 118 | engines: {node: '>=8'} 119 | dependencies: 120 | binary-extensions: 2.2.0 121 | dev: false 122 | 123 | /is-extglob@2.1.1: 124 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 125 | engines: {node: '>=0.10.0'} 126 | dev: false 127 | 128 | /is-glob@4.0.3: 129 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 130 | engines: {node: '>=0.10.0'} 131 | dependencies: 132 | is-extglob: 2.1.1 133 | dev: false 134 | 135 | /is-number@7.0.0: 136 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 137 | engines: {node: '>=0.12.0'} 138 | dev: false 139 | 140 | /isexe@2.0.0: 141 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 142 | dev: false 143 | 144 | /normalize-path@3.0.0: 145 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 146 | engines: {node: '>=0.10.0'} 147 | dev: false 148 | 149 | /onchange@7.1.0: 150 | resolution: {integrity: sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==} 151 | hasBin: true 152 | dependencies: 153 | '@blakeembrey/deque': 1.0.5 154 | '@blakeembrey/template': 1.1.0 155 | arg: 4.1.3 156 | chokidar: 3.5.3 157 | cross-spawn: 7.0.3 158 | ignore: 5.2.4 159 | tree-kill: 1.2.2 160 | dev: false 161 | 162 | /path-key@3.1.1: 163 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 164 | engines: {node: '>=8'} 165 | dev: false 166 | 167 | /picomatch@2.3.1: 168 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 169 | engines: {node: '>=8.6'} 170 | dev: false 171 | 172 | /readdirp@3.6.0: 173 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 174 | engines: {node: '>=8.10.0'} 175 | dependencies: 176 | picomatch: 2.3.1 177 | dev: false 178 | 179 | /shebang-command@2.0.0: 180 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 181 | engines: {node: '>=8'} 182 | dependencies: 183 | shebang-regex: 3.0.0 184 | dev: false 185 | 186 | /shebang-regex@3.0.0: 187 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 188 | engines: {node: '>=8'} 189 | dev: false 190 | 191 | /to-regex-range@5.0.1: 192 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 193 | engines: {node: '>=8.0'} 194 | dependencies: 195 | is-number: 7.0.0 196 | dev: false 197 | 198 | /tree-kill@1.2.2: 199 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 200 | hasBin: true 201 | dev: false 202 | 203 | /which@2.0.2: 204 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 205 | engines: {node: '>= 8'} 206 | hasBin: true 207 | dependencies: 208 | isexe: 2.0.0 209 | dev: false 210 | -------------------------------------------------------------------------------- /spicetify/globals.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 Metadata = Partial>; 130 | type ContextTrack = { 131 | uri: string; 132 | uid?: string; 133 | metadata?: Metadata; 134 | }; 135 | type ProvidedTrack = ContextTrack & { 136 | removed?: string[]; 137 | blocked?: string[]; 138 | provider?: string; 139 | }; 140 | type ContextOption = { 141 | contextURI?: string; 142 | index?: number; 143 | trackUri?: string; 144 | page?: number; 145 | trackUid?: string; 146 | sortedBy?: string; 147 | filteredBy?: string; 148 | shuffleContext?: boolean; 149 | repeatContext?: boolean; 150 | repeatTrack?: boolean; 151 | offset?: number; 152 | next_page_url?: string; 153 | restrictions?: Record; 154 | referrer?: string; 155 | }; 156 | type PlayerState = { 157 | timestamp: number; 158 | context_uri: string; 159 | context_url: string; 160 | context_restrictions: Record; 161 | index?: { 162 | page: number; 163 | track: number; 164 | }; 165 | track?: ProvidedTrack; 166 | playback_id?: string; 167 | playback_quality?: string; 168 | playback_speed?: number; 169 | position_as_of_timestamp: number; 170 | duration: number; 171 | is_playing: boolean; 172 | is_paused: boolean; 173 | is_buffering: boolean; 174 | play_origin: { 175 | feature_identifier: string; 176 | feature_version: string; 177 | view_uri?: string; 178 | external_referrer?: string; 179 | referrer_identifier?: string; 180 | device_identifier?: string; 181 | }; 182 | options: { 183 | shuffling_context?: boolean; 184 | repeating_context?: boolean; 185 | repeating_track?: boolean; 186 | }; 187 | restrictions: Record; 188 | suppressions: { 189 | providers: string[]; 190 | }; 191 | debug: { 192 | log: string[]; 193 | }; 194 | prev_tracks: ProvidedTrack[]; 195 | next_tracks: ProvidedTrack[]; 196 | context_metadata: Metadata; 197 | page_metadata: Metadata; 198 | session_id: string; 199 | queue_revision: string; 200 | }; 201 | namespace Player { 202 | /** 203 | * Register a listener `type` on Spicetify.Player. 204 | * 205 | * On default, `Spicetify.Player` always dispatch: 206 | * - `songchange` type when player changes track. 207 | * - `onplaypause` type when player plays or pauses. 208 | * - `onprogress` type when track progress changes. 209 | * - `appchange` type when user changes page. 210 | */ 211 | function addEventListener( 212 | type: string, 213 | callback: (event?: Event) => void 214 | ): void; 215 | function addEventListener( 216 | type: "songchange", 217 | callback: (event?: Event & { data: PlayerState }) => void 218 | ): void; 219 | function addEventListener( 220 | type: "onplaypause", 221 | callback: (event?: Event & { data: PlayerState }) => void 222 | ): void; 223 | function addEventListener( 224 | type: "onprogress", 225 | callback: (event?: Event & { data: number }) => void 226 | ): void; 227 | function addEventListener( 228 | type: "appchange", 229 | callback: ( 230 | event?: Event & { 231 | data: { 232 | /** 233 | * App href path 234 | */ 235 | path: string; 236 | /** 237 | * App container 238 | */ 239 | container: HTMLElement; 240 | }; 241 | } 242 | ) => void 243 | ): void; 244 | /** 245 | * Skip to previous track. 246 | */ 247 | function back(): void; 248 | /** 249 | * An object contains all information about current track and player. 250 | */ 251 | const data: PlayerState; 252 | /** 253 | * Decrease a small amount of volume. 254 | */ 255 | function decreaseVolume(): void; 256 | /** 257 | * Dispatches an event at `Spicetify.Player`. 258 | * 259 | * On default, `Spicetify.Player` always dispatch 260 | * - `songchange` type when player changes track. 261 | * - `onplaypause` type when player plays or pauses. 262 | * - `onprogress` type when track progress changes. 263 | * - `appchange` type when user changes page. 264 | */ 265 | function dispatchEvent(event: Event): void; 266 | const eventListeners: { 267 | [key: string]: Array<(event?: Event) => void>; 268 | }; 269 | /** 270 | * Convert milisecond to `mm:ss` format 271 | * @param milisecond 272 | */ 273 | function formatTime(milisecond: number): string; 274 | /** 275 | * Return song total duration in milisecond. 276 | */ 277 | function getDuration(): number; 278 | /** 279 | * Return mute state 280 | */ 281 | function getMute(): boolean; 282 | /** 283 | * Return elapsed duration in milisecond. 284 | */ 285 | function getProgress(): number; 286 | /** 287 | * Return elapsed duration in percentage (0 to 1). 288 | */ 289 | function getProgressPercent(): number; 290 | /** 291 | * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). 292 | */ 293 | function getRepeat(): number; 294 | /** 295 | * Return current shuffle state. 296 | */ 297 | function getShuffle(): boolean; 298 | /** 299 | * Return track heart state. 300 | */ 301 | function getHeart(): boolean; 302 | /** 303 | * Return current volume level (0 to 1). 304 | */ 305 | function getVolume(): number; 306 | /** 307 | * Increase a small amount of volume. 308 | */ 309 | function increaseVolume(): void; 310 | /** 311 | * Return a boolean whether player is playing. 312 | */ 313 | function isPlaying(): boolean; 314 | /** 315 | * Skip to next track. 316 | */ 317 | function next(): void; 318 | /** 319 | * Pause track. 320 | */ 321 | function pause(): void; 322 | /** 323 | * Resume track. 324 | */ 325 | function play(): void; 326 | /** 327 | * Play a track, playlist, album, etc. immediately 328 | * @param uri Spotify URI 329 | * @param context 330 | * @param options 331 | */ 332 | function playUri(uri: string, context?: any, options?: any): Promise; 333 | /** 334 | * Unregister added event listener `type`. 335 | * @param type 336 | * @param callback 337 | */ 338 | function removeEventListener( 339 | type: string, 340 | callback: (event?: Event) => void 341 | ): void; 342 | /** 343 | * Seek track to position. 344 | * @param position can be in percentage (0 to 1) or in milisecond. 345 | */ 346 | function seek(position: number): void; 347 | /** 348 | * Turn mute on/off 349 | * @param state 350 | */ 351 | function setMute(state: boolean): void; 352 | /** 353 | * Change Repeat mode 354 | * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. 355 | */ 356 | function setRepeat(mode: number): void; 357 | /** 358 | * Turn shuffle on/off. 359 | * @param state 360 | */ 361 | function setShuffle(state: boolean): void; 362 | /** 363 | * Set volume level 364 | * @param level 0 to 1 365 | */ 366 | function setVolume(level: number): void; 367 | /** 368 | * Seek to previous `amount` of milisecond 369 | * @param amount in milisecond. Default: 15000. 370 | */ 371 | function skipBack(amount?: number): void; 372 | /** 373 | * Seek to next `amount` of milisecond 374 | * @param amount in milisecond. Default: 15000. 375 | */ 376 | function skipForward(amount?: number): void; 377 | /** 378 | * Toggle Heart (Favourite) track state. 379 | */ 380 | function toggleHeart(): void; 381 | /** 382 | * Toggle Mute/No mute. 383 | */ 384 | function toggleMute(): void; 385 | /** 386 | * Toggle Play/Pause. 387 | */ 388 | function togglePlay(): void; 389 | /** 390 | * Toggle No repeat/Repeat all/Repeat one. 391 | */ 392 | function toggleRepeat(): void; 393 | /** 394 | * Toggle Shuffle/No shuffle. 395 | */ 396 | function toggleShuffle(): void; 397 | } 398 | /** 399 | * Adds a track or array of tracks to prioritized queue. 400 | */ 401 | function addToQueue(uri: ContextTrack[]): Promise; 402 | /** 403 | * @deprecated 404 | */ 405 | const BridgeAPI: any; 406 | /** 407 | * @deprecated 408 | */ 409 | const CosmosAPI: any; 410 | /** 411 | * Async wrappers of CosmosAPI 412 | */ 413 | namespace CosmosAsync { 414 | type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; 415 | interface Error { 416 | code: number; 417 | error: string; 418 | message: string; 419 | stack?: string; 420 | } 421 | 422 | type Headers = Record; 423 | type Body = Record; 424 | 425 | interface Response { 426 | body: any; 427 | headers: Headers; 428 | status: number; 429 | uri: string; 430 | isSuccessStatus(status: number): boolean; 431 | } 432 | 433 | function head(url: string, headers?: Headers): Promise; 434 | function get( 435 | url: string, 436 | body?: Body, 437 | headers?: Headers 438 | ): Promise; 439 | function post( 440 | url: string, 441 | body?: Body, 442 | headers?: Headers 443 | ): Promise; 444 | function put( 445 | url: string, 446 | body?: Body, 447 | headers?: Headers 448 | ): Promise; 449 | function del( 450 | url: string, 451 | body?: Body, 452 | headers?: Headers 453 | ): Promise; 454 | function patch( 455 | url: string, 456 | body?: Body, 457 | headers?: Headers 458 | ): Promise; 459 | function sub( 460 | url: string, 461 | callback: (b: Response["body"]) => void, 462 | onError?: (e: Error) => void, 463 | body?: Body, 464 | headers?: Headers 465 | ): Promise; 466 | function postSub( 467 | url: string, 468 | body: Body | null, 469 | callback: (b: Response["body"]) => void, 470 | onError?: (e: Error) => void 471 | ): Promise; 472 | function request( 473 | method: Method, 474 | url: string, 475 | body?: Body, 476 | headers?: Headers 477 | ): Promise; 478 | function resolve( 479 | method: Method, 480 | url: string, 481 | body?: Body, 482 | headers?: Headers 483 | ): Promise; 484 | } 485 | /** 486 | * Fetch interesting colors from URI. 487 | * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) 488 | */ 489 | function colorExtractor(uri: string): Promise<{ 490 | DESATURATED: string; 491 | LIGHT_VIBRANT: string; 492 | PROMINENT: string; 493 | VIBRANT: string; 494 | VIBRANT_NON_ALARMING: string; 495 | }>; 496 | /** 497 | * @deprecated 498 | */ 499 | function getAblumArtColors(): any; 500 | /** 501 | * Fetch track analyzed audio data. 502 | * Beware, not all tracks have audio data. 503 | * @param uri is optional. Leave it blank to get current track 504 | * or specify another track uri. 505 | */ 506 | function getAudioData(uri?: string): Promise; 507 | /** 508 | * Set of APIs method to register, deregister hotkeys/shortcuts 509 | */ 510 | namespace Keyboard { 511 | type ValidKey = 512 | | "BACKSPACE" 513 | | "TAB" 514 | | "ENTER" 515 | | "SHIFT" 516 | | "CTRL" 517 | | "ALT" 518 | | "CAPS" 519 | | "ESCAPE" 520 | | "SPACE" 521 | | "PAGE_UP" 522 | | "PAGE_DOWN" 523 | | "END" 524 | | "HOME" 525 | | "ARROW_LEFT" 526 | | "ARROW_UP" 527 | | "ARROW_RIGHT" 528 | | "ARROW_DOWN" 529 | | "INSERT" 530 | | "DELETE" 531 | | "A" 532 | | "B" 533 | | "C" 534 | | "D" 535 | | "E" 536 | | "F" 537 | | "G" 538 | | "H" 539 | | "I" 540 | | "J" 541 | | "K" 542 | | "L" 543 | | "M" 544 | | "N" 545 | | "O" 546 | | "P" 547 | | "Q" 548 | | "R" 549 | | "S" 550 | | "T" 551 | | "U" 552 | | "V" 553 | | "W" 554 | | "X" 555 | | "Y" 556 | | "Z" 557 | | "WINDOW_LEFT" 558 | | "WINDOW_RIGHT" 559 | | "SELECT" 560 | | "NUMPAD_0" 561 | | "NUMPAD_1" 562 | | "NUMPAD_2" 563 | | "NUMPAD_3" 564 | | "NUMPAD_4" 565 | | "NUMPAD_5" 566 | | "NUMPAD_6" 567 | | "NUMPAD_7" 568 | | "NUMPAD_8" 569 | | "NUMPAD_9" 570 | | "MULTIPLY" 571 | | "ADD" 572 | | "SUBTRACT" 573 | | "DECIMAL_POINT" 574 | | "DIVIDE" 575 | | "F1" 576 | | "F2" 577 | | "F3" 578 | | "F4" 579 | | "F5" 580 | | "F6" 581 | | "F7" 582 | | "F8" 583 | | "F9" 584 | | "F10" 585 | | "F11" 586 | | "F12" 587 | | ";" 588 | | "=" 589 | | " | " 590 | | "-" 591 | | "." 592 | | "/" 593 | | "`" 594 | | "[" 595 | | "\\" 596 | | "]" 597 | | '"' 598 | | "~" 599 | | "!" 600 | | "@" 601 | | "#" 602 | | "$" 603 | | "%" 604 | | "^" 605 | | "&" 606 | | "*" 607 | | "(" 608 | | ")" 609 | | "_" 610 | | "+" 611 | | ":" 612 | | "<" 613 | | ">" 614 | | "?" 615 | | "|"; 616 | type KeysDefine = 617 | | string 618 | | { 619 | key: string; 620 | ctrl?: boolean; 621 | shift?: boolean; 622 | alt?: boolean; 623 | meta?: boolean; 624 | }; 625 | const KEYS: Record; 626 | function registerShortcut( 627 | keys: KeysDefine, 628 | callback: (event: KeyboardEvent) => void 629 | ): void; 630 | function registerIsolatedShortcut( 631 | keys: KeysDefine, 632 | callback: (event: KeyboardEvent) => void 633 | ): void; 634 | function registerImportantShortcut( 635 | keys: KeysDefine, 636 | callback: (event: KeyboardEvent) => void 637 | ): void; 638 | function _deregisterShortcut(keys: KeysDefine): void; 639 | function deregisterImportantShortcut(keys: KeysDefine): void; 640 | function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; 641 | } 642 | 643 | /** 644 | * @deprecated 645 | */ 646 | const LiveAPI: any; 647 | 648 | namespace LocalStorage { 649 | /** 650 | * Empties the list associated with the object of all key/value pairs, if there are any. 651 | */ 652 | function clear(): void; 653 | /** 654 | * Get key value 655 | */ 656 | function get(key: string): string | null; 657 | /** 658 | * Delete key 659 | */ 660 | function remove(key: string): void; 661 | /** 662 | * Set new value for key 663 | */ 664 | function set(key: string, value: string): void; 665 | } 666 | /** 667 | * To create and prepend custom menu item in profile menu. 668 | */ 669 | namespace Menu { 670 | /** 671 | * Create a single toggle. 672 | */ 673 | class Item { 674 | constructor( 675 | name: string, 676 | isEnabled: boolean, 677 | onClick: (self: Item) => void, 678 | icon?: Icon | string 679 | ); 680 | name: string; 681 | isEnabled: boolean; 682 | /** 683 | * Change item name 684 | */ 685 | setName(name: string): void; 686 | /** 687 | * Change item enabled state. 688 | * Visually, item would has a tick next to it if its state is enabled. 689 | */ 690 | setState(isEnabled: boolean): void; 691 | /** 692 | * Change icon 693 | */ 694 | setIcon(icon: Icon | string): void; 695 | /** 696 | * Item is only available in Profile menu when method "register" is called. 697 | */ 698 | register(): void; 699 | /** 700 | * Stop item to be prepended into Profile menu. 701 | */ 702 | deregister(): void; 703 | } 704 | 705 | /** 706 | * Create a sub menu to contain Item toggles. 707 | * `Item`s in `subItems` array shouldn't be registered. 708 | */ 709 | class SubMenu { 710 | constructor(name: string, subItems: Item[]); 711 | name: string; 712 | /** 713 | * Change SubMenu name 714 | */ 715 | setName(name: string): void; 716 | /** 717 | * Add an item to sub items list 718 | */ 719 | addItem(item: Item); 720 | /** 721 | * Remove an item from sub items list 722 | */ 723 | removeItem(item: Item); 724 | /** 725 | * SubMenu is only available in Profile menu when method "register" is called. 726 | */ 727 | register(): void; 728 | /** 729 | * Stop SubMenu to be prepended into Profile menu. 730 | */ 731 | deregister(): void; 732 | } 733 | } 734 | 735 | /** 736 | * Keyboard shortcut library 737 | * 738 | * Documentation: https://craig.is/killing/mice v1.6.5 739 | * 740 | * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, 741 | * so new extension should use this library instead. 742 | */ 743 | function Mousetrap(element?: any): void; 744 | 745 | /** 746 | * Contains vast array of internal APIs. 747 | * Please explore in Devtool Console. 748 | */ 749 | const Platform: any; 750 | /** 751 | * Queue object contains list of queuing tracks, 752 | * history of played tracks and current track metadata. 753 | */ 754 | const Queue: { 755 | nextTracks: any[]; 756 | prevTracks: any[]; 757 | queueRevision: string; 758 | track: any; 759 | }; 760 | /** 761 | * Remove a track or array of tracks from current queue. 762 | */ 763 | function removeFromQueue(uri: ContextTrack[]): Promise; 764 | /** 765 | * Display a bubble of notification. Useful for a visual feedback. 766 | * @param message Message to display. Can use inline HTML for styling. 767 | * @param isError If true, bubble will be red. Defaults to false. 768 | * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. 769 | */ 770 | function showNotification( 771 | text: string, 772 | isError?: boolean, 773 | msTimeout?: number 774 | ): void; 775 | /** 776 | * Set of APIs method to parse and validate URIs. 777 | */ 778 | class URI { 779 | constructor(type: string, props: any); 780 | public type: string; 781 | public hasBase62Id: boolean; 782 | 783 | public id?: string; 784 | public disc?: any; 785 | public args?: any; 786 | public category?: string; 787 | public username?: string; 788 | public artist?: string; 789 | public album?: string; 790 | public query?: string; 791 | public country?: string; 792 | public global?: boolean; 793 | public context?: string | typeof URI | null; 794 | public anchor?: string; 795 | public play?: any; 796 | public toplist?: any; 797 | 798 | /** 799 | * 800 | * @return The URI representation of this uri. 801 | */ 802 | toURI(): string; 803 | 804 | /** 805 | * 806 | * @return The URI representation of this uri. 807 | */ 808 | toString(): string; 809 | 810 | /** 811 | * Get the URL path of this uri. 812 | * 813 | * @param opt_leadingSlash True if a leading slash should be prepended. 814 | * @return The path of this uri. 815 | */ 816 | toURLPath(opt_leadingSlash: boolean): string; 817 | 818 | /** 819 | * 820 | * @param origin The origin to use for the URL. 821 | * @return The URL string for the uri. 822 | */ 823 | toURL(origin?: string): string; 824 | 825 | /** 826 | * Clones a given SpotifyURI instance. 827 | * 828 | * @return An instance of URI. 829 | */ 830 | clone(): URI | null; 831 | 832 | /** 833 | * Gets the path of the URI object by removing all hash and query parameters. 834 | * 835 | * @return The path of the URI object. 836 | */ 837 | getPath(): string; 838 | 839 | /** 840 | * The various URI Types. 841 | * 842 | * Note that some of the types in this enum are not real URI types, but are 843 | * actually URI particles. They are marked so. 844 | * 845 | */ 846 | static Type: { 847 | AD: string; 848 | ALBUM: string; 849 | GENRE: string; 850 | QUEUE: string; 851 | APPLICATION: string; 852 | ARTIST: string; 853 | ARTIST_TOPLIST: string; 854 | ARTIST_CONCERTS: string; 855 | AUDIO_FILE: string; 856 | COLLECTION: string; 857 | COLLECTION_ALBUM: string; 858 | COLLECTION_ARTIST: string; 859 | COLLECTION_MISSING_ALBUM: string; 860 | COLLECTION_TRACK_LIST: string; 861 | CONCERT: string; 862 | CONTEXT_GROUP: string; 863 | DAILY_MIX: string; 864 | EMPTY: string; 865 | EPISODE: string; 866 | /** URI particle; not an actual URI. */ 867 | FACEBOOK: string; 868 | FOLDER: string; 869 | FOLLOWERS: string; 870 | FOLLOWING: string; 871 | IMAGE: string; 872 | INBOX: string; 873 | INTERRUPTION: string; 874 | LIBRARY: string; 875 | LIVE: string; 876 | ROOM: string; 877 | EXPRESSION: string; 878 | LOCAL: string; 879 | LOCAL_TRACK: string; 880 | LOCAL_ALBUM: string; 881 | LOCAL_ARTIST: string; 882 | MERCH: string; 883 | MOSAIC: string; 884 | PLAYLIST: string; 885 | PLAYLIST_V2: string; 886 | PRERELEASE: string; 887 | PROFILE: string; 888 | PUBLISHED_ROOTLIST: string; 889 | RADIO: string; 890 | ROOTLIST: string; 891 | SEARCH: string; 892 | SHOW: string; 893 | SOCIAL_SESSION: string; 894 | SPECIAL: string; 895 | STARRED: string; 896 | STATION: string; 897 | TEMP_PLAYLIST: string; 898 | TOPLIST: string; 899 | TRACK: string; 900 | TRACKSET: string; 901 | USER_TOPLIST: string; 902 | USER_TOP_TRACKS: string; 903 | UNKNOWN: string; 904 | MEDIA: string; 905 | QUESTION: string; 906 | POLL: string; 907 | }; 908 | 909 | /** 910 | * Creates a new URI object from a parsed string argument. 911 | * 912 | * @param str The string that will be parsed into a URI object. 913 | * @throws TypeError If the string argument is not a valid URI, a TypeError will 914 | * be thrown. 915 | * @return The parsed URI object. 916 | */ 917 | static fromString(str: string): URI; 918 | 919 | /** 920 | * Parses a given object into a URI instance. 921 | * 922 | * Unlike URI.fromString, this function could receive any kind of value. If 923 | * the value is already a URI instance, it is simply returned. 924 | * Otherwise the value will be stringified before parsing. 925 | * 926 | * This function also does not throw an error like URI.fromString, but 927 | * instead simply returns null if it can't parse the value. 928 | * 929 | * @param value The value to parse. 930 | * @return The corresponding URI instance, or null if the 931 | * passed value is not a valid value. 932 | */ 933 | static from(value: any): URI | null; 934 | 935 | static isAd(uri: any): boolean; 936 | static isAlbum(uri: any): boolean; 937 | static isGenre(uri: any): boolean; 938 | static isQueue(uri: any): boolean; 939 | static isApplication(uri: any): boolean; 940 | static isArtist(uri: any): boolean; 941 | static isArtistToplist(uri: any): boolean; 942 | static isArtistConcerts(uri: any): boolean; 943 | static isAudioFile(uri: any): boolean; 944 | static isCollection(uri: any): boolean; 945 | static isCollectionAlbum(uri: any): boolean; 946 | static isCollectionArtist(uri: any): boolean; 947 | static isCollectionMissingAlbum(uri: any): boolean; 948 | static isCollectionTrackList(uri: any): boolean; 949 | static isConcert(uri: any): boolean; 950 | static isContextGroup(uri: any): boolean; 951 | static isDailyMix(uri: any): boolean; 952 | static isEmpty(uri: any): boolean; 953 | static isEpisode(uri: any): boolean; 954 | static isFacebook(uri: any): boolean; 955 | static isFolder(uri: any): boolean; 956 | static isFollowers(uri: any): boolean; 957 | static isFollowing(uri: any): boolean; 958 | static isImage(uri: any): boolean; 959 | static isInbox(uri: any): boolean; 960 | static isInterruption(uri: any): boolean; 961 | static isLibrary(uri: any): boolean; 962 | static isLive(uri: any): boolean; 963 | static isRoom(uri: any): boolean; 964 | static isExpression(uri: any): boolean; 965 | static isLocal(uri: any): boolean; 966 | static isLocalTrack(uri: any): boolean; 967 | static isLocalAlbum(uri: any): boolean; 968 | static isLocalArtist(uri: any): boolean; 969 | static isMerch(uri: any): boolean; 970 | static isMosaic(uri: any): boolean; 971 | static isPlaylist(uri: any): boolean; 972 | static isPlaylistV2(uri: any): boolean; 973 | static isPrerelease(uri: any): boolean; 974 | static isProfile(uri: any): boolean; 975 | static isPublishedRootlist(uri: any): boolean; 976 | static isRadio(uri: any): boolean; 977 | static isRootlist(uri: any): boolean; 978 | static isSearch(uri: any): boolean; 979 | static isShow(uri: any): boolean; 980 | static isSocialSession(uri: any): boolean; 981 | static isSpecial(uri: any): boolean; 982 | static isStarred(uri: any): boolean; 983 | static isStation(uri: any): boolean; 984 | static isTempPlaylist(uri: any): boolean; 985 | static isToplist(uri: any): boolean; 986 | static isTrack(uri: any): boolean; 987 | static isTrackset(uri: any): boolean; 988 | static isUserToplist(uri: any): boolean; 989 | static isUserTopTracks(uri: any): boolean; 990 | static isUnknown(uri: any): boolean; 991 | static isMedia(uri: any): boolean; 992 | static isQuestion(uri: any): boolean; 993 | static isPoll(uri: any): boolean; 994 | static isPlaylistV1OrV2(uri: any): boolean; 995 | } 996 | 997 | /** 998 | * Create custom menu item and prepend to right click context menu 999 | */ 1000 | namespace ContextMenu { 1001 | type OnClickCallback = ( 1002 | uris: string[], 1003 | uids?: string[], 1004 | contextUri?: string 1005 | ) => void; 1006 | type ShouldAddCallback = ( 1007 | uris: string[], 1008 | uids?: string[], 1009 | contextUri?: string 1010 | ) => boolean; 1011 | 1012 | // Single context menu item 1013 | class Item { 1014 | /** 1015 | * List of valid icons to use. 1016 | */ 1017 | static readonly iconList: Icon[]; 1018 | constructor( 1019 | name: string, 1020 | onClick: OnClickCallback, 1021 | shouldAdd?: ShouldAddCallback, 1022 | icon?: Icon, 1023 | disabled?: boolean 1024 | ); 1025 | name: string; 1026 | icon: Icon | string; 1027 | disabled: boolean; 1028 | /** 1029 | * A function returning boolean determines whether item should be prepended. 1030 | */ 1031 | shouldAdd: ShouldAddCallback; 1032 | /** 1033 | * A function to call when item is clicked 1034 | */ 1035 | onClick: OnClickCallback; 1036 | /** 1037 | * Item is only available in Context Menu when method "register" is called. 1038 | */ 1039 | register: () => void; 1040 | /** 1041 | * Stop Item to be prepended into Context Menu. 1042 | */ 1043 | deregister: () => void; 1044 | } 1045 | 1046 | /** 1047 | * Create a sub menu to contain `Item`s. 1048 | * `Item`s in `subItems` array shouldn't be registered. 1049 | */ 1050 | class SubMenu { 1051 | constructor( 1052 | name: string, 1053 | subItems: Iterable, 1054 | shouldAdd?: ShouldAddCallback, 1055 | disabled?: boolean 1056 | ); 1057 | name: string; 1058 | disabled: boolean; 1059 | /** 1060 | * A function returning boolean determines whether item should be prepended. 1061 | */ 1062 | shouldAdd: ShouldAddCallback; 1063 | addItem: (item: Item) => void; 1064 | removeItem: (item: Item) => void; 1065 | /** 1066 | * SubMenu is only available in Context Menu when method "register" is called. 1067 | */ 1068 | register: () => void; 1069 | /** 1070 | * Stop SubMenu to be prepended into Context Menu. 1071 | */ 1072 | deregister: () => void; 1073 | } 1074 | } 1075 | 1076 | /** 1077 | * Popup Modal 1078 | */ 1079 | namespace PopupModal { 1080 | interface Content { 1081 | title: string; 1082 | /** 1083 | * You can specify a string for simple text display 1084 | * or a HTML element for interactive config/setting menu 1085 | */ 1086 | content: string | Element; 1087 | /** 1088 | * Bigger window 1089 | */ 1090 | isLarge?: boolean; 1091 | } 1092 | 1093 | function display(e: Content): void; 1094 | function hide(): void; 1095 | } 1096 | 1097 | /** React instance to create components */ 1098 | const React: any; 1099 | /** React DOM instance to render and mount components */ 1100 | const ReactDOM: any; 1101 | 1102 | /** Stock React components exposed from Spotify library */ 1103 | namespace ReactComponent { 1104 | type ContextMenuProps = { 1105 | /** 1106 | * Decide whether to use the global singleton context menu (rendered in ) 1107 | * or a new inline context menu (rendered in a sibling 1108 | * element to `children`) 1109 | */ 1110 | renderInline?: boolean; 1111 | /** 1112 | * Determins what will trigger the context menu. For example, a click, or a right-click 1113 | */ 1114 | trigger?: "click" | "right-click"; 1115 | /** 1116 | * Determins is the context menu should open or toggle when triggered 1117 | */ 1118 | action?: "toggle" | "open"; 1119 | /** 1120 | * The preferred placement of the context menu when it opens. 1121 | * Relative to trigger element. 1122 | */ 1123 | placement?: 1124 | | "top" 1125 | | "top-start" 1126 | | "top-end" 1127 | | "right" 1128 | | "right-start" 1129 | | "right-end" 1130 | | "bottom" 1131 | | "bottom-start" 1132 | | "bottom-end" 1133 | | "left" 1134 | | "left-start" 1135 | | "left-end"; 1136 | /** 1137 | * The x and y offset distances at which the context menu should open. 1138 | * Relative to trigger element and `position`. 1139 | */ 1140 | offset?: [number, number]; 1141 | /** 1142 | * Will stop the client from scrolling while the context menu is open 1143 | */ 1144 | preventScrollingWhileOpen?: boolean; 1145 | /** 1146 | * The menu UI to render inside of the context menu. 1147 | */ 1148 | menu: 1149 | | typeof Spicetify.ReactComponent.Menu 1150 | | typeof Spicetify.ReactComponent.AlbumMenu 1151 | | typeof Spicetify.ReactComponent.PodcastShowMenu 1152 | | typeof Spicetify.ReactComponent.ArtistMenu 1153 | | typeof Spicetify.ReactComponent.PlaylistMenu; 1154 | /** 1155 | * A child of the context menu. Should be `