Repository: huhridge/huh-spicetify-extensions
Branch: main
Commit: 35f13148eeab
Files: 17
Total size: 147.4 KB
Directory structure:
gitextract_6oyyqymh/
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug-report.md
│ └── feature-request.md
├── .gitignore
├── README.md
├── fullAlbumDate/
│ ├── README.md
│ └── fullAlbumDate.js
├── fullAppDisplayModified/
│ ├── README.md
│ └── fullAppDisplayMod.js
├── goToSong/
│ ├── README.md
│ └── goToSong.js
├── listPlaylistsWithSong/
│ ├── README.md
│ └── listPlaylistsWithSong.js
├── manifest.json
├── playlistIntersection/
│ ├── README.md
│ └── playlistIntersection.js
└── skipStats/
├── README.md
└── skipStats.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug Report
about: Create a report to help me resolve issues
title: "[Extension Name Here][BUG]"
labels: bug
assignees: huhridge
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Console Errors**
To check the console, run `spicetify enable-devtools` in the terminal, and press Ctrl/Cmd+Shift+i and check the console.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS:
- Spotify Version:
- Spicetify Version:
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature Request
about: Suggest an idea for this project
title: "[REQ]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .gitignore
================================================
.DS_Store
.DS_Store
.DS_Store
================================================
FILE: README.md
================================================
# huh-spicetify-extensions
Collection of my spicetify extensions
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
## Full App Display modified
Filename : `fullAppDisplayMod.js`
Minimal album cover art display with beautiful blur effect background. Activating button locates in top bar. While in display mode, double click anywhere to exit. Right click anywhere to open setting menu. Now also includes lyrics if `lyrics-plus` custom app installed.

Screenshots
* Album Art
* Colorful Background(doesn't work offline)
For more information: [fullAppDisplayMod README](/fullAppDisplayModified/README.md)
## skipStats
Filename : `skipStats.js`

Extension to track your skips!
- Tracks your skips when listening to playlists or albums!
- Displays the data in a readable manner
- Auto-skip songs over a certain value of skips
For more information: [skipStats README](/skipStats/README.md)
## List Playlists with Song
Filename : `listPlaylistsWithSong.js`
Adds context menu button to view playlists in your library that contain the selected song.

### To use:
Right Click on selected song, and click "List playlists with this Song".

#### Note:
~~Currently, doesn't work on the currently playing song(like in the bottom bar pictured below), finding a workaround.~~
Now it works, fixed by yours truly!
## Go to Song
Filename : `goToSong.js`
Go to the currrently playing song in a playlist **/or/** currently playing playlist.
### To use:
- Currently playing playlist: Go to Profile > GoToSong > Choose "Go To Song in current Playlist"
- 
- Any Playlist: Right Click on the Playlist, and choose "Go to Currently Playing Song"
- 
### Note:
You may need to adjust your delay if it's giving an error, follow the instructions in the popup.

## playlistIntersection
Filename : `playlistIntersection.js`
Adds context menu buttons to see
- songs in common between two playlists
- songs only present in one playlist

### To use:
Check the full readme: [playlistIntersection readme](/playlistIntersection/README.md)
## Display full Album date
Filename : `fullAlbumDate.js`
Display full album date instead of just year

================================================
FILE: fullAlbumDate/README.md
================================================
# Display full Album date
Filename : `fullAlbumDate.js`
Display full album date instead of just year

## More
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: fullAlbumDate/fullAlbumDate.js
================================================
(async function fullAlbumDate() {
if (!Spicetify.Platform?.History || !Spicetify.CosmosAsync || !Spicetify.Locale) {
setTimeout(fullAlbumDate, 300);
return;
}
const { CosmosAsync, Locale } = Spicetify;
const { History } = Spicetify.Platform;
async function getAlbumDate(uri) {
const albumInfo = await CosmosAsync.get(`https://api.spotify.com/v1/albums/${uri}`);
const albumDate = new Date(albumInfo.release_date);
return Locale.formatRelativeTime( albumDate );
}
function replaceDate(newDate) {
const dateElement =
document.querySelector(".main-entityHeader-divider.main-type-mesto") ??
document.querySelector(".main-entityHeader-metaData span:nth-last-child(2)");
if (!dateElement) {
setTimeout(replaceDate, 100, newDate);
return;
}
dateElement.textContent = newDate;
}
async function setDate() {
const { pathname } = History.location;
if (pathname.startsWith("/album/")) {
const uri = pathname.split("/")[2];
const newDate = await getAlbumDate(uri);
replaceDate(newDate);
}
}
setDate();
History.listen(setDate);
})();
================================================
FILE: fullAppDisplayModified/README.md
================================================
# Full App Display modified
Filename : `fullAppDisplayMod.js`
Minimal album cover art display with beautiful blur effect background. Activating button locates in top bar. While in display mode, double click anywhere to exit. Right click anywhere to open setting menu. Now also includes lyrics if `lyrics-plus` custom app installed.

Screenshots:
* Album Art
* Colorful Background(doesn't work offline)
Colorful background changes text color if the color is too light,
Settings:
(May look a bit different depending on your theme)
## More
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: fullAppDisplayModified/fullAppDisplayMod.js
================================================
// @ts-check
// NAME: Full App Display
// AUTHOR: khanhas
// VERSION: 1.2
// DESCRIPTION: Fancy artwork and track status display.
///
(function FullAppDisplay() {
if (!Spicetify.Keyboard || !Spicetify.React || !Spicetify.ReactDOM || !Spicetify.Platform) {
setTimeout(FullAppDisplay, 200);
return;
}
const { React: react, ReactDOM: reactDOM } = Spicetify;
const { useState, useEffect } = react;
const CONFIG = getConfig();
if (
!CONFIG["colorChoice"] ||
CONFIG["colorChoice"] == "colorDark" ||
CONFIG["colorChoice"] == "colorLight" ||
CONFIG["colorChoice"] == "colorRaw"
) {
CONFIG["colorChoice"] = "LIGHT_VIBRANT";
saveConfig();
}
if (!CONFIG["version"]) {
CONFIG["version"] = "1.2";
saveConfig();
}
let updateVisual;
let nextUri, prevColor, nextColor, finImage;
let isHidden = false;
let time;
const style = document.createElement("style");
const styleBase = `
#full-app-display {
display: none;
position: fixed;
width: 100%;
height: 100%;
cursor: default;
left: 0;
top: 0;
}
#fad-header {
position: fixed;
width: 100%;
height: 80px;
-webkit-app-region: drag;
}
#fad-body {
height: 100vh;
}
#fad-foreground {
position: relative;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transform: scale(var(--fad-scale));
transition: all 1s ease;
}
#fad-art-image {
position: relative;
width: 100%;
height: 100%;
padding-bottom: 100%;
border-radius: 15px;
background-size: cover;
}
#fad-art-inner {
position: absolute;
left: 3%;
bottom: 0;
width: 94%;
height: 94%;
z-index: -1;
backface-visibility: hidden;
transform: translateZ(0);
filter: blur(6px);
backdrop-filter: blur(6px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6) !important;
}
#fad-progress-container {
width: 100%;
display: flex;
align-items: center;
}
#fad-progress {
width: 100%;
height: 6px;
margin: 6px 0 6px;
border-radius: 6px;
background-color: #ffffff50;
overflow: hidden;
}
#fad-progress-inner {
height: 100%;
transition: width 100ms ease;
border-radius: 6px;
background-color: #ffffff;
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.8) !important;
}
#fad-volume {
width: 4rem;
height: 16rem;
position: fixed;
left: 1rem;
transition: opacity ease 350ms;
}
#fad-volume:hover {
opacity: 1 !important;
}
#fad-volicon {
height: fit-content;
display: grid;
justify-content: start;
}
#fad-volbar {
display: flex;
height: 200px;
width: 8px;
border-radius: 6px;
overflow: hidden;
background-color: #ffffff50;
justify-content: center;
padding-top: 10px;
align-items: end;
margin-left: 27.5px;
margin-top: 10px;
position: absolute;
}
#fad-volbar-inner {
width: 8px;
border-radius: 6px;
background-color: rgb(255, 255, 255);
transition: height 0.5s ease 0s;
}
#fad-duration {
margin-left: 10px;
}
#fad-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -2;
}
body.fad-activated #full-app-display {
display: block
}
.fad-background-fade {
transition: background-image 1s linear;
}
body.video-full-screen.video-full-screen--hide-ui {
cursor: auto;
}
#fad-controls button, #fad-extracontrols button {
background-color: transparent;
border: 0;
color: currentColor;
padding: 0 5px;
pointer-events: auto;
}
#fad-controls button:hover, #fad-extracontrols button:hover, #fad-volicon button:hover{
transform: scale(1.1);
}
#fad-artist svg, #fad-album svg {
display: inline-block;
}
#fad-upnext {
position: absolute;
width: 399px;
height: 90px;
display: flex;
border-radius: 10px;
animation: textchange 0.5s forwards;
transition: clip-path 0.4s;
}
#fad-upnext-image {
width: 64px;
align-self: center;
margin-left: 13px;
height: 64px;
background-size: cover;
background-position: center;
border-radius: 5px;
box-shadow: 0 4px 8px rgb(0 0 0 / 30%) !important;
z-index: 0;
}
#fad-upnext-blur {
position: absolute;
backdrop-filter: blur(15px) brightness(0.6);
background-color: rgb(255,255,255,0.1);
width: 100%;
height: 100%;
border-radius: 10px;
}
#fad-upnext-details{
margin-left: 15px;
align-self: center;
font-size: 14.5px;
display: flex;
flex-direction: column;
align-items: flex-start;
color: #ffffff;
overflow: hidden;
white-space: nowrap;
}
#scroll-queue{
position: absolute;
width: 399px;
height: 90px;
color: transparent;
z-index: 2;
}
.dont-scale{
transform: scale(calc(1/(var(--fad-scale))));
}
.dot-after:after{
background-color: currentColor;
border-radius: 50%;
bottom: 3px;
content: "";
display: block;
height: 4px;
left: 50%;
position: relative;
transform: translateX(-50%);
width: 4px;
}
.crossed-out:after{
background-color: currentColor;
bottom: 24px;
transform: rotate(45deg);
transform-origin: 0 0;
content: "";
display: block;
height: 27px;
left: 18px;
position: relative;
width: 0.5px;
}
::-webkit-scrollbar {
width: 8px;
}
.fad-grad-image{
position: absolute;
filter: blur(40px) brightness(0.60);
border-radius: 100em;
animation: rotategrad 50s linear infinite 1s;
}
@keyframes textchange {
0%{
opacity: 0;
}
30%{
opacity: 0.3;
}
60%{
opacity: 0.6;
}
90%{
opacity: 0.9;
}
}
@keyframes rotategrad {
0% {
transform: rotate(18deg);
}
100% {
transform: rotate(378deg);
}
}
`;
const styleChoices = [
`
#fad-foreground {
flex-direction: row;
text-align: left;
}
#fad-art {
width: calc(100vw - 840px);
min-width: 200px;
max-width: 340px;
}
#fad-details {
padding-left: 40px;
line-height: initial;
max-width: 70%;
color: #FFFFFF;
filter: invert(0);
}
#fad-title {
font-size: 87px;
font-weight: 900;
}
#fad-artist, #fad-album {
font-size: 54px;
font-weight: 400;
}
#fad-artist svg, #fad-album svg {
margin-right: 5px;
}
#fad-status {
display: flex;
min-width: 400px;
max-width: 400px;
align-items: center;
}
#fad-status.active {
margin-top: 20px;
}
#fad-controls {
display: flex;
margin-right: 10px;
z-index: 0;
}
#fad-extracontrols {
height: 28px;
display: flex;
}
#fad-elapsed {
min-width: 52px;
}`,
`
#fad-art {
width: calc(100vh - 400px);
max-width: 340px;
}
#fad-foreground {
flex-direction: column;
text-align: center;
}
#fad-details {
padding-top: 40px;
line-height: initial;
max-width: 70%;
color: #FFFFFF;
filter: invert(0);
}
#fad-title {
line-height: 1;
font-size: 54px;
font-weight: 900;
}
#fad-artist, #fad-album {
font-size: 33px;
font-weight: 400;
}
#fad-artist svg, #fad-album svg {
width: 25px;
height: 25px;
margin-right: 5px;
}
#fad-status {
display: flex;
min-width: 400px;
max-width: 400px;
align-items: center;
flex-direction: column;
}
#fad-status.active {
margin: 20px auto 0;
}
#fad-controls {
margin-top: 20px;
order: 2;
z-index: 0;
}
#fad-extracontrols {
order: 3;
width: 100%;
height: 28px;
display: flex;
}
#fad-elapsed {
min-width: 56px;
margin-right: 10px;
text-align: right;
}`,
];
const lyricsPlusBase = `
#fad-body {
display: grid;
grid-template-columns: 1fr 1fr;
}
#fad-foreground {
padding: 0 50px 0 100px;
width: 50vw;
}
#fad-lyrics-plus-container {
position: relative;
width: 50vw;
}
.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-config-button-container {
display: none;
}
`;
const lyricsPlusStyleChoices = [
`
#fad-title {
font-size: 4vw;
}
#fad-artist, #fad-album {
font-size: 2.5vw;
font-weight: 400;
}
#fad-art {
max-width: 210px;
margin-left: 50px;
}`,
`
#fad-title {
font-size: 3.9vw;
}
#fad-artist, #fad-album {
font-size: 2.5vw;
font-weight: 400;
}
`,
];
const verticalMonitorStyle = [
`
#fad-body {
grid-template-columns: none;
}
#fad-foreground, #fad-lyrics-plus-container {
width: 100%;
height: 50vh;
}
#fad-foreground {
padding: 0 50px 0;
}
.lyrics-lyricsContainer-LyricsContainer.fad-enabled {
height: 50vh;
--lyrics-align-text: center !important;
}
#fad-volume {
top: 15vh;
}
`,
];
updateStyle();
function displayUpdate() {
let updateText = react.createElement(
"p",
{
className: "fad-update",
},
`
This update brings:
`,
react.createElement("li", {}, "Added seekable progress bar: Now you can seek songs from FAD itself, click on it to seek!"),
react.createElement("li", {}, "Added show only on hover mode for volume bar (change in settings)"),
react.createElement("li", {}, "Bug Fixes: Reworked the upnext and queue function, to account for the scale.")
);
Spicetify.PopupModal.display({
title: "What's New with FullAppDisplayMod",
content: updateText,
isLarge: true,
});
}
if (CONFIG["version"] == "1.1") {
displayUpdate();
CONFIG["version"] = "1.2";
saveConfig();
}
async function fetchColors(uri) {
let colors = {};
try {
const body = await Spicetify.CosmosAsync.get(`https://spclient.wg.spotify.com/colorextractor/v1/extract-presets?uri=${uri}&format=json`);
for (const color of body.entries[0].color_swatches) {
colors[color.preset] = `#${color.color.toString(16).padStart(6, "0")}`;
}
} catch {
colors = {
DARK_VIBRANT: "#000000",
DESATURATED: "#000000",
LIGHT_VIBRANT: "#000000",
PROMINENT: "#000000",
VIBRANT: "#000000",
VIBRANT_NON_ALARMING: "#000000",
};
}
return colors;
}
function lightnessColor(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
let r = parseInt(result[1], 16);
let g = parseInt(result[2], 16);
let b = parseInt(result[3], 16);
return (Math.max(r, g, b) + Math.min(r, g, b)) / 2;
}
const DisplayIcon = ({ icon, size }) => {
return react.createElement("svg", {
width: size,
height: size,
viewBox: "0 0 16 16",
fill: "currentColor",
dangerouslySetInnerHTML: {
__html: icon,
},
});
};
const SubInfo = ({ text, id, icon }) => {
return react.createElement(
"div",
{
id,
},
CONFIG.icons && react.createElement(DisplayIcon, { icon, size: 35 }),
react.createElement("span", null, text)
);
};
const ButtonIcon = ({ icon, onClick, className = null, style = null, onMouseEnter = null, onMouseLeave = null }) => {
return react.createElement(
"button",
{
className,
style,
onClick,
onMouseEnter,
onMouseLeave,
},
react.createElement(DisplayIcon, { icon, size: 20 })
);
};
const ProgressBar = () => {
const [value, setValue] = useState(Spicetify.Player.getProgress());
useEffect(() => {
const update = ({ data }) => {
setValue(data);
};
Spicetify.Player.addEventListener("onprogress", update);
// @ts-ignore
return () => Spicetify.Player.removeEventListener("onprogress", update);
});
const duration = Spicetify.Platform.PlayerAPI._state.duration;
return react.createElement(
"div",
{ id: "fad-progress-container" },
react.createElement("span", { id: "fad-elapsed" }, Spicetify.Player.formatTime(value)),
react.createElement(
"div",
{
id: "fad-progress",
onClick: (e) => {
e.persist();
console.log(e);
let coors = document.querySelector("#fad-progress").getBoundingClientRect();
let temp = (e.pageX - coors.x) / coors.width;
console.log(temp);
Spicetify.Player.seek(temp);
setTimeout(console.log(Spicetify.Player.getProgressPercent()), 200);
document.querySelector("#fad-progress-inner").style.width = `${temp}%`;
},
},
react.createElement("div", {
id: "fad-progress-inner",
style: {
width: (value / duration) * 100 + "%",
},
})
),
react.createElement("span", { id: "fad-duration" }, Spicetify.Player.formatTime(duration))
);
};
// @ts-ignore
const VolumeBar = () => {
// @ts-ignore
const [value, setValue] = useState(Spicetify.Platform.PlaybackAPI._volume);
let isHover = false;
if (CONFIG["volumeBar"] == "onlyHover") {
isHover = true;
}
useEffect(() => {
const update = ({ data }) => {
setValue(data.volume);
};
Spicetify.Platform.PlaybackAPI._events.addListener("volume", update);
return () => Spicetify.Platform.PlaybackAPI._events.removeListener("volume", update);
});
return react.createElement(
"div",
{
id: "fad-volume",
style: {
top: `${(window.innerHeight - 256) / 2}px`,
opacity: isHover ? 0 : 1,
},
onWheel: (event) => {
let dir = event.deltaY < 0 ? 1 : -1;
let temp = parseInt(document.querySelector("#fad-volbar-inner").style.height) / 2 + dir * 1;
if (temp < 0) {
temp = 0;
} else if (temp > 100) {
temp = 100;
}
Spicetify.Player.setVolume(temp / 100);
document.querySelector("#fad-volbar-inner").style.height = `${2 * temp}px`;
},
},
react.createElement(
"div",
{ id: "fad-volicon" },
react.createElement(ButtonIcon, {
style: {
marginLeft: "18px",
backgroundColor: "transparent",
border: "0",
color: "white",
padding: "0 5px",
pointerEvents: "auto",
},
// @ts-ignore
icon: Spicetify.Player.getMute() ? Spicetify.SVGIcons["volume-off"] : Spicetify.SVGIcons["volume"],
onClick: () => {
if (!Spicetify.Player.getMute()) {
document.querySelector("#fad-volicon svg").innerHTML = Spicetify.SVGIcons["volume-off"];
document.querySelector("#fad-volbar-inner").style.height = `0px`;
Spicetify.Player.toggleMute();
} else {
Spicetify.Player.toggleMute();
document.querySelector("#fad-volicon svg").innerHTML = Spicetify.SVGIcons["volume"];
setTimeout(() => {
document.querySelector("#fad-volbar-inner").style.height = `${Spicetify.Player.getVolume() * 200}px`;
}, 200);
}
},
})
),
react.createElement(
"div",
{
id: "fad-volbar",
onClick: (e) => {
let temp = 200 - e.nativeEvent.layerY;
if (temp < 0) {
temp = 0;
}
Spicetify.Player.setVolume(temp / 200);
document.querySelector("#fad-volbar-inner").style.height = `${temp}px`;
},
},
react.createElement("div", {
id: "fad-volbar-inner",
style: {
height: `${value * 200}px`,
},
})
)
);
};
const upNext = async ({ index, queue }) => {
let meta,
uri,
bottom = -100,
right = 0,
color,
context,
invertDetails = false,
invertWhole = false,
isContext = false,
isColor = false;
let deets = document.querySelector("#fad-details");
const coor = deets.getBoundingClientRect();
let scale = CONFIG["scale"];
if (coor.bottom + scale * 90 + 10 > window.innerHeight) {
bottom = bottom + (coor.bottom + 90 * scale + 10 - window.innerHeight) / scale + 10;
right = -409;
}
if (Spicetify.Player.getRepeat() == 2) {
uri = Spicetify.Player.data.item.uri;
meta = Spicetify.Player.data.item.metadata;
} else {
// @ts-ignore
uri = Spicetify.Queue.nextTracks[index].contextTrack.uri;
// @ts-ignore
meta = Spicetify.Queue.nextTracks[index].contextTrack.metadata;
}
let artistNames = Object.keys(meta)
.filter((key) => key.startsWith("artist_name"))
.sort()
.map((key) => meta[key])
.join(" • ");
//@ts-ignore
if (Spicetify.Queue.nextTracks[index].provider == "context") {
isContext = true;
context = Spicetify.Player.data.context.metadata.context_description;
if (!context) {
const uriObj = Spicetify.URI.fromString(Spicetify.Player.data.context.uri);
switch (uriObj.type) {
case Spicetify.URI.Type.SEARCH:
context = `Search`;
break;
case Spicetify.URI.Type.COLLECTION:
context = "Liked Songs";
break;
case Spicetify.URI.Type.STATION:
case Spicetify.URI.Type.RADIO:
// @ts-ignore
const rType = uriObj.args[0];
context = `${rType} radio`;
break;
case Spicetify.URI.Type.FOLDER:
context = "Playlist Folder";
break;
default:
context = "unknown";
}
}
}
if (CONFIG["optionBackground"] === "colorText") {
isColor = true;
color = await fetchColors(uri);
color = color[CONFIG["colorChoice"]];
const luma =
parseInt(color.substring(1, 3), 16) * 0.2126 +
parseInt(color.substring(3, 5), 16) * 0.7152 +
parseInt(color.substring(5, 7), 16) * 0.0722;
if (luma > 180) {
invertDetails = true;
}
if (deets.style.filter == "invert(1)") {
invertWhole = true;
}
}
return react.createElement(
"div",
{
id: "fad-upnext",
style: {
bottom: queue ? "" : `${bottom}px`,
right: queue ? "" : `${right}px`,
backgroundColor: isColor ? color : "",
backgroundImage: isColor ? "" : `url(${meta.image_url})`,
backgroundPosition: isColor ? "" : "center",
backgroundRepeat: isColor ? "" : "no-repeat",
backgroundSize: isColor ? "" : "cover",
border: isColor ? "" : "2px solid",
borderColor: isColor ? "" : "white",
clipPath: queue ? (index == 0 ? "inset(0px 0px 0px)" : "inset(90px 0px 0px)") : "",
filter: invertWhole ? "invert(1)" : "invert(0)",
},
ref: (el) => !queue && el && el.style.setProperty("box-shadow", "0 0 8px rgb(0 0 0 / 30%)", "important"),
},
!isColor &&
react.createElement("div", {
id: "fad-upnext-blur",
}),
react.createElement("div", {
id: "fad-upnext-image",
style: {
backgroundImage: `url(${meta.image_url})`,
},
}),
react.createElement(
"div",
{
id: "fad-upnext-details",
style: { filter: invertDetails ? "invert(1)" : "invert(0)" },
},
react.createElement(
"p",
{
id: "fad-upnext-provider",
style: {
fontWeight: "700",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "300px",
},
},
queue
? isContext
? `Track No.${index + 1} in Queue from ${context}`
: `Track No.${index + 1} from Queue`
: isContext
? `Next From ${context}:`
: "Next in Queue:"
// isContext && react.createElement("em", {}, `${context}:`)
),
react.createElement(
"div",
{
id: "fad-upnext-title",
style: {
fontSize: "18px",
fontWeight: "900",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "300px",
},
},
meta.title
),
react.createElement(
"div",
{
id: "fad-upnext-artist",
style: {
fontWeight: "500",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "300px",
},
},
artistNames
)
)
);
};
const PlayerControls = () => {
const [value, setValue] = useState(Spicetify.Player.isPlaying());
let timer;
useEffect(() => {
const update = () => setValue(Spicetify.Player.isPlaying());
Spicetify.Player.addEventListener("onplaypause", update);
// @ts-ignore
return () => Spicetify.Player.removeEventListener("onplaypause", update);
});
return react.createElement(
"div",
{ id: "fad-controls" },
react.createElement(ButtonIcon, {
// @ts-ignore
icon: Spicetify.SVGIcons["skip-back"],
onClick: Spicetify.Player.back,
}),
react.createElement(ButtonIcon, {
// @ts-ignore
icon: Spicetify.SVGIcons[value ? "pause" : "play"],
onClick: Spicetify.Player.togglePlay,
}),
react.createElement(ButtonIcon, {
// @ts-ignore
icon: Spicetify.SVGIcons["skip-forward"],
onClick: Spicetify.Player.next,
onMouseEnter: async () => {
timer = setTimeout(async () => {
let cont = document.createElement("div");
cont.id = "fad-upnext-container";
let fore = document.querySelector("#fad-details");
fore.append(cont);
reactDOM.render(await upNext({ index: 0, queue: false }), cont);
}, 450);
},
onMouseLeave: () => {
let cont = document.querySelectorAll("#fad-upnext-container");
for (const con of cont) {
con.remove();
}
clearTimeout(timer);
},
})
);
};
const ExtraPlayerControls = () => {
const [isShuffle, setShuffle] = useState(Spicetify.Player.getShuffle());
const [isRepeat, setRepeat] = useState(Spicetify.Player.getRepeat());
const [isHeart, setHeart] = useState(Spicetify.Player.getHeart());
const [isPodcast, setPodcast] = useState(Spicetify.URI.isEpisode(Spicetify.Player.data.item.uri));
useEffect(() => {
const update = ({ data }) => {
data.item.metadata["collection.in_collection"] == "true" ? setHeart(true) : setHeart(false);
setPodcast(Spicetify.URI.isEpisode(Spicetify.Player.data.item.uri));
// @ts-ignore
const state = Spicetify.Player.origin._state;
if (!state.restrictions?.canToggleShuffle) {
setShuffle(undefined);
}
if (!state.restrictions?.canToggleRepeatContext || !state.restrictions?.canToggleRepeatTrack) {
setRepeat(undefined);
}
};
Spicetify.Player.addEventListener("songchange", update);
// @ts-ignore
return () => Spicetify.Player.removeEventListener("songchange", update);
});
return react.createElement(
"div",
{
id: "fad-extracontrols",
style: {
marginTop: CONFIG.vertical ? (CONFIG.enableControl ? "-25px" : "10px") : CONFIG.enableControl ? "-25px" : "",
width: CONFIG.vertical ? (CONFIG.enableControl ? "100%" : "") : CONFIG.enableControl ? "360px" : "",
alignSelf: !CONFIG.vertical && CONFIG.enableControl ? "baseline" : "",
},
},
react.createElement(ButtonIcon, {
// @ts-ignore
className: isShuffle
? "dot-after"
: // @ts-ignore
!Spicetify.Player.origin._state.restrictions?.canToggleShuffle || isShuffle == undefined
? "crossed-out"
: "",
style: {
marginLeft: CONFIG.vertical ? "18px" : "",
},
// @ts-ignore
icon: Spicetify.SVGIcons["shuffle"],
onClick: () => {
Spicetify.Player.toggleShuffle();
setShuffle(!isShuffle);
},
}),
react.createElement(ButtonIcon, {
// @ts-ignore
className: isRepeat
? "dot-after"
: // @ts-ignore
!Spicetify.Player.origin._state.restrictions?.canToggleRepeatContext ||
// @ts-ignore
!Spicetify.Player.origin._state.restrictions?.canToggleRepeatTrack ||
isRepeat == undefined
? "crossed-out"
: "",
// @ts-ignore
icon: Spicetify.SVGIcons[isRepeat == 2 ? "repeat-once" : "repeat"],
onClick: () => {
Spicetify.Player.toggleRepeat();
setRepeat((isRepeat + 1) % 3);
},
}),
react.createElement(ButtonIcon, {
// @ts-ignore
icon: isPodcast
? Spicetify.SVGIcons[isHeart ? "check-alt-fill" : "plus-alt"]
: Spicetify.SVGIcons[isHeart ? "heart-active" : "heart"],
style: {
marginLeft: CONFIG.vertical || CONFIG.enableControl ? "auto" : "",
marginRight: !CONFIG.vertical && !CONFIG.enableControl ? "10px" : "",
},
onClick: () => {
Spicetify.Player.toggleHeart();
setHeart(!isHeart);
},
}),
CONFIG.enableControl &&
react.createElement(ButtonIcon, {
// @ts-ignore
icon: '',
className: "fad-queue-button",
// @ts-ignore
// @ts-ignore
onClick: async (e) => {
let ele = document.querySelector(".fad-queue-button");
if (ele.classList.contains("dot-after")) {
let cont = document.querySelector("#fad-queue-container");
cont.remove();
ele.classList.remove("dot-after");
return;
}
ele.classList.add("dot-after");
let body = document.querySelector("#fad-body");
let noticont = document.createElement("div");
noticont.className = "main-notificationBubbleContainer-NotificationBubbleContainer";
let notitext = document.createElement("div");
notitext.className = "main-notificationBubble-NotificationBubble main-notificationBubble-isNotice";
notitext.innerText = "Generating queue...";
noticont.append(notitext);
body.append(noticont);
setTimeout(function () {
noticont.remove();
}, 1000);
const next = await upNext({ index: 0, queue: false });
const bottom = next.props.style.bottom;
const right = next.props.style.right;
CONFIG["viewing"] = 0;
let tracks = [];
for (var i = 0; i < 10; i++) {
try {
tracks.push(await upNext({ index: i, queue: true }));
} catch {
break;
}
}
let scroll = react.createElement(
"div",
{
id: "scroll-queue",
style: {
bottom: bottom,
right: right,
borderRadius: "10px",
boxShadow: "0 0 8px rgb(0 0 0 / 30%)",
},
onWheel: (e) => {
var now = Date.now();
if (time !== -1 && now - time < 1000) return;
time = now;
if (e.deltaY > 0) {
if (CONFIG["viewing"] == tracks.length - 1) {
return;
}
CONFIG["viewing"] += 1;
var item = document.querySelector("#scroll-queue").childNodes[CONFIG["viewing"]];
// @ts-ignore
item.style.clipPath = "inset(0px 0px 0px)";
} else {
if (CONFIG["viewing"] == 0) {
return;
}
var item = document.querySelector("#scroll-queue").childNodes[CONFIG["viewing"]];
// @ts-ignore
item.style.clipPath = "inset(90px 0px 0px)";
CONFIG["viewing"] -= 1;
}
},
},
tracks
);
let fore = document.querySelector("#fad-details");
let wrapper = document.createElement("div");
wrapper.id = "fad-queue-container";
fore.append(wrapper);
reactDOM.render(scroll, wrapper);
},
})
);
};
class FAD extends react.Component {
constructor(props) {
super(props);
this.state = {
title: "",
artist: "",
album: "",
cover: "",
};
this.currTrackImg = new Image();
this.nextTrackImg = new Image();
this.mousetrap = new Spicetify.Mousetrap();
}
async getAlbumDate(uri) {
const id = uri.replace("spotify:album:", "");
// const albumInfo = await Spicetify.CosmosAsync.get(`hm://album/v1/album-app/album/${id}/desktop`);
// const albumDate = new Date(albumInfo.year, (albumInfo.month || 1) - 1, albumInfo.day || 0);
// hermes protocol deprecated 1.1.81 onwards
const albumInfo = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/albums/${id}`);
const albumDate = new Date(albumInfo.release_date);
const recentDate = new Date();
recentDate.setMonth(recentDate.getMonth() - 6);
return albumDate.toLocaleString("default", albumDate > recentDate ? { year: "numeric", month: "short" } : { year: "numeric" });
}
async fetchInfo() {
const meta = Spicetify.Player.data.item.metadata;
const prevUri = nextUri;
nextUri = Spicetify.Player.data.item.uri;
const uriFinal = nextUri.split(":")[2];
let isLocalOrEpisode =
Spicetify.URI.isLocalTrack(Spicetify.Player.data.item.uri) || Spicetify.URI.isEpisode(Spicetify.Player.data.item.uri);
if (!isLocalOrEpisode) {
const ximage = await Spicetify.CosmosAsync.get("https://api.spotify.com/v1/tracks/" + uriFinal);
let images = ximage.album.images;
for (const image of images) {
if (image.height == 640) {
finImage = image.url;
}
}
updateStyle();
} else {
finImage = meta.image_xlarge_url;
style.innerHTML =
styleBase +
styleChoices[CONFIG.vertical ? 1 : 0] +
(window.innerHeight > window.innerWidth && CONFIG.verticalMonitor ? verticalMonitorStyle : "");
}
// prepare title
let rawTitle = meta.title;
if (CONFIG["trimTitle"] === "justFeat") {
rawTitle = rawTitle
.replace(/-\s+(feat|with|ft).*/i, "")
.replace(/(\(|\[)(feat|with|ft)\.?\s+.*(\)|\])/i, "")
.trim();
} else if (CONFIG["trimTitle"] === "trimEvery") {
rawTitle = rawTitle
.replace(/\(.+?\)/g, "")
.replace(/\[.+?\]/g, "")
.replace(/\s\-\s.+?$/, "")
.replace(/,.+?$/, "")
.trim();
}
// prepare artist
let artistName;
if (CONFIG.showAllArtists) {
artistName = Object.keys(meta)
.filter((key) => key.startsWith("artist_name"))
.sort()
.map((key) => meta[key])
.join(", ");
} else {
artistName = meta.artist_name;
}
// prepare album
let albumText = meta.album_title || "";
if (CONFIG.showAlbum) {
const albumURI = meta.album_uri;
if (albumURI?.startsWith("spotify:album:")) {
albumText += " • " + (await this.getAlbumDate(albumURI));
}
}
// if (meta.image_xlarge_url === this.currTrackImg.src) {
if (finImage === this.currTrackImg.src) {
this.setState({
title: rawTitle || "",
artist: artistName || "",
album: albumText || "",
});
if (CONFIG["optionBackground"] === "colorText" && !isLocalOrEpisode) {
this.animateCanvasColor(prevUri, prevUri);
} else if (CONFIG["optionBackground"] === "static" && !isLocalOrEpisode) {
this.animateCanvasColor(prevUri, prevUri, true);
} else if (CONFIG["optionBackground"] === "albumart") {
this.animateCanvas(this.currTrackImg, this.currTrackImg);
}
return;
}
if (isHidden) {
isHidden = false;
updateStyle();
}
// TODO: Pre-load next track
// Wait until next track image is downloaded then update UI text and images
const previousImg = this.currTrackImg.cloneNode();
this.currTrackImg.src = finImage;
this.currTrackImg.onload = () => {
const bgImage = this.currTrackImg.src;
if (CONFIG["optionBackground"] === "colorText" && !isLocalOrEpisode) {
this.animateCanvasColor(prevUri, nextUri);
} else if (CONFIG["optionBackground"] === "static" && !isLocalOrEpisode) {
this.animateCanvasColor(prevUri, nextUri, true);
} else if (CONFIG["optionBackground"] === "albumart") {
this.animateCanvas(previousImg, this.currTrackImg);
}
if (CONFIG.enableFade) {
this.deets.style.animation = "";
void this.deets.offsetWidth;
this.deets.style.animation = "textchange 1s forwards";
}
this.setState({
title: rawTitle || "",
artist: artistName || "",
album: albumText || "",
cover: bgImage,
});
if (CONFIG.lyricsPlus) {
autoHideLyrics();
}
};
this.currTrackImg.onerror = () => {
// Placeholder
this.currTrackImg.src =
"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCI+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiB4PSIwIiB5PSIwIiAvPgogIDxwYXRoIGZpbGw9IiNCM0IzQjMiIGQ9Ik0yNi4yNSAxNi4xNjJMMjEuMDA1IDEzLjEzNEwyMS4wMTIgMjIuNTA2QzIwLjU5NCAyMi4xOTIgMjAuMDgxIDIxLjk5OSAxOS41MTkgMjEuOTk5QzE4LjE0MSAyMS45OTkgMTcuMDE5IDIzLjEyMSAxNy4wMTkgMjQuNDk5QzE3LjAxOSAyNS44NzggMTguMTQxIDI2Ljk5OSAxOS41MTkgMjYuOTk5QzIwLjg5NyAyNi45OTkgMjIuMDE5IDI1Ljg3OCAyMi4wMTkgMjQuNDk5QzIyLjAxOSAyNC40MjIgMjIuMDA2IDE0Ljg2NyAyMi4wMDYgMTQuODY3TDI1Ljc1IDE3LjAyOUwyNi4yNSAxNi4xNjJaTTE5LjUxOSAyNS45OThDMTguNjkyIDI1Ljk5OCAxOC4wMTkgMjUuMzI1IDE4LjAxOSAyNC40OThDMTguMDE5IDIzLjY3MSAxOC42OTIgMjIuOTk4IDE5LjUxOSAyMi45OThDMjAuMzQ2IDIyLjk5OCAyMS4wMTkgMjMuNjcxIDIxLjAxOSAyNC40OThDMjEuMDE5IDI1LjMyNSAyMC4zNDYgMjUuOTk4IDE5LjUxOSAyNS45OThaIi8+Cjwvc3ZnPgo=";
};
}
animateCanvas(prevImg, nextImg) {
const { innerWidth: width, innerHeight: height } = window;
this.back.width = width;
this.back.height = height;
const dim = width > height ? width : height;
this.deets.style.filter = "invert(0)";
if (!(CONFIG["volumeBar"] === "disable")) {
document.querySelector("#fad-volume").style.filter = "invert(0)";
}
if (CONFIG.lyricsPlus) {
this.lyrics.style.setProperty("--lyrics-color-active", "#ffffff");
this.lyrics.style.setProperty("--lyrics-color-inactive", "#ffffff50");
}
const ctx = this.back.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.filter = `blur(30px) brightness(0.6)`;
const blur = 30;
if (!CONFIG.enableFade) {
ctx.globalAlpha = 1;
width > height
? ctx.drawImage(nextImg, -blur * 2, -blur * 2 - (width - height) / 2, dim + 4 * blur, dim + 4 * blur)
: ctx.drawImage(nextImg, -blur * 2 - (height - width) / 2, -blur * 2, dim + 4 * blur, dim + 4 * blur);
return;
}
let factor = 0.0;
const animate = () => {
ctx.globalAlpha = 1;
width > height
? ctx.drawImage(prevImg, -blur * 2, -blur * 2 - (width - height) / 2, dim + 4 * blur, dim + 4 * blur)
: ctx.drawImage(prevImg, -blur * 2 - (height - width) / 2, -blur * 2, dim + 4 * blur, dim + 4 * blur);
ctx.globalAlpha = Math.sin((Math.PI / 2) * factor);
width > height
? ctx.drawImage(nextImg, -blur * 2, -blur * 2 - (width - height) / 2, dim + 4 * blur, dim + 4 * blur)
: ctx.drawImage(nextImg, -blur * 2 - (height - width) / 2, -blur * 2, dim + 4 * blur, dim + 4 * blur);
if (factor < 1.0) {
factor += 0.016;
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
async animateCanvasColor(prevUri, nextUri, isStatic = false) {
const { innerWidth: width, innerHeight: height } = window;
const ctx = this.back.getContext("2d");
if (isStatic) {
if (ctx.fillStyle == CONFIG["staticColor"]) {
return;
} else {
ctx.filter = "brightness(1)";
ctx.imageSmoothingEnabled = false;
ctx.globalAlpha = 1;
ctx.fillStyle = CONFIG["staticColor"];
ctx.fillRect(0, 0, width, height);
return;
}
}
prevColor = await fetchColors(prevUri);
nextColor = await fetchColors(nextUri);
this.back.width = width;
this.back.height = height;
CONFIG["color"] = nextColor;
saveConfig();
this.deets.style.filter = "invert(0)";
if (!(CONFIG["volumeBar"] === "disable")) {
document.querySelector("#fad-volume").style.filter = "invert(0)";
}
if (CONFIG.lyricsPlus) {
this.lyrics.style.setProperty("--lyrics-color-active", "#ffffff");
this.lyrics.style.setProperty("--lyrics-color-inactive", "#ffffff50");
}
prevColor = prevColor[CONFIG["colorChoice"]];
nextColor = nextColor[CONFIG["colorChoice"]];
const luma =
parseInt(nextColor.substring(1, 3), 16) * 0.2126 +
parseInt(nextColor.substring(3, 5), 16) * 0.7152 +
parseInt(nextColor.substring(5, 7), 16) * 0.0722;
console.log(nextColor);
if (luma > 180) {
this.deets.style.filter = "invert(1)";
if (!(CONFIG["volumeBar"] === "disable")) {
document.querySelector("#fad-volume").style.filter = "invert(1)";
}
if (CONFIG.lyricsPlus) {
this.lyrics.style.setProperty("--lyrics-color-active", "#000000");
this.lyrics.style.setProperty("--lyrics-color-inactive", "#00000050");
}
}
if (!CONFIG.enableFade) {
ctx.globalAlpha = 1;
ctx.fillStyle = nextColor;
ctx.fillRect(0, 0, width, height);
return;
}
let factor = 0.0;
const animate = () => {
ctx.globalAlpha = 1;
ctx.fillStyle = prevColor;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = Math.sin((Math.PI / 2) * factor);
ctx.fillStyle = nextColor;
ctx.fillRect(0, 0, width, height);
if (factor < 1.0) {
factor += 0.016;
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
componentDidMount() {
this.updateInfo = this.fetchInfo.bind(this);
Spicetify.Player.addEventListener("songchange", this.updateInfo);
this.updateInfo();
updateVisual = () => {
updateStyle();
this.fetchInfo();
};
this.onQueueChange = async (queue) => {
queue = queue.data;
let nextTrack;
if (queue.queued.length) {
nextTrack = queue.queued[0];
} else {
nextTrack = queue.nextUp[0];
}
this.nextTrackImg.src = nextTrack.metadata.image_xlarge_url;
};
const scaleLimit = { min: 0.1, max: 4, step: 0.05 };
this.onScaleChange = (event) => {
if (!event.ctrlKey) return;
let dir = event.deltaY < 0 ? 1 : -1;
let temp = (CONFIG["scale"] || 1) + dir * scaleLimit.step;
if (temp < scaleLimit.min) {
temp = scaleLimit.min;
} else if (temp > scaleLimit.max) {
temp = scaleLimit.max;
}
CONFIG["scale"] = temp;
saveConfig();
updateVisual();
};
Spicetify.Platform.PlayerAPI._events.addListener("queue_update", this.onQueueChange);
this.mousetrap.bind("esc", deactivate);
window.dispatchEvent(new Event("fad-request"));
}
componentWillUnmount() {
Spicetify.Player.removeEventListener("songchange", this.updateInfo);
Spicetify.Platform.PlayerAPI._events.removeListener("queue_update", this.onQueueChange);
this.mousetrap.unbind("esc");
}
render() {
return react.createElement(
"div",
{
id: "full-app-display",
className: "Video VideoPlayer--fullscreen VideoPlayer--landscape",
onDoubleClick: deactivate,
onContextMenu: openConfig,
},
!(CONFIG["optionBackground"] === "grad") &&
react.createElement("canvas", {
id: "fad-background",
ref: (el) => (this.back = el),
}),
CONFIG["optionBackground"] === "grad" &&
react.createElement(
"div",
{ id: "fad-gradient-background" },
react.createElement("img", {
src: this.state.cover,
className: "fad-grad-image",
style: {
right: "-15%",
top: "-20%",
zIndex: 10,
transform: "scale(2)",
},
}),
react.createElement("img", {
src: this.state.cover,
className: "fad-grad-image",
style: {
left: "-5%",
bottom: "-10%",
transform: "scale(1.5)",
zIndex: 1,
animationDirection: "reverse",
},
}),
react.createElement("img", {
src: this.state.cover,
className: "fad-grad-image",
style: {
width: "200%",
right: "-50%",
top: "-33%",
filter: "blur(69px) brightness(0.6)",
zIndex: 0,
animationDirection: "reverse",
},
})
),
react.createElement("div", { id: "fad-header" }),
react.createElement(
"div",
{ id: "fad-body" },
react.createElement(
"div",
{
id: "fad-foreground",
style: {
"--fad-scale": CONFIG["scale"] || 1,
zIndex: 20,
},
ref: (el) => {
if (!el) return;
el.onmousewheel = this.onScaleChange;
},
},
react.createElement(
"div",
{ id: "fad-art" },
react.createElement(
"div",
{
id: "fad-art-image",
className: CONFIG.enableFade && "fad-background-fade",
style: {
backgroundImage: `url("${this.state.cover}")`,
},
},
react.createElement("div", { id: "fad-art-inner" })
)
),
react.createElement(
"div",
{ id: "fad-details", ref: (el) => (this.deets = el) },
react.createElement("div", { id: "fad-title" }, this.state.title),
react.createElement(SubInfo, {
id: "fad-artist",
text: this.state.artist,
// @ts-ignore
icon: Spicetify.SVGIcons.artist,
}),
CONFIG.showAlbum &&
react.createElement(SubInfo, {
id: "fad-album",
text: this.state.album,
// @ts-ignore
icon: Spicetify.SVGIcons.album,
}),
react.createElement(
"div",
{
id: "fad-status",
className: (CONFIG.enableControl || CONFIG.enableProgress) && "active",
style: {
flexDirection: !CONFIG.vertical && CONFIG.enableControl && CONFIG.enableExtraControl ? "column" : "",
},
},
CONFIG.enableControl && react.createElement(PlayerControls),
CONFIG.enableExtraControl && react.createElement(ExtraPlayerControls),
CONFIG.enableProgress && react.createElement(ProgressBar)
)
)
),
// @ts-ignore
!(CONFIG["volumeBar"] === "disable") && react.createElement(VolumeBar),
CONFIG.lyricsPlus &&
react.createElement("div", {
id: "fad-lyrics-plus-container",
style: {
"--lyrics-color-active": "#ffffff",
"--lyrics-color-inactive": "#ffffff50",
},
ref: (el) => (this.lyrics = el),
})
)
);
}
}
const classes = ["video", "video-full-screen", "video-full-window", "video-full-screen--hide-ui", "fad-activated"];
const container = document.createElement("div");
container.id = "fad-main";
let lastApp;
async function toggleFullscreen() {
if (CONFIG.enableFullscreen) {
await document.documentElement.requestFullscreen();
// @ts-ignore
} else if (document.webkitIsFullScreen) {
await document.exitFullscreen();
}
}
async function activate() {
await toggleFullscreen();
document.body.classList.add(...classes);
document.body.append(style, container);
reactDOM.render(react.createElement(FAD), container);
requestLyricsPlus();
}
function deactivate() {
// @ts-ignore
if (CONFIG.enableFullscreen || document.webkitIsFullScreen) {
document.exitFullscreen();
}
document.body.classList.remove(...classes);
reactDOM.unmountComponentAtNode(container);
style.remove();
container.remove();
window.dispatchEvent(new Event("fad-request"));
if (lastApp && lastApp !== "/lyrics-plus") {
Spicetify.Platform.History.push(lastApp);
}
}
function toggleFad() {
if (document.body.classList.contains("fad-activated")) {
deactivate();
} else {
activate();
}
}
function updateStyle() {
style.innerHTML =
styleBase +
styleChoices[CONFIG.vertical ? 1 : 0] +
(checkLyricsPlus() && CONFIG.lyricsPlus && !isHidden
? lyricsPlusBase +
lyricsPlusStyleChoices[CONFIG.vertical ? 1 : 0] +
(window.innerHeight > window.innerWidth && CONFIG.verticalMonitor ? verticalMonitorStyle : "")
: "");
}
function checkLyricsPlus() {
return Spicetify.Config?.custom_apps?.includes("lyrics-plus") || !!document.querySelector("a[href='/lyrics-plus']");
}
function autoHideLyrics() {
// @ts-ignore
if (!document.querySelector("#fad-lyrics-plus-container").innerText) {
setTimeout(autoHideLyrics, 100);
} else {
// @ts-ignore
if (document.querySelector("#fad-lyrics-plus-container").innerText == "(• _ • )") {
isHidden = true;
updateStyle();
}
}
}
function requestLyricsPlus() {
if (CONFIG.lyricsPlus && checkLyricsPlus()) {
lastApp = Spicetify.Platform.History.location.pathname;
if (lastApp !== "/lyrics-plus") {
Spicetify.Platform.History.push("/lyrics-plus");
}
}
window.dispatchEvent(new Event("fad-request"));
autoHideLyrics();
}
function getConfig() {
try {
const parsed = JSON.parse(Spicetify.LocalStorage.get("full-app-display-config") || "{}");
if (parsed && typeof parsed === "object") {
return parsed;
}
throw "";
} catch {
Spicetify.LocalStorage.set("full-app-display-config", "{}");
return {};
}
}
function saveConfig() {
Spicetify.LocalStorage.set("full-app-display-config", JSON.stringify(CONFIG));
}
const ConfigItem = ({ name, field, func, disabled = false }) => {
const [value, setValue] = useState(CONFIG[field]);
return react.createElement(
"div",
{ className: "setting-row" },
react.createElement("label", { className: "col description" }, name),
react.createElement(
"div",
{ className: "col action" },
react.createElement(
"button",
{
className: "switch" + (value ? "" : " disabled"),
disabled,
onClick: () => {
const state = !value;
CONFIG[field] = state;
setValue(state);
saveConfig();
func();
},
},
// @ts-ignore
react.createElement(DisplayIcon, { icon: Spicetify.SVGIcons.check, size: 16 })
)
)
);
};
const ConfigSelection = ({ name, field, options, def, func }) => {
const [value, setValue] = useState(CONFIG[field] ?? def);
return react.createElement(
"div",
{ className: "setting-row" },
react.createElement("label", { className: "col description" }, name),
react.createElement(
"div",
{ className: "col action" },
react.createElement(
"select",
{
value,
onChange: (e) => {
setValue(e.target.value);
CONFIG[field] = e.target.value;
saveConfig();
func();
},
},
Object.keys(options).map((item) =>
react.createElement(
"option",
{
value: item,
},
options[item]
)
)
)
)
);
};
const ConfigInput = ({ name, field, func, isColor = false }) => {
const [value, setValue] = useState(CONFIG[field]);
return react.createElement(
"div",
{ className: "setting-row" },
react.createElement("label", { className: "col description" }, name),
react.createElement(
"div",
{ className: "col action" },
react.createElement("input", {
type: isColor ? "color" : "",
value,
className: "input",
onChange: (e) => {
setValue(e.target.value);
CONFIG[field] = e.target.value;
saveConfig();
func();
},
})
)
);
};
const ConfigHotkey = ({ name, field, def, onChange = () => {} }) => {
const [value, setValue] = useState(CONFIG[field] ?? def);
const [trap] = useState(new Spicetify.Mousetrap());
function record() {
trap.handleKey = (character, modifiers, e) => {
if (e.type == "keydown") {
const sequence = [...new Set([...modifiers, character])];
if (sequence.length === 1 && sequence[0] === "esc") {
onChange("");
setValue("");
return;
}
setValue(sequence.join("+"));
}
};
}
function finishRecord() {
trap.handleKey = () => {};
onChange(value);
}
return react.createElement(
"div",
{
className: "setting-row",
},
react.createElement(
"label",
{
className: "col description",
},
name
),
react.createElement(
"div",
{
className: "col action",
},
react.createElement("input", {
value,
onFocus: record,
onBlur: finishRecord,
})
)
);
};
const colorRow = ({ name, color }) => {
let originalColor;
const modal = document.getElementsByTagName("generic-modal");
return react.createElement(
"div",
{ className: "color-row" },
react.createElement("label", { className: "col description" }, name),
react.createElement(
"div",
{ className: "col action" },
react.createElement("div", {
className: "col color",
style: {
height: "20px",
width: "20px",
border: "2px solid black",
clear: "both",
backgroundColor: CONFIG["color"][color],
},
// @ts-ignore
// @ts-ignore
onMouseEnter: (e) => {
originalColor = CONFIG["colorChoice"];
CONFIG["colorChoice"] = color;
// @ts-ignore
modal[0].style.opacity = 0.37;
updateVisual();
},
// @ts-ignore
// @ts-ignore
onMouseLeave: (e) => {
CONFIG["colorChoice"] = originalColor;
// @ts-ignore
modal[0].style.opacity = 1;
updateVisual();
},
// @ts-ignore
// @ts-ignore
onClick: (e) => {
CONFIG["colorChoice"] = color;
updateVisual();
// @ts-ignore
modal[0].style.opacity = 1;
Spicetify.PopupModal.hide();
},
})
)
);
};
function openColor(event) {
event.preventDefault();
const style = react.createElement("style", {
dangerouslySetInnerHTML: {
__html: `
.color-row::after {
content: "";
display: table;
clear: both;
}
.color-row .col {
display: flex;
padding: 10px 0;
align-items: center;
}
.color-row .col.description {
float: left;
padding-right: 15px;
}
.color-row .col.action {
float: right;
text-align: right;
}
`,
},
});
let colorContainer = react.createElement(
"div",
null,
style,
react.createElement(colorRow, { name: "Dark Vibrant", color: "DARK_VIBRANT" }),
react.createElement(colorRow, { name: "Desaturated", color: "DESATURATED" }),
react.createElement(colorRow, { name: "Light Vibrant", color: "LIGHT_VIBRANT" }),
react.createElement(colorRow, { name: "Vibrant", color: "VIBRANT" }),
react.createElement(colorRow, { name: "Vibrant(NA)", color: "VIBRANT_NON_ALARMING" })
);
Spicetify.PopupModal.display({
title: "Color Display (Hover to preview)",
content: colorContainer,
});
}
function openConfig(event) {
try {
event.preventDefault();
} catch {}
const style = react.createElement("style", {
dangerouslySetInnerHTML: {
__html: `
.setting-row::after {
content: "";
display: table;
clear: both;
}
.setting-row .col {
display: flex;
padding: 10px 0;
align-items: center;
}
.setting-row .col.description {
float: left;
padding-right: 15px;
}
.setting-row .col.action {
float: right;
text-align: right;
}
.setting-row .col.action input {
padding-left: 10px;
}
button.switch {
align-items: center;
border: 0px;
border-radius: 50%;
background-color: rgba(var(--spice-rgb-shadow), .7);
color: var(--spice-text);
cursor: pointer;
display: flex;
margin-inline-start: 12px;
padding: 8px;
}
button.switch.disabled,
button.switch[disabled] {
color: rgba(var(--spice-rgb-text), .3);
}
select {
color: var(--spice-text);
background: rgba(var(--spice-rgb-shadow), .7);
border: 0;
height: 32px;
}
`,
},
});
let configContainer = react.createElement(
"div",
null,
style,
react.createElement(ConfigItem, {
name: checkLyricsPlus() ? "Enable Lyrics Plus integration" : "Unable to find Lyrics Plus",
field: "lyricsPlus",
func: () => {
updateVisual();
requestLyricsPlus();
openConfig();
},
disabled: !checkLyricsPlus(),
}),
react.createElement(ConfigSelection, {
name: "Background",
field: "optionBackground",
options: {
albumart: "Album Art",
colorText: "Colorful background",
static: "Static Color",
// grad: "Gradient",
},
func: updateVisual,
}),
CONFIG["optionBackground"] == "static" &&
react.createElement(ConfigInput, {
name: "Select static color:",
field: "staticColor",
func: () => {
const ctx = document.getElementById("fad-background")?.getContext("2d");
ctx.filter = "brightness(1)";
ctx.imageSmoothingEnabled = false;
ctx.globalAlpha = 1;
ctx.fillStyle = CONFIG["staticColor"];
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
},
isColor: true,
}),
react.createElement(ConfigItem, { name: "Enable progress bar", field: "enableProgress", func: updateVisual }),
react.createElement(ConfigSelection, {
name: "Enable volume bar",
field: "volumeBar",
options: {
disable: "Disable",
onlyHover: "Show on hover only",
alwaysOn: "Show always",
},
func: updateVisual,
}),
react.createElement(ConfigItem, { name: "Enable controls", field: "enableControl", func: updateVisual }),
react.createElement(ConfigItem, { name: "Enable extra controls", field: "enableExtraControl", func: updateVisual }),
react.createElement(ConfigSelection, {
name: "Trim title",
field: "trimTitle",
options: {
dontTrim: "Don't trim title",
justFeat: "Trim just feat. and with ",
trimEvery: "Trim Everything",
},
func: updateVisual,
}),
react.createElement(ConfigItem, { name: "Show album", field: "showAlbum", func: updateVisual }),
react.createElement(ConfigItem, { name: "Show all artists", field: "showAllArtists", func: updateVisual }),
react.createElement(ConfigItem, { name: "Show icons", field: "icons", func: updateVisual }),
react.createElement(ConfigItem, { name: "Vertical mode", field: "vertical", func: updateVisual }),
CONFIG.lyricsPlus &&
window.innerHeight > window.innerWidth &&
react.createElement(ConfigItem, { name: "Vertical Monitor Mode", field: "verticalMonitor", func: updateStyle }),
react.createElement(ConfigItem, { name: "Enable fullscreen", field: "enableFullscreen", func: toggleFullscreen }),
react.createElement(ConfigItem, { name: "Enable song change animation", field: "enableFade", func: updateVisual }),
react.createElement(ConfigHotkey, {
name: "FAD hotkey: ",
field: "hotkey",
def: "alt+f",
onChange: (key) => {
CONFIG["hotkey"] = key;
saveConfig();
Spicetify.Mousetrap.bind(key, toggleFad);
},
}),
react.createElement(ConfigItem, { name: "Enable development features", field: "enableDev", func: openConfig }),
CONFIG.enableDev &&
react.createElement(ConfigSelection, {
name: "Color Choice (Press F6 for colors)",
field: "colorChoice",
options: {
DARK_VIBRANT: "Dark Vibrant",
DESATURATED: "Desaturated",
LIGHT_VIBRANT: "Light Vibrant",
VIBRANT: "Vibrant",
VIBRANT_NON_ALARMING: "Vibrant(NA)",
},
def: "LIGHT_VIBRANT",
func: updateVisual,
})
);
Spicetify.PopupModal.display({
title: "Full App Display",
content: configContainer,
});
}
// Add activator on top bar
new Spicetify.Topbar.Button(
"Full App Display",
// @ts-ignore
``,
activate
);
Spicetify.Mousetrap.bind(CONFIG["hotkey"] ?? "alt+f", toggleFad);
Spicetify.Mousetrap.bind("f6", openColor);
})();
================================================
FILE: goToSong/README.md
================================================
# Go to Song
Filename : `goToSong.js`
Go to the currrently playing song in a playlist **/or/** currently playing playlist.
## To use:
* Currently playing playlist: Go to Profile > GoToSong > Choose "Go To Song in current Playlist"

* Any Playlist: Right Click on the Playlist, and choose "Go to Currently Playing Song"

## Note:
You may need to adjust your delay if it's giving an error, follow the instructions in the popup.

## More
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: goToSong/goToSong.js
================================================
// @ts-check
// NAME: goToSong
// AUTHOR: huhridge
// DESCRIPTION: Go to currently playing song in playlists.
///
(function goToSong(){
const { Player, Menu, LocalStorage, Platform, ContextMenu, URI, React: react, ReactDOM: reactDOM} = Spicetify
let tracks, index, playlisturi, curruri;
if (!(Player && Menu && LocalStorage && Platform)) {
setTimeout(goToSong, 1000)
return
}
function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, delayInms);
});
}
if (!Spicetify.LocalStorage.get("goToDelay")){
Spicetify.LocalStorage.set("goToDelay", '200')
}
const ConfigInput = ({ name, lkey}) => {
const [value, setValue] = react.useState(Spicetify.LocalStorage.get("goToDelay"));
const setValueCallback = react.useCallback(
(event) => {
const value = event.target.value;
setValue(value);
Spicetify.LocalStorage.set(lkey, value)
},
[value]
);
return react.createElement(
"div",
{
className: "setting-row",
},
react.createElement(
"label",
{
className: "col description",
},
name
),
react.createElement(
"div",
{
className: "col action",
},
react.createElement("input", {
type: "number",
value,
onChange: setValueCallback,
})
)
);
};
function shouldDisplayGoTo(uris) {
if (uris.length > 1) {
return false;
}
const uri = uris[0];
const uriObj = Spicetify.URI.fromString(uri);
if (uriObj.type == Spicetify.URI.Type.PLAYLIST || uriObj.type == Spicetify.URI.Type.PLAYLIST_V2) {
return true;
}
return false;
}
async function scrollSong(playlisturi) {
tracks = await Spicetify.Platform.PlaylistAPI.getContents(playlisturi)
curruri = Spicetify.Player.data.item.uri
for (var i=0; i < tracks.items.length; i++){
if (tracks.items[i].uri == curruri){
break;
}
}
if (i == tracks.items.length){
Spicetify.showNotification("Song not in Playlist.")
return;
}
const playlisturl = '/playlist/' + playlisturi.split(':')[2]
if (!(Spicetify.Platform.History.location.pathname == playlisturl)){
Spicetify.Platform.History.push(playlisturl)
await delay(1000)
}
if ((i+2)<58 && i == (tracks.items.length-1)){
document.querySelector(`[aria-rowindex="${i+1}"]`).click()
await delay(1)
document.querySelector(`[aria-rowindex="${i+2}"]`).click()
return;
}
if ((i+2)<58){
document.querySelector(`[aria-rowindex="${i+2}"]`).click()
}
else{
try {
for (var j=57; j < tracks.items.length; j += 28){
document.querySelector(`[aria-rowindex="${j}"]`).click()
const delayms = Spicetify.LocalStorage.get('goToDelay')
await delay(Number(delayms))
if (Math.abs(j-(i+2)) < 28){
break
}
}
document.querySelector(`[aria-rowindex="${i+2}"]`).click()
}
catch(err){
Spicetify.showNotification(err + ' Try to Adjust your delay in Profile > GoToSong > Set Delay')
}
}
}
async function gotoCurrPlay() {
if (Spicetify.Player.data.context_uri.startsWith('spotify:playlist:')){
playlisturi = Spicetify.Player.data.context_uri
await scrollSong(playlisturi)
}
else {
Spicetify.showNotification('The song currently played is not part of a playlist.')
}
}
async function gotoselectedPlay(uris){
await scrollSong(uris[0])
}
let configContent = react.createElement(ConfigInput, {name: "Set Delay(in ms) (200 default)", lkey: "goToDelay"})
const goTocurrPlay = new Spicetify.Menu.Item(
"Go To Song in Playlist",
false,
gotoCurrPlay,
)
const goToConfig = new Spicetify.Menu.Item("Set Delay", false, () =>{
Spicetify.PopupModal.display({
title: "Set Delay",
content: configContent,
})
})
new Spicetify.Menu.SubMenu("GoToSong", [goTocurrPlay,goToConfig]).register();
new Spicetify.ContextMenu.Item("Go To Currently Playing Song", gotoselectedPlay, shouldDisplayGoTo).register();
})();
================================================
FILE: listPlaylistsWithSong/README.md
================================================
# List Playlists with Song
Filename : `listPlaylistsWithSong.js`
Adds context menu button to view playlists in your library that contain the selected song.

## To use:
Right Click on selected song, and click "List playlists with this Song".

### Note:
~~Currently, doesn't work on the currently playing song(like in the bottom bar), finding a workaround.~~
Now it shows up, fixed by your truly!
## More
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: listPlaylistsWithSong/listPlaylistsWithSong.js
================================================
//@ts-check
// NAME: ListPlaylistsWithSong
// AUTHOR: huhridge (based on elijaholmos's version)
// DESCRIPTION: Adds context menu button to view playlists in your library that contain the selected song
///
(async function listPlaylistsWithSong() {
const { Player, Menu, LocalStorage, Platform, React: react, ReactDOM: reactDOM } = Spicetify;
if (!(Player && Menu && LocalStorage && Platform)) {
setTimeout(listPlaylistsWithSong, 1000);
return;
}
function delay(delayInms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, delayInms);
});
}
// const user = await Spicetify.Platform.UserAPI.getUser()
async function recursivePlaylistFolder(folder) {
//to get every playlist no matter how deep, thanks to elijaholmos for reminding me, else i would have forgotten it.
let playlists = [];
for (const playlist of folder) {
if (playlist.type == "playlist") {
if ((playlist.isCollaborative || playlist.isOwnedBySelf || playlist.canAdd) && playlist.totalLength > 0) {
let image;
try {
image = !playlist.images[0]
? (await Spicetify.Platform.PlaylistAPI.getMetadata(playlist.uri)).images[0].url
: playlist.images[0].url;
} catch {
image = "";
}
playlists.push({
uri: playlist.uri,
title: playlist.name,
desc: playlist.description,
isCollab: playlist.isCollaborative || playlist.canAdd,
noOfSongs: playlist.totalLength,
created: playlist.addedAt.toLocaleString("default", { year: "numeric", month: "short", day: "numeric" }),
image: image,
});
}
} else if (playlist.type == "folder") {
playlists.push(...(await recursivePlaylistFolder(playlist.items)));
}
}
return playlists;
}
async function getUserLibrary() {
let playlistsToCheck = Array();
const userContents = await Spicetify.Platform.RootlistAPI.getContents();
for (const playlist of userContents.items) {
if (playlist.type == "playlist") {
if ((playlist.isCollaborative || playlist.isOwnedBySelf || playlist.canAdd) && playlist.totalLength > 0) {
let image;
try {
image = !playlist.images[0]
? (await Spicetify.Platform.PlaylistAPI.getMetadata(playlist.uri)).images[0].url
: playlist.images[0].url;
} catch {
image = "";
}
playlistsToCheck.push({
uri: playlist.uri,
title: playlist.name,
desc: playlist.description,
isCollab: playlist.isCollaborative || playlist.canAdd,
noOfSongs: playlist.totalLength,
created: playlist.addedAt.toLocaleString("default", { year: "numeric", month: "short", day: "numeric" }),
image: image,
});
}
} else if (playlist.type == "folder") {
playlistsToCheck.push(...(await recursivePlaylistFolder(playlist.items)));
}
}
return playlistsToCheck;
}
async function checkPlaylist(playlist, songUri) {
var songFound = false;
let addedAtDate;
const tracks = await Spicetify.Platform.PlaylistAPI.getContents(playlist.uri);
for (var i = 0; i < tracks.items.length; i++) {
if (tracks.items[i].uri == songUri) {
songFound = true;
addedAtDate = new Date(tracks.items[i].addedAt).toLocaleString("default", { year: "numeric", month: "short", day: "numeric" });
break;
}
}
if (songFound) {
playlist.index = i + 1;
playlist.songAddedAt = addedAtDate;
return playlist;
} else {
return false;
}
}
const playlistCard = ({ playlist }) => {
let isDesc = false;
if (playlist.desc) {
isDesc = true;
}
return react.createElement(
"div",
{
className: "contentSpacing main-entityHeader-container main-entityHeader-nonWrapped main-trackList-trackListHeaderRow",
style: {
minHeight: "280px",
marginLeft: "2%",
marginRight: "2%",
justifyContent: "left",
},
},
react.createElement(
"div",
{ className: "main-entityHeader-imageContainer" },
react.createElement("img", {
className: "main-image-image",
src: playlist.image,
style: {
height: "inherit",
},
})
),
react.createElement(
"div",
{ className: "main-entityHeader-headerText" },
react.createElement(
"h2",
{ className: "main-entityHeader-subtitle main-entityHeader-small main-entityHeader-uppercase main-entityHeader-bold" },
playlist.isCollab ? "Collaborative Playlist" : "Playlist"
),
react.createElement(
"h1",
{
className: "main-entityHeader-title main-type-bass",
style: {
padding: "0.08em 0px",
visibility: "visible",
width: "100%",
fontSize: "9vmin",
lineHeight: "9vmin",
},
},
react.createElement(
"a",
{
href: playlist.uri,
draggable: "false",
},
playlist.title
)
),
isDesc &&
react.createElement("h2", { className: "main-entityHeader-subtitle main-entityHeader-gray main-type-viola" }, playlist.desc),
react.createElement(
"span",
{ className: "main-entityHeader-metaData main-type-mesto" },
`${playlist.created} • ${playlist.noOfSongs} songs`
)
)
);
};
async function listPlaylists(uris) {
// getting playlists to display
const allPlaylists = await getUserLibrary();
const playlistsFound = [];
for (var playlist of allPlaylists) {
const playlistRes = await checkPlaylist(playlist, uris[0]);
if (playlistRes) {
playlistsFound.push(playlistRes);
}
}
if (playlistsFound.length == 0) {
Spicetify.showNotification("Song is not in any of your playlists.");
return;
}
// getting song data to prepare elements
const songmeta = await Spicetify.CosmosAsync.get("https://api.spotify.com/v1/tracks/" + uris[0].split(":")[2]);
Spicetify.Platform.History.push(`/album/${songmeta.album.uri.split(":")[2]}?highlight=${uris[0]}`);
await delay(2000); // waiting to load.
// modifying album page and saving info card and song row
let songRow = document.querySelector(`[aria-selected="true"]`);
let section = document.querySelector(`[data-testid="album-page"]`);
// if (songmeta.album.total_tracks == 1) {
// songRow = document.querySelector(`[aria-rowindex="2"]`).cloneNode(true);
// } else {
// songRow = document.querySelector(`[aria-rowindex="3"]`).cloneNode(true);
// songRow.childNodes[0].childNodes[1].childNodes[0].childNodes[0].innerText = songmeta.name;
// if (songmeta.artists.length > 1) {
// let artists = "";
// for (const artist of songmeta.artists) {
// artists = artists.concat(artist.name, ", ");
// }
// artists = artists.slice(0, -2);
// if (songmeta.explicit) {
// songRow.childNodes[0].childNodes[1].childNodes[0].childNodes[2].innerHTML = artists;
// } else {
// songRow.childNodes[0].childNodes[1].childNodes[0].childNodes[1].innerHTML = artists;
// }
// }
// }
let info = document.querySelector(`[data-testid="album-page"] > div`).cloneNode(true);
info.classList.add("main-trackList-trackListHeaderRow");
section.innerHTML = ""; //wiping all other elements
await delay(200); // waiting for the topbar text to appear, to remove it
document
.querySelector(".main-topBar-topbarContent.main-entityHeader-topbarContent.main-entityHeader-topbarContentFadeIn")
.classList.remove("main-entityHeader-topbarContentFadeIn");
section.appendChild(info);
// creating the heading
let appearsIn = document.createElement("h1");
appearsIn.className = "main-type-bass main-trackList-trackListHeaderRow";
appearsIn.style.fontSize = "48px";
appearsIn.style.lineHeight = "60px";
appearsIn.style.paddingLeft = "10px";
appearsIn.style.height = "auto";
appearsIn.innerText = `Appears In ${playlistsFound.length}/${allPlaylists.length} of your playlists:`;
section.appendChild(appearsIn); //adding it
// modifying info card
let infoText = section.childNodes[0].childNodes[5];
infoText.childNodes[0].innerText = "SONG";
infoText.childNodes[1].childNodes[0].style.fontSize = "8vmin";
infoText.childNodes[1].childNodes[0].style.lineHeight = "8vmin";
infoText.childNodes[1].childNodes[0].innerText = songmeta.name;
if (songmeta.album.total_tracks > 1) {
let albumText = infoText.childNodes[0].cloneNode();
albumText.classList.remove("main-entityHeader-uppercase", "main-entityHeader-small");
albumText.style = "margin-top: 0px; margin-bottom: 8px";
albumText.innerText = `Track ${songmeta.track_number} / ${songmeta.album.total_tracks} • ${songmeta.album.name}`;
infoText.childNodes[1].appendChild(albumText);
infoText.childNodes[2].childNodes[2].innerText = `1 song, ${parseInt(songmeta.duration_ms / 1000 / 60)} min ${parseInt(
(songmeta.duration_ms / 1000) % 60
)} sec`;
}
// preparing song row element
let songImage = document.createElement("img"); //getting small album art and adding it
songImage.className = "main-image-image main-trackList-rowImage";
songImage.src = songmeta.album.images.at(-1).url;
songImage.width = 40;
songImage.height = 40;
songRow.childNodes[0].classList.remove("main-trackList-selected");
songRow.childNodes[0].style = "grid-template-columns: [index] 30px [first] 4fr [var1] 3fr [last] minmax(240px,2fr);";
songRow.childNodes[0].childNodes[1].insertBefore(songImage, songRow.childNodes[0].childNodes[1].firstChild);
songRow.childNodes[0].childNodes[2].childNodes[0].style.width = "fit-content";
songRow.childNodes[0].childNodes[2].childNodes[0].innerText = songmeta.album.name;
songRow.childNodes[0].childNodes[3].childNodes[1].classList.remove("main-trackList-rowDuration");
if (songRow.childNodes[0].classList.contains("main-trackList-active")) {
let spanIndex = document.createElement("span"); //adding index in playlist to song row
spanIndex.classList.add("main-trackList-number", "main-type-ballad");
songRow.childNodes[0].childNodes[0].childNodes[0].childNodes[0].remove();
songRow.childNodes[0].childNodes[0].childNodes[0].appendChild(spanIndex);
}
// finally rendering the playlists
for (const playlist of playlistsFound) {
let preElement = document.createElement("div");
preElement.classList.add("main-trackList-trackListHeaderRow");
preElement.style.height = "auto";
section.append(preElement);
const playlist_card = react.createElement(playlistCard, { playlist: playlist });
reactDOM.render(playlist_card, preElement);
songRow.childNodes[0].childNodes[0].childNodes[0].childNodes[0].innerText = playlist.index;
songRow.childNodes[0].childNodes[3].childNodes[1].innerText = playlist.songAddedAt;
// finally adding song row
preElement.appendChild(songRow.cloneNode(true));
}
}
new Spicetify.ContextMenu.Item(
"List playlists with this Song",
listPlaylists,
(uris) => {
if (uris.length != 1) return false;
return Spicetify.URI.fromString(uris[0]).type == Spicetify.URI.Type.TRACK;
},
"search"
).register();
})();
================================================
FILE: manifest.json
================================================
[
{
"name": "Full App Display Modified",
"description": "View your music at a glance.",
"preview": "fullAppDisplayModified/previews/preview.gif",
"main": "fullAppDisplayModified/fullAppDisplayMod.js",
"readme": "fullAppDisplayModified/README.md"
},
{
"name" : "SkipStats",
"description" : "See your skipping stats in playlists and albums!",
"preview" : "skipStats/preview.jpg",
"main" : "skipStats/skipStats.js",
"readme" : "skipStats/README.md"
},
{
"name": "listPlaylistsWithSong",
"description": "Adds context menu button to view playlists in your library that contain the selected song",
"preview": "listPlaylistsWithSong/preview1.jpg",
"main": "listPlaylistsWithSong/listPlaylistsWithSong.js",
"readme": "listPlaylistsWithSong/README.md"
},
{
"name": "GoToSong",
"description": "Go to currently playing song in playlist",
"preview": "goToSong/preview1.jpg",
"main": "goToSong/goToSong.js",
"readme": "goToSong/README.md"
},
{
"name": "playlistIntersection",
"description": "Adds context menu buttons to see songs in common between two playlists.",
"preview": "playlistIntersection/preview.jpg",
"main": "playlistIntersection/playlistIntersection.js",
"readme": "playlistIntersection/README.md"
},
{
"name": "Display full Album date",
"description": "Display the full album date instead of just year",
"preview": "fullAlbumDate/preview.jpg",
"main": "fullAlbumDate/fullAlbumDate.js",
"readme": "fullAlbumDate/README.md"
}
]
================================================
FILE: playlistIntersection/README.md
================================================
# playlistIntersection
Filename : `playlistIntersection.js`
Adds context menu buttons to see
* songs in common between two playlists
* songs only present in one playlist
and convert them to a playlist.

## To use:
* Right Click on desired playlist, and click "Select for Intersection".

* Right Click on a second playlist, and click "Compare with Selected Playlist".

* If you want to clear your first selection, Go to profile, and click "Clear Selection from Intersection".

### Note:
After comparing, please wait for all the songs to load, and then change the mode or it **will** malfunction.
## Convert to Playlist:

Clicking this button will turn the displayed tracks into a playlist.
## Changing Modes:

Clicking this button, will cycle through the following modes:
* Intersection: This mode displays the song common in both the playlists. This is the default mode and will always open.


* Songs only in Playlist 1: This displays the song present only in the first playlist.


* Songs only in Playlist 2: This displays the song present only in the second playlist.


## More
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: playlistIntersection/playlistIntersection.js
================================================
//@ts-check
// NAME: playlistIntersection
// AUTHOR: huhridge
// DESCRIPTION: Adds context menu buttons to see songs in common between two playlists.
///
///
(async function playlistIntersecter() {
const { Player, Menu, LocalStorage, Platform, ReactDOM: reactDOM } = Spicetify;
let play1, play2, name1, name2, commonTracks;
let renderedTracks;
/** @type {React} */
const react = Spicetify.React;
if (!(Player && Menu && LocalStorage && Platform)) {
setTimeout(playlistIntersecter, 1000);
return;
}
function delay(delayInms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, delayInms);
});
}
function trackIntersection(track1, track2) {
var inter = new Array();
var uri1 = new Array();
for (i = 0; i < track1.length; i++) {
uri1.push(track1[i].uri);
}
for (var i = 0; i < track2.length; i++) {
var index1 = uri1.indexOf(track2[i].uri);
if (index1 >= 0) {
track2[i].index1 = index1;
track2[i].index2 = i;
inter.push(track2[i]);
}
}
return inter;
}
const playlistInfo = (playlist, isLeft) => {
let isDesc = false;
if (playlist.description) {
isDesc = true;
}
return react.createElement(
"div",
{
className: "contentSpacing main-entityHeader-container main-entityHeader-nonWrapped",
style: {
marginTop: "1.5%",
marginBottom: "1.5%",
justifyContent: isLeft ? "left" : "start",
paddingLeft: isLeft ? "32px" : "16px",
paddingRight: isLeft ? "16px" : "32px",
borderRight: isLeft ? "1px solid rgba(255,255,255,.1)" : "",
},
},
react.createElement(
"div",
{
draggable: false,
style: {
alignSelf: "center",
position: "relative",
height: "232px",
minWidth: "232px",
width: "232px",
marginInlineEnd: "24px",
},
},
react.createElement("img", {
className: "main-image-image main-entityHeader-shadow",
style: {
height: "100%",
width: "100%",
},
src: playlist.images[0].url,
})
),
react.createElement(
"div",
{
className: "main-entityHeader-headerText",
style: {
justifyContent: "center",
},
},
react.createElement(
"h2",
{
className: "main-entityHeader-subtitle main-entityHeader-small main-entityHeader-uppercase main-entityHeader-bold",
},
playlist.isCollaborative ? "Collaborative Playlist" : "Playlist"
),
react.createElement(
"span",
{
className: "main-entityHeader-title",
},
react.createElement(
"h1",
{
dir: "auto",
className: "main-type-bass",
style: {
padding: "0.08em 0px",
visibility: "visible",
width: "100%",
lineHeight: "3vw",
letterSpacing: "-0.04em",
fontWeight: "900",
},
ref: (el) => el && el.style.setProperty("font-size", "2.5vw", "important"),
},
react.createElement(
"a",
{
href: playlist.uri,
draggable: "false",
},
playlist.name
)
)
),
isDesc &&
react.createElement(
"h2",
{ className: "main-entityHeader-subtitle main-entityHeader-gray main-type-viola" },
playlist.description
),
react.createElement(
"div",
{
className: "main-entityHeader-metaData",
},
react.createElement(
"span",
{
className: "main-type-mesto",
},
react.createElement(
"a",
{
href: playlist.owner.uri,
},
playlist.owner.displayName
),
` • ${playlist.totalLength} songs`
)
)
)
);
};
const songRowHeader = () => {
return react.createElement(
"div",
{
className: "main-trackList-trackListRowGrid",
role: "row",
"aria-rowindex": "1",
style: {
marginTop: "10px",
marginBottom: "8px",
},
ref: (el) =>
el && el.style.setProperty("grid-template-columns", "[index] 1fr [first] 6fr [var1] 6fr [var2] 1fr [last] 1fr", "important"),
},
react.createElement(
"div",
{
className: "main-trackList-rowSectionIndex",
role: "columnheader",
"aria-colindex": "1",
},
"#"
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionStart",
role: "columnheader",
"aria-colindex": "2",
},
react.createElement(
"span",
{
className: "standalone-ellipsis-one-line main-type-minuet",
},
"title"
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionVariable",
role: "columnheader",
"aria-colindex": "3",
},
react.createElement(
"span",
{
className: "standalone-ellipsis-one-line main-type-minuet",
},
"album"
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionVariable",
role: "columnheader",
"aria-colindex": "4",
},
react.createElement(
"svg",
{
role: "img",
height: "16",
width: "16",
fill: "var(--spice-subtext)",
viewBox: "0 0 16 16",
},
react.createElement("path", {
d: "M7.999 3h-1v5h3V7h-2V3zM7.5 0a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm0 14C3.916 14 1 11.084 1 7.5S3.916 1 7.5 1 14 3.916 14 7.5 11.084 14 7.5 14z",
}),
react.createElement("path", {
fill: "none",
d: "M16 0v16H0V0z",
})
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionIndex",
role: "columnheader",
"aria-colindex": "5",
},
"#"
)
);
};
const songRow = (song, i, display = "both") => {
let artists = "";
for (const artist of song.artists) {
artists = artists.concat(artist.name, ", ");
}
let displayOne = true;
let displayTwo = true;
artists = artists.slice(0, -2);
if (display == "1") {
displayTwo = false;
} else if (display == "2") {
displayOne = false;
}
return react.createElement(
"div",
{
className: "main-trackList-trackListRow main-trackList-trackListRowGrid",
draggable: true,
role: "presentation",
"aria-rowindex": `${i}`,
ref: (el) =>
el && el.style.setProperty("grid-template-columns", "[index] 1fr [first] 6fr [var1] 6fr [var2] 1fr [last] 1fr", "important"),
onDoubleClick: () => {
Spicetify.Player.playUri(song.uri);
},
},
react.createElement(
"div",
{
className: "main-trackList-rowSectionStart",
role: "gridcell",
style: {
justifySelf: "end",
},
"aria-colindex": "1",
},
displayOne &&
react.createElement(
"div",
{
className: "main-trackList-rowMarker",
},
react.createElement(
"span",
{
className: "main-trackList-number main-type-ballad",
},
song.index1 + 1
)
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionStart",
role: "gridcell",
"aria-colindex": "2",
},
react.createElement("img", {
className: "main-image-image main-trackList-rowImage",
draggable: false,
width: "40",
height: "40",
src: song.album.images[1].url,
}),
react.createElement(
"div",
{
className: "main-trackList-rowMainContent",
},
react.createElement(
"div",
{
className: "main-trackList-rowTitle standalone-ellipsis-one-line main-type-ballad",
dir: "auto",
},
song.name
),
song.isExplicit &&
react.createElement(
"span",
{
className: "main-trackList-rowBadges main-type-ballad",
style: {
color: "var(--spice-subtext)",
},
},
react.createElement(
"span",
{
className: "main-tag-container",
title: "Explicit",
},
"E"
)
),
react.createElement(
"span",
{
className: "main-trackList-rowSubTitle standalone-ellipsis-one-line main-type-mesto",
style: {
color: "var(--spice-subtext)",
},
},
artists
)
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionVariable",
role: "gridcell",
"aria-colindex": "3",
},
react.createElement(
"a",
{
draggable: true,
className: "standalone-ellipsis-one-line main-type-mesto",
href: song.album.uri,
},
song.album.name
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionVariable",
role: "gridcell",
"aria-colindex": "4",
},
react.createElement(
"span",
{
className: "main-type-mesto",
style: {
color: "var(--spice-subtext)",
},
},
`${parseInt(song.duration.milliseconds / 1000 / 60)}:${parseInt((song.duration.milliseconds / 1000) % 60).toLocaleString(
undefined,
{
minimumIntegerDigits: 2,
useGrouping: false,
}
)}`
)
),
react.createElement(
"div",
{
className: "main-trackList-rowSectionEnd",
role: "gridcell",
"aria-colindex": "5",
style: {
justifyContent: "end",
},
},
displayTwo &&
react.createElement(
"div",
{
className: "main-trackList-rowMarker",
},
react.createElement(
"span",
{
className: "main-trackList-number main-type-ballad",
},
song.index2 + 1
)
)
)
);
};
const interHeading = () => {
return react.createElement(
"div",
{
className: "interHeading",
style: {
display: "flex",
justifyContent: "flex-start",
borderBottom: "1px solid rgba(255,255,255,.1)",
},
},
react.createElement("h1", {
className: "main-type-bass main-trackList-trackListHeaderRow",
style: {
fontSize: "30px",
height: "100%",
lineHeight: "60px",
paddingLeft: "10px",
justifySelf: "flex-start",
border: "none",
},
}),
react.createElement(
"button",
{
className: "changeMode main-topBar-button",
title: "Change Mode",
style: {
display: "inline-flex",
alignSelf: "center",
width: "32px",
height: "32px",
marginLeft: "auto",
marginRight: "16px",
borderWidth: "0px",
borderRadius: "5px",
alignItems: "center",
justifyContent: "center",
},
ref: (el) => el && el.style.setProperty("background-color", "var(--spice-button)", "important"),
onClick: () => {
let ele = document.querySelector(".changeMode.main-topBar-button");
if (LocalStorage.get("spicetify-intermode") == "Intersect" && Spicetify.LocalStorage.get("spicetify-interorder") == "0") {
LocalStorage.set("spicetify-intermode", "exceptIntersect");
Spicetify.LocalStorage.set("spicetify-interorder", "1");
ele.style.backgroundColor = "var(--spice-button-active)";
let d = ele.firstChild.firstChild.getAttribute("d");
d = d + "M4.318 10.836A.5.5 0 006.089 11.548.5.5 0 004.318 10.836Z";
ele.firstChild.firstChild.setAttribute("d", d);
exceptIntersect();
} else if (
LocalStorage.get("spicetify-intermode") == "exceptIntersect" &&
Spicetify.LocalStorage.get("spicetify-interorder") == "1"
) {
Spicetify.LocalStorage.set("spicetify-interorder", "2");
ele.firstChild.innerHTML = Spicetify.SVGIcons.copy;
let d = ele.firstChild.firstChild.getAttribute("d");
d = d + "M10.423 3.806A.5.5 0 0011.411 5.234.5.5 0 0010.423 3.806Z";
ele.firstChild.firstChild.setAttribute("d", d);
exceptIntersect();
} else {
LocalStorage.set("spicetify-intermode", "Intersect");
Spicetify.LocalStorage.set("spicetify-interorder", "0");
ele.firstChild.innerHTML = Spicetify.SVGIcons.copy;
ele.style.backgroundColor = "var(--spice-button)";
renderIntersect();
}
},
},
react.createElement("svg", {
role: "img",
width: "16",
height: "16",
fill: "black",
viewBox: "0 0 16 16",
class: "modeSVG",
dangerouslySetInnerHTML: {
__html: Spicetify.SVGIcons.copy,
},
})
),
react.createElement(
"button",
{
className: "convertPlaylist main-topBar-button",
title: "Convert to Playlist",
style: {
display: "inline-flex",
alignSelf: "center",
width: "32px",
height: "32px",
marginRight: "16px",
borderWidth: "0px",
borderRadius: "5px",
alignItems: "center",
justifyContent: "center",
},
ref: (el) => el && el.style.setProperty("background-color", "var(--spice-button)", "important"),
onClick: () => {
convertToPlaylist();
},
},
react.createElement("svg", {
role: "img",
width: "16",
height: "16",
fill: "black",
viewBox: "0 0 16 16",
class: "playlistSVG",
dangerouslySetInnerHTML: {
__html: Spicetify.SVGIcons.playlist,
},
})
)
);
};
async function intersect() {
play1 = LocalStorage.get("spicetify-interplaylist1");
play2 = LocalStorage.get("spicetify-interplaylist2");
LocalStorage.set("spicetify-intermode", "Intersect");
LocalStorage.set("spicetify-interorder", "0");
LocalStorage.remove("spicetify-interplaylist1");
LocalStorage.remove("spicetify-interplaylist2");
const tracks1 = (await Spicetify.Platform.PlaylistAPI.getContents(play1)).items;
const tracks2 = (await Spicetify.Platform.PlaylistAPI.getContents(play2)).items;
commonTracks = trackIntersection(tracks1, tracks2);
if (commonTracks.length == 0) {
Spicetify.showNotification("No common tracks between the playlists.");
return;
}
const meta1 = await Spicetify.Platform.PlaylistAPI.getMetadata(play1);
name1 = meta1.name;
const meta2 = await Spicetify.Platform.PlaylistAPI.getMetadata(play2);
name2 = meta2.name;
Spicetify.Platform.History.push(`/playlist/${meta1.uri.split(":")[2]}`);
await delay(1000);
let section = document.querySelector(`[data-testid="playlist-page"]`);
section.innerHTML = "";
await delay(200); // waiting for the topbar text to appear, to remove it
document
.querySelector(".main-topBar-topbarContent.main-entityHeader-topbarContent.main-entityHeader-topbarContentFadeIn")
.classList.remove("main-entityHeader-topbarContentFadeIn");
let style = document.createElement("style");
style.innerHTML = `
.main-type-mesto {
font-size: 14px;
font-weight: 400;
letter-spacing: normal;
line-height: 16px;
text-transform: none
}
.main-type-bass {
font-size: 96px;
font-weight: 900;
letter-spacing: -.04em;
line-height: 96px;
text-transform: none
}
.main-type-minuet {
font-size: 12px;
font-weight: 400;
letter-spacing: .1em;
line-height: 16px;
text-transform: uppercase
}
.main-type-ballad {
font-size: 16px;
font-weight: 400;
letter-spacing: normal;
line-height: 24px;
text-transform: none
}
`;
section.append(style);
let playContainer = document.createElement("div");
playContainer.classList.add("main-trackList-trackListHeaderRow");
playContainer.style.display = "flex";
playContainer.style.borderBottom = "1px solid rgba(255,255,255,.1)";
playContainer.style.height = "100%"
section.append(playContainer);
const playele1 = playlistInfo(meta1, true);
const playele2 = playlistInfo(meta2, false);
let container = document.querySelector(".main-trackList-trackListHeaderRow");
let preElement1 = document.createElement("div");
preElement1.style.width = "50%";
container.append(preElement1);
reactDOM.render(playele1, preElement1);
let preElement2 = document.createElement("div");
preElement2.style.width = "50%";
container.append(preElement2);
reactDOM.render(playele2, preElement2);
let headingWrapper = document.createElement("div");
section.append(headingWrapper);
let heading = interHeading();
reactDOM.render(heading, headingWrapper);
let songHeader = document.createElement("div");
songHeader.className = "main-trackList-trackListHeaderRow";
songHeader.style.height = "100%"
songHeader.style.background = "var(--spice-main)";
songHeader.style.borderBottom = "1px solid rgba(255,255,255,.1)";
songHeader.style.marginBottom = "8px";
songHeader.style.position = "sticky";
songHeader.style.top = "64px";
songHeader.style.zIndex = "2";
section.append(songHeader);
const trackRowHeader = await songRowHeader();
reactDOM.render(trackRowHeader, songHeader);
renderIntersect();
}
async function renderIntersect() {
let section = document.querySelector(`[data-testid="playlist-page"]`);
let songContainer;
try {
songContainer = document.querySelector(".interSongContainer");
songContainer.innerHTML = "";
} catch {
songContainer = document.createElement("div");
songContainer.className = "interSongContainer";
section.append(songContainer);
}
let heading1 = document.querySelector(".main-type-bass.main-trackList-trackListHeaderRow");
if (commonTracks.length == 1) {
heading1.innerText = `${commonTracks.length} song appears in both of the playlists: `;
} else {
heading1.innerText = `${commonTracks.length} songs appear in both of the playlists: `;
}
//rendering songs
let i = 2;
renderedTracks = commonTracks;
for (const track of commonTracks) {
let preElement = document.createElement("div");
songContainer.append(preElement);
const trackRow = songRow(track, i);
reactDOM.render(trackRow, preElement);
i += 1;
}
}
async function exceptIntersect() {
const toDisplay = LocalStorage.get("spicetify-interorder");
let tracks = [];
let isOne, meta;
if (toDisplay == "1") {
tracks = (await Spicetify.Platform.PlaylistAPI.getContents(play1)).items;
meta = await Spicetify.Platform.PlaylistAPI.getMetadata(play1);
for (var i = 0; i < tracks.length; i++) {
tracks[i].index1 = i;
}
isOne = true;
} else {
tracks = (await Spicetify.Platform.PlaylistAPI.getContents(play2)).items;
meta = await Spicetify.Platform.PlaylistAPI.getMetadata(play2);
for (var i = 0; i < tracks.length; i++) {
tracks[i].index2 = i;
}
isOne = false;
}
for (const track of commonTracks) {
isOne ? delete tracks[track.index1] : delete tracks[track.index2];
}
let heading = document.querySelector(".main-type-bass.main-trackList-trackListHeaderRow");
if (tracks.length == 1) {
heading.innerText = `${tracks.length} song exists only in ${meta.name}`;
} else {
heading.innerText = `${tracks.length - commonTracks.length} songs exist only in ${meta.name}`;
}
let container = document.querySelector(".interSongContainer");
container.innerHTML = "";
renderedTracks = tracks;
let j = 2;
for (const track of tracks) {
if (!track) {
continue;
}
let preElement = document.createElement("div");
container.append(preElement);
const trackRow = songRow(track, j, toDisplay);
reactDOM.render(trackRow, preElement);
j += 1;
}
}
async function convertToPlaylist() {
let songUris = renderedTracks
.filter(track => track) // Ensure no null tracks
.map(track => track.uri);
let playname;
if (LocalStorage.get("spicetify-intermode") == "Intersect") {
playname = `${name1} ∩ ${name2}`;
} else {
if (LocalStorage.get("spicetify-interorder") == "1") {
playname = `${name1} ∅ ${name2}`;
} else {
playname = `${name2} ∅ ${name1}`;
}
}
const response = await Spicetify.CosmosAsync.post(
"https://api.spotify.com/v1/me/playlists", {
name: playname,
public: false
}
);
if (!response || !response.id) {
Spicetify.showNotification("Failed to create playlist.");
return;
}
const uri = response.id
await delay(100);
while (songUris.length) {
const b = songUris.splice(0, 100);
Spicetify.CosmosAsync.post("https://api.spotify.com/v1/playlists/" + uri + "/tracks", {
uris: b,
});
}
Spicetify.showNotification(`Succesfully created playlist ${playname}`);
}
const clearSelection = new Spicetify.Menu.Item("Clear Selection from Intersection", false, (self) => {
LocalStorage.remove("spicetify-interplaylist1");
self.deregister();
});
new Spicetify.ContextMenu.Item(
"Select for Intersection",
(uris) => {
LocalStorage.set("spicetify-interplaylist1", uris[0]);
clearSelection.register();
},
(uris) => {
if (uris.length > 1) {
return false;
}
if (LocalStorage.get("spicetify-interplaylist1")) {
return false;
}
const uriObj = Spicetify.URI.fromString(uris[0]);
if (uriObj.type == Spicetify.URI.Type.PLAYLIST || uriObj.type == Spicetify.URI.Type.PLAYLIST_V2) {
return true;
}
return false;
},
"copy"
).register();
new Spicetify.ContextMenu.Item(
"Compare with Selected Playlist",
(uris) => {
LocalStorage.set("spicetify-interplaylist2", uris[0]);
clearSelection.deregister();
intersect();
},
(uris) => {
if (uris.length > 1) {
return false;
}
if (!LocalStorage.get("spicetify-interplaylist1")) {
return false;
}
if (uris[0] == LocalStorage.get("spicetify-interplaylist1")) {
return false;
}
const uriObj = Spicetify.URI.fromString(uris[0]);
if (uriObj.type == Spicetify.URI.Type.PLAYLIST || uriObj.type == Spicetify.URI.Type.PLAYLIST_V2) {
return true;
}
return false;
},
"copy"
).register();
})();
================================================
FILE: skipStats/README.md
================================================
# skipStats
Filename : `skipStats.js`

Extension to track your skips!
- Tracks your skips when listening to playlists or albums!
- Displays the data in a readable manner
- Auto-skip songs over a certain value of skips
## To use:
- To see the skips in current playlist/album: Click on the profile and select "See Skips for current playlist/album".

- Right Click on a playlist or album, and click "See Skip Stats".

## Config
- Set the auto skip limit: Go to profile menu and click on "Auto-Skip".

- Reset Stats: Go to profile menu and click on the option accordingly.
## Install
Copy `skipStats.js` into your [Spicetify](https://github.com/khanhas/spicetify-cli) extensions directory:
| **Platform** | **Path** |
|------------|-----------------------------------------------------------------------------------|
| **Linux** | `~/.config/spicetify/Extensions` or `$XDG_CONFIG_HOME/.config/spicetify/Extensions/` |
| **MacOS** | `~/.config/spicetify/Extensions` or `$SPICETIFY_CONFIG/Extensions` |
| **Windows** | `%userprofile%\.spicetify\Extensions\` |
After putting the extension file into the correct folder, run the following command to install the extension:
```
spicetify config extensions skipStats.js
spicetify apply
```
Note: Using the `config` command to add the extension will always append the file name to the existing extensions list. It does not replace the whole key's value.
Or you can manually edit your `config-xpui.ini` file. Add your desired extension filenames in the extensions key, separated them by the | character.
Example:
```ini
[AdditionalOptions]
...
extensions = autoSkipExplicit.js|shuffle+.js|trashbin.js|skipStats.js
```
🌟 Like it? Gimme some love!
[](https://github.com/huhridge/huh-spicetify-extensions/)
================================================
FILE: skipStats/skipStats.js
================================================
// @ts-check
// NAME: Skip Stats
// AUTHOR: huhridge
// DESCRIPTION: Tracks skipping stats in playlists and albums.
//TODO: Add liked songs.
///
(function skipStats() {
if (!Spicetify.React || !Spicetify.ReactDOM || !Spicetify.Platform) {
setTimeout(skipStats, 200);
return;
}
const { LocalStorage, Platform, ReactDOM: reactDOM, React: react } = Spicetify;
let progress;
if (!Spicetify.LocalStorage.get("autoSkipThreshold")) {
Spicetify.LocalStorage.set("autoSkipThreshold", "0");
}
if (!LocalStorage.get("skipData")) {
LocalStorage.set("skipData", JSON.stringify({}));
}
setInterval(() => {
progress = Spicetify.Player.getProgressPercent();
}, 500);
Spicetify.Player.addEventListener("songchange", trackSkips);
async function trackSkips() {
if (progress < 0.95) {
await delay(200);
//@ts-ignore
let song_key = Spicetify.Queue.prevTracks.slice(-1)[0].contextTrack.uri;
let skipData = JSON.parse(LocalStorage.get("skipData"));
if (!skipData[song_key]) {
skipData[song_key] = 1;
} else {
skipData[song_key] += 1;
}
LocalStorage.set("skipData", JSON.stringify(skipData));
}
//auto skip
let skipData = JSON.parse(LocalStorage.get("skipData"));
let thresh = Number(Spicetify.LocalStorage.get("autoSkipThreshold"));
if (thresh > 0 && skipData[Spicetify.Player.data.item.uri] >= thresh) {
Spicetify.Player.next();
Spicetify.showNotification("The track was auto-skipped due to being skipped too many times.");
}
LocalStorage.set("skipData", JSON.stringify(skipData));
}
function resetSkips(mode, uri = "") {
if (mode === "all") {
LocalStorage.set("skipData", JSON.stringify({}));
Spicetify.showNotification("Resetted all skip data!");
} else if (mode === "current") {
let skipData = JSON.parse(LocalStorage.get("skipData"));
skipData[Spicetify.Player.data.item.uri] = 0;
LocalStorage.set("skipData", JSON.stringify(skipData));
Spicetify.showNotification("Resetted skip data for current track!");
} else if (mode === "context") {
let skipData = JSON.parse(LocalStorage.get("skipData"));
skipData[uri] = 0;
LocalStorage.set("skipData", JSON.stringify(skipData));
Spicetify.showNotification("Resetted skip data for selected track!");
}
}
async function seeStats(uri) {
const uriObj = Spicetify.URI.fromString(uri);
let tracks;
switch (uriObj.type) {
case Spicetify.URI.Type.PLAYLIST:
case Spicetify.URI.Type.PLAYLIST_V2:
tracks = await fetchPlaylist(uri);
break;
case Spicetify.URI.Type.ALBUM:
tracks = await fetchAlbum(uri);
break;
case Spicetify.URI.Type.COLLECTION:
tracks = await fetchCollection();
break;
}
let skipData = JSON.parse(LocalStorage.get("skipData"));
tracks = tracks.filter((item) => Boolean(skipData[item.uri]));
if (tracks.length == 0) {
Spicetify.showNotification("No Skipping Data found!");
return;
}
tracks.forEach((item) => {
item.skips = skipData[item.uri];
});
tracks.sort((a, b) => b.skips - a.skips);
const skipTable = statTable({ tracks: tracks });
// @ts-ignore
Spicetify.PopupModal.display({ title: "SkipStats", content: skipTable, isLarge: true });
}
function delay(delayInms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, delayInms);
});
}
const fetchPlaylist = async (uri) => {
const res = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri}/rows`);
return res.rows.map((item, index) => ({
uri: item.link,
title: item.name,
index: index + 1,
album: item.album.name,
artists: item.artists.map((item) => item.name).join(", "),
}));
};
const fetchCollection = async () => {
const res = await Spicetify.CosmosAsync.get("sp://core-collection/unstable/@/list/tracks/all?responseFormat=protobufJson");
return res.item.map((item) => ({
uri: item.trackMetadata.link,
title: item.trackMetadata.name,
index: item.index + 1,
album: item.trackMetadata.album.name,
artists: item.trackMetadata.artist.map((item) => item.name).join(", "),
}));
};
const fetchAlbum = async (uri) => {
const arg = uri.split(":")[2];
const res = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/albums/${arg}`);
return res.tracks.items.map((item) => ({
uri: item.uri,
title: item.name,
index: item.track_number,
album: res.name,
artists: item.artists.map((item) => item.name).join(", "),
}));
};
const statTable = ({ tracks }) => {
const headers = ["Title", "Album", "Artists", "Skips"];
let thresh = Number(Spicetify.LocalStorage.get("autoSkipThreshold"));
let isThresh = Boolean(thresh);
const style = react.createElement("style", {
dangerouslySetInnerHTML: {
__html: `
div[aria-label="SkipStats"] > div {
min-width: max-content;
}
.styled-table {
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
border-radius: 8px;
}
.styled-table thead tr {
background-color: var(--spice-sidebar);
color: var(--spice-sidebar-text);
text-align: left;
border-bottom: thin solid #dddddd;
}
tbody td:nth-child(4) {
text-align: center;
}
.styled-table th,
.styled-table td {
padding: 12px 15px;
width: 1%;
white-space: nowrap;
}
.styled-table tbody tr {
border-bottom: thin solid #dddddd;
}
td.auto-skip {
color: var(--spice-button-active);
}
`,
},
});
return react.createElement(
"table",
{ className: "styled-table" },
style,
react.createElement(
"thead",
null,
react.createElement(
"tr",
null,
headers.map((item) => react.createElement("th", null, item))
)
),
react.createElement(
"tbody",
null,
tracks.map((track) =>
react.createElement(
"tr",
{
"data-id": track.uri,
},
// react.createElement("td", null, track.index),
react.createElement("td", null, track.title),
react.createElement("td", null, track.album),
react.createElement("td", null, track.artists),
react.createElement(
"td",
{
className: isThresh && track.skips >= thresh ? "auto-skip" : "",
},
track.skips
)
)
)
)
);
};
const Config = ({ name, lkey }) => {
const [value, setValue] = react.useState(Spicetify.LocalStorage.get(lkey));
const setValueCallback = react.useCallback(
(event) => {
const value = event.target.value;
setValue(value);
Spicetify.LocalStorage.set(lkey, value);
},
[value]
);
const style = react.createElement("style", {
dangerouslySetInnerHTML: {
__html: `
.setting-row::after {
content: "";
display: table;
clear: both;
}
.setting-row .col {
padding: 16px 0 4px;
align-items: center;
}
.setting-row .col.description {
float: left;
padding-right: 15px;
cursor: default;
}
.setting-row .col.action {
float: right;
display: flex;
justify-content: flex-end;
align-items: center;
}
.col.action input {
width: 100%;
margin-top: 10px;
padding: 0 5px;
height: 32px;
border: 0;
color: var(--spice-text);
background-color: initial;
border-bottom: 1px solid var(--spice-text);
}
`,
},
});
return react.createElement(
"div",
{
className: "skip-stats-config-container",
},
style,
react.createElement(
"div",
{
className: "setting-row",
},
react.createElement(
"label",
{
className: "col description",
},
name
),
react.createElement(
"div",
{
className: "col action",
},
react.createElement("input", {
type: "number",
value,
onChange: setValueCallback,
})
)
)
);
};
const currentSkips = new Spicetify.Menu.Item("See Skips for current playlist/album", false, async () => {
const uriObj = Spicetify.URI.fromString(Spicetify.Player.data.context_uri);
switch (uriObj.type) {
case Spicetify.URI.Type.PLAYLIST:
case Spicetify.URI.Type.PLAYLIST_V2:
case Spicetify.URI.Type.ALBUM:
case Spicetify.URI.Type.COLLECTION:
await seeStats(Spicetify.Player.data.context_uri);
break;
default:
throw Spicetify.showNotification("Unsupported context type! Please use for a playlist or an album only");
}
});
const autoSkip = new Spicetify.Menu.Item("Auto-Skip", false, () => {
Spicetify.PopupModal.display({
title: "Auto-Skip Threshold",
content: react.createElement(Config, { name: "Auto-Skip after this many skips (0 for off)", lkey: "autoSkipThreshold" }),
});
});
const resetStatsAll = new Spicetify.Menu.Item("Reset all stats", false, () => {
resetSkips("all");
});
const resetStatsCurrent = new Spicetify.Menu.Item("Reset stats for current track", false, () => {
resetSkips("current");
});
new Spicetify.Menu.SubMenu("skipStats", [currentSkips, autoSkip, resetStatsAll, resetStatsCurrent]).register();
new Spicetify.ContextMenu.Item(
"See Skip Stats",
async (uris) => {
await seeStats(uris[0]);
},
(uris) => {
if (uris.length > 1) {
return false;
}
const uriObj = Spicetify.URI.fromString(uris[0]);
switch (uriObj.type) {
case Spicetify.URI.Type.PLAYLIST:
case Spicetify.URI.Type.PLAYLIST_V2:
case Spicetify.URI.Type.ALBUM:
// case Spicetify.URI.Type.COLLECTION:
return true;
}
return false;
},
"skip-forward"
).register();
new Spicetify.ContextMenu.Item(
"Reset Skips for this track",
(uris) => {
resetSkips("context", uris[0]);
},
(uris) => {
if (uris.length != 1) return false;
return Spicetify.URI.fromString(uris[0]).type == Spicetify.URI.Type.TRACK;
}
).register();
})();