├── QueueTime ├── QueueTime.png ├── README.md └── QueueTime.js ├── SleepTimer ├── SleepTimer1.png ├── SleepTimer2.png └── README.md ├── CoverAmbience ├── CoverAmbience1.png ├── CoverAmbience2.png ├── CoverAmbience3.png ├── CoverAmbience4.png ├── README.md └── CoverAmbience.js ├── README.md ├── manifest.json └── LICENSE /QueueTime/QueueTime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/QueueTime/QueueTime.png -------------------------------------------------------------------------------- /SleepTimer/SleepTimer1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/SleepTimer/SleepTimer1.png -------------------------------------------------------------------------------- /SleepTimer/SleepTimer2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/SleepTimer/SleepTimer2.png -------------------------------------------------------------------------------- /CoverAmbience/CoverAmbience1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/CoverAmbience/CoverAmbience1.png -------------------------------------------------------------------------------- /CoverAmbience/CoverAmbience2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/CoverAmbience/CoverAmbience2.png -------------------------------------------------------------------------------- /CoverAmbience/CoverAmbience3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/CoverAmbience/CoverAmbience3.png -------------------------------------------------------------------------------- /CoverAmbience/CoverAmbience4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Theblockbuster1/spicetify-extensions/HEAD/CoverAmbience/CoverAmbience4.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spicetify Extensions 2 | Extensions made for [Spicetify](https://github.com/spicetify/spicetify-cli), installable via [Spicetify Marketplace](https://github.com/spicetify/spicetify-marketplace). 3 | - [Queue Time](QueueTime) - Display time remaining in the current queue. 4 | - [Sleep Timer](SleepTimer) - Ported from mobile, automatically pause music after a certain number of minutes or songs. 5 | - [Cover Ambience](CoverAmbience) - Produces a lovely aesthetic glow from the currently playing album cover. -------------------------------------------------------------------------------- /QueueTime/README.md: -------------------------------------------------------------------------------- 1 | # Queue Time 2 | Simply displays the time remaining in the current queue. 3 | 4 | ![!Queue Time Screenshot](/QueueTime/QueueTime.png) 5 | 6 |

7 | 8 | stars - spicetify-extensions 9 | -------------------------------------------------------------------------------- /SleepTimer/README.md: -------------------------------------------------------------------------------- 1 | # Sleep Timer 2 | Ported from mobile, automatically pause music after a certain number of minutes or songs. 3 | 4 | ![!Sleep Timer Disabled Screenshot](/SleepTimer/SleepTimer1.png) 5 | ![!Sleep Timer Enabled Screenshot](/SleepTimer/SleepTimer2.png) 6 | 7 |

8 | 9 | stars - spicetify-extensions 10 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Queue Time", 4 | "description": "Display time remaining in the current queue.", 5 | "preview": "QueueTime/QueueTime.png", 6 | "main": "QueueTime/QueueTime.js", 7 | "readme": "QueueTime/README.md" 8 | }, 9 | { 10 | "name": "Sleep Timer", 11 | "description": "Automatically pause music after a certain number of minutes or songs.", 12 | "preview": "SleepTimer/SleepTimer1.png", 13 | "main": "SleepTimer/SleepTimer.js", 14 | "readme": "SleepTimer/README.md" 15 | }, 16 | { 17 | "name": "Cover Ambience", 18 | "description": "Produces a lovely aesthetic glow from the currently playing album cover.", 19 | "preview": "CoverAmbience/CoverAmbience4.png", 20 | "main": "CoverAmbience/CoverAmbience.js", 21 | "readme": "CoverAmbience/README.md" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /CoverAmbience/README.md: -------------------------------------------------------------------------------- 1 | # Cover Ambience 2 | Produces a lovely aesthetic glow from the currently playing song cover. 3 | 4 | ![!New UI Queue Time Screenshot](/CoverAmbience/CoverAmbience4.png) 5 | 6 | Old UI Screenshots: 7 | 8 | ![!Green Queue Time Screenshot](/CoverAmbience/CoverAmbience1.png) 9 | 10 | ![!Red Queue Time Screenshot](/CoverAmbience/CoverAmbience2.png) 11 | 12 | ![!Purple Queue Time Screenshot](/CoverAmbience/CoverAmbience3.png) 13 | 14 |

15 | 16 | stars - spicetify-extensions 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Theblockbuster1 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 | -------------------------------------------------------------------------------- /QueueTime/QueueTime.js: -------------------------------------------------------------------------------- 1 | let qt_style = document.createElement( "style" ); 2 | qt_style.innerHTML = ` 3 | .queue-queuePage-header, 4 | #queue-panel .NWVZ_rxlezZ8xTHlMg4Y:first-child .LFdMliaHVgrpBcqNKHU3, 5 | .vLZJk3f3zoMmc3u9QMrc .LIaQPESoX4ijscRRn3lz:first-of-type, 6 | #queue-panel .KHNumev0cQFGYG2rSV1p:first-child .fYX4XCQz81A_L1WZ88uc { 7 | position: relative; 8 | } 9 | .queue-queuePage-header::after, 10 | #queue-panel .NWVZ_rxlezZ8xTHlMg4Y:first-child .LFdMliaHVgrpBcqNKHU3::after, 11 | .vLZJk3f3zoMmc3u9QMrc .LIaQPESoX4ijscRRn3lz:first-of-type::after, 12 | #queue-panel .KHNumev0cQFGYG2rSV1p:first-child .fYX4XCQz81A_L1WZ88uc::after { 13 | content: var(--queue-remaining); 14 | color: var(--spice-subtext); 15 | font-size: 1rem; 16 | position: absolute; 17 | bottom: 0; 18 | right: 0; 19 | font-weight: initial; 20 | } 21 | /* for queue panel only: */ 22 | .queue-panel .queue-queuePage-header::after, 23 | #queue-panel .NWVZ_rxlezZ8xTHlMg4Y:first-child .LFdMliaHVgrpBcqNKHU3::after, 24 | #queue-panel .KHNumev0cQFGYG2rSV1p:first-child .fYX4XCQz81A_L1WZ88uc::after { 25 | top: 4.5px; 26 | } 27 | `; 28 | document.head.appendChild( qt_style ); 29 | 30 | let momentScript = document.createElement( "script" ); 31 | momentScript.setAttribute( 'src', 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js' ); 32 | momentScript.setAttribute( 'integrity', 'sha512-+H4iLjY3JsKiF2V6N366in5IQHj2uEsGV7Pp/GRcm0fn76aPAk5V8xB6n8fQhhSonTqTXs/klFz4D0GIn6Br9g==' ); 33 | momentScript.setAttribute( 'crossorigin', 'anonymous' ); 34 | momentScript.setAttribute( 'referrerpolicy', 'no-referrer' ); 35 | document.head.appendChild( momentScript ); 36 | 37 | setInterval( () => { 38 | const totalTime = Spicetify.Queue?.nextTracks.slice( 0 ).reduce( ( acc, cur, _, arr ) => { 39 | if ( isNaN( Number( cur.contextTrack.metadata.duration ) ) ) arr.splice(1); 40 | return acc + ( Number( cur.contextTrack.metadata.duration ) || 0 ) 41 | }, 0 ) || 0; 42 | document.querySelectorAll( 43 | `.queue-queuePage-header, 44 | #queue-panel .NWVZ_rxlezZ8xTHlMg4Y:first-child .LFdMliaHVgrpBcqNKHU3, 45 | .vLZJk3f3zoMmc3u9QMrc .LIaQPESoX4ijscRRn3lz:first-of-type, 46 | #queue-panel .KHNumev0cQFGYG2rSV1p:first-child .fYX4XCQz81A_L1WZ88uc` 47 | )?.forEach(e => e.style.setProperty( '--queue-remaining', `'${moment.utc( totalTime + Spicetify.Player.getDuration() - Spicetify.Player.getProgress() ).format( 'HH:mm:ss' )} Remaining'` ) ); 48 | }, 1000 ); 49 | -------------------------------------------------------------------------------- /CoverAmbience/CoverAmbience.js: -------------------------------------------------------------------------------- 1 | let ca_style = document.createElement('style'); 2 | ca_style.innerHTML = ` 3 | :root { 4 | --cover-ambience-background: var(--spice-player); 5 | } 6 | .Root__now-playing-bar.LibraryX { 7 | --cover-ambience-background: var(--spice-sidebar); 8 | } 9 | .LibraryX .main-nowPlayingBar-container, 10 | .LibraryX .main-nowPlayingBar-container:before, 11 | .LibraryX [data-testid="now-playing-bar"] > div, 12 | .LibraryX [data-testid="now-playing-bar"] > div:before { 13 | border-radius: 8px; 14 | } 15 | 16 | aside.main-nowPlayingBar-container, footer.main-nowPlayingBar-container, [data-testid="now-playing-bar"] > div { 17 | transition: background 0.5s ease; 18 | background-size: 100%; 19 | --bg-img: linear-gradient(to right, var(--cover-ambience-color) 0, var(--cover-ambience-background) 280px, var(--cover-ambience-background) 100%); 20 | --bg-img-before: linear-gradient(to right, var(--cover-ambience-color-before) 0, var(--cover-ambience-background) 280px, var(--cover-ambience-background) 100%); 21 | background-image: var(--bg-img) !important; 22 | position: relative; 23 | z-index: 100; 24 | --cover-ambience-border-opacity: ${localStorage.CoverAmbienceBorderOpacity || 0.5}; 25 | --cover-ambience-border-opacity-small: ${((localStorage.CoverAmbienceBorderOpacity || 0.5) * 80) / 100}; 26 | } 27 | aside.main-nowPlayingBar-container:before, footer.main-nowPlayingBar-container:before, [data-testid="now-playing-bar"] > div:before { 28 | background-image: var(--bg-img-before); 29 | content: ""; 30 | display: block; 31 | height: 100%; 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | opacity: 0; 36 | width: 100%; 37 | z-index: -100; 38 | transition: opacity 0.6s; 39 | opacity: var(--cover-ambience-opacity); 40 | } 41 | 42 | /* Add outlines to song text/artist/genre */ 43 | .Root__now-playing-bar .main-trackInfo-name 44 | { 45 | text-shadow: -1px -1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity)), 1px -1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity)), -1px 1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity)), 1px 1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity)); 46 | } 47 | .Root__now-playing-bar .main-trackInfo-artists, .Root__now-playing-bar .main-trackInfo-genres 48 | { 49 | text-shadow: -1px -1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity-small)), 1px -1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity-small)), -1px 1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity-small)), 1px 1px 0 rgba(var(--spice-rgb-player), var(--cover-ambience-border-opacity-small)); 50 | } 51 | `; 52 | document.head.appendChild(ca_style); 53 | 54 | const hexToRGB = hex => 55 | hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => '#' + r + r + g + g + b + b) 56 | .substring(1).match(/.{2}/g) 57 | .map(x => parseInt(x, 16)) 58 | 59 | function RGBToHSL(rgb) { 60 | if (rgb.length) { 61 | // make r, g, and b fractions of 1 62 | let r = rgb[0] / 255, 63 | g = rgb[1] / 255, 64 | b = rgb[2] / 255, 65 | 66 | // find greatest and smallest channel values 67 | cmin = Math.min(r,g,b), 68 | cmax = Math.max(r,g,b), 69 | delta = cmax - cmin, 70 | h = 0, 71 | s = 0, 72 | l = 0; 73 | 74 | // calculate hue 75 | // no difference 76 | if (delta == 0) 77 | h = 0; 78 | // red is max 79 | else if (cmax == r) 80 | h = ((g - b) / delta) % 6; 81 | // green is max 82 | else if (cmax == g) 83 | h = (b - r) / delta + 2; 84 | // blue is max 85 | else 86 | h = (r - g) / delta + 4; 87 | 88 | h = Math.round(h * 60); 89 | 90 | // make negative hues positive behind 360° 91 | if (h < 0) 92 | h += 360; 93 | 94 | // calculate lightness 95 | l = (cmax + cmin) / 2; 96 | 97 | // calculate saturation 98 | s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 99 | 100 | // multiply l and s by 100 101 | s = +(s * 100).toFixed(1); 102 | l = +(l * 100).toFixed(1); 103 | 104 | // normalize color 105 | s = s > 50 ? 50 : s; 106 | l = l > 35 ? 35 : l; 107 | l = l < 8 ? 8 : l; 108 | 109 | return "hsl(" + h + "," + s + "%," + l + "%)"; 110 | 111 | } else { 112 | return "hsl(0, 0%, 50%)"; 113 | } 114 | } 115 | 116 | async function fetchExtractedColors() { 117 | const res = await fetch(`https://api-partner.spotify.com/pathfinder/v1/query?operationName=fetchExtractedColors&variables=${encodeURIComponent(JSON.stringify({ uris: [Spicetify.Player.data.item.metadata.image_url] }))}&extensions=${encodeURIComponent(JSON.stringify({"persistedQuery":{"version":1,"sha256Hash":"d7696dd106f3c84a1f3ca37225a1de292e66a2d5aced37a66632585eeb3bbbfa"}}))}`, { 118 | method: "GET", 119 | headers: { 120 | authorization: `Bearer ${(await Spicetify.CosmosAsync.get('sp://oauth/v2/token')).accessToken}` 121 | } 122 | }) 123 | .then(res => res.json()); 124 | if (!res.data.extractedColors) return [128, 128, 128]; 125 | return hexToRGB(res.data.extractedColors[0].colorRaw.hex); 126 | } 127 | 128 | LibraryX = false; // 'false' because class is not on by default 129 | async function checkBackgroundColor() { 130 | let LibraryXCheck = Spicetify.RemoteConfigResolver?.value.localConfiguration.values.get('enableYLXSidebar') || true; 131 | if (LibraryX != LibraryXCheck) { 132 | LibraryX = LibraryXCheck; 133 | let rootClasses = document.querySelector('.Root__now-playing-bar')?.classList; 134 | if (LibraryXCheck) rootClasses.add('LibraryX'); 135 | else rootClasses.remove('LibraryX'); 136 | } 137 | } 138 | 139 | var beforeElement = false; 140 | async function setGradient() { 141 | checkBackgroundColor(); 142 | let style = document.querySelector('.main-nowPlayingBar-container, [data-testid="now-playing-bar"] > div')?.style; 143 | let rgb = (await fetchExtractedColors() || [128, 128, 128]); 144 | let color = RGBToHSL(rgb); 145 | if (beforeElement) { 146 | style.setProperty('--cover-ambience-color', color); 147 | style.setProperty('--cover-ambience-opacity', 0); 148 | beforeElement = false; 149 | } else { 150 | style.setProperty('--cover-ambience-color-before', color); 151 | style.setProperty('--cover-ambience-opacity', 1); 152 | beforeElement = true; 153 | } 154 | } 155 | 156 | function initiate() { 157 | setGradient(); 158 | Spicetify.Player.addEventListener('songchange', setGradient); 159 | setInterval(checkBackgroundColor, 5000); 160 | 161 | window.updateCABorderOpacity = function(value) { 162 | let style = document.querySelector('.main-nowPlayingBar-container, [data-testid="now-playing-bar"] > div')?.style; 163 | style.setProperty('--cover-ambience-border-opacity', value / 100); 164 | style.setProperty('--cover-ambience-border-opacity-small', ((value / 100) * 80) / 100); 165 | 166 | localStorage.CoverAmbienceBorderOpacity = value / 100; 167 | } 168 | 169 | const info = document.querySelector("div.main-nowPlayingWidget-trackInfo.main-trackInfo-container"); 170 | 171 | var contextMenu = 0; 172 | 173 | info.addEventListener('mousedown', e => { // check if the context menu was clicked within the track info container 174 | if (e.button != 2) return; // right clicks only 175 | contextMenu = Date.now(); 176 | }) 177 | 178 | let borderOpacityMenuItem = new Spicetify.ContextMenu.Item( 179 | "Edit Border Opacity", 180 | () => { 181 | Spicetify.PopupModal.display({ 182 | title: 'Border Opacity %', 183 | content: ` 224 | 225 | `, 226 | isLarge: true 227 | }); 228 | }, 229 | () => { 230 | return Date.now() - contextMenu <= 500; 231 | }, 232 | 'edit', 233 | false, 234 | ); 235 | 236 | borderOpacityMenuItem.register(); 237 | } 238 | 239 | if (Spicetify.Player.data) { 240 | initiate(); 241 | } else { 242 | const observer = new MutationObserver((_, observer) => { 243 | if (Spicetify.Player.data) { 244 | observer.disconnect(); 245 | initiate(); 246 | } 247 | }); 248 | observer.observe(document.body, { 249 | childList: true, 250 | subtree: true 251 | }); 252 | } 253 | --------------------------------------------------------------------------------