├── 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 | 
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SleepTimer/README.md:
--------------------------------------------------------------------------------
1 | # Sleep Timer
2 | Ported from mobile, automatically pause music after a certain number of minutes or songs.
3 |
4 | 
5 | 
6 |
7 |
8 |
9 |
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 | 
5 |
6 | Old UI Screenshots:
7 |
8 | 
9 |
10 | 
11 |
12 | 
13 |
14 |
15 |
16 |
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 |
--------------------------------------------------------------------------------