// START METADATA
// NAME: Trashbin
// AUTHOR: khanhas
// DESCRIPTION: Throw songs to trashbin and never hear it again.
// END METADATA
///
(function TrashBin() {
/**
* By default, trash songs list is saved in Spicetify.LocalStorage but
* everything will be cleaned if Spotify is uninstalled. So instead
* of collecting trash songs again, you can use JsonBin service to
* store your list, which is totally free and fast. Go to website
* https://jsonbin.io/ , create a blank json:
{}
* and hit Create. After that, it will generate an
* Access URL, hit Copy and
* paste it to constant jsonBinURL below. URL should look like this:
//api.jsonbin.io/b/XXXXXXXXXXXXXXXXXXXX
*/
const jsonBinURL = "";
let trashSongList = {};
let trashArtistList = {};
let userHitBack = false;
let trashIcon = {};
let baseArtistUri = "artist_uri";
let banSong = () => {
return;
};
const THROW_TEXT = "Throw To Trashbin";
const UNTHROW_TEXT = "Take Out Of Trashbin";
const TRASH_BUTTON = ``;
const TRASH_ARTIST_BUTTON = `
`;
function init() {
if (
!Spicetify.Player.data ||
!Spicetify.Player ||
(!jsonBinURL && !Spicetify.LocalStorage)
) {
setTimeout(init, 1000);
return;
}
if (jsonBinURL) {
$.ajax({
url: `https:${jsonBinURL}/latest`,
method: "GET",
success: (data) => {
const oldFormat = dataIsInOldFormat(data);
if (oldFormat) {
trashSongList = migrateDataToNewFormat(
data["TrashSongList"]
);
trashArtistList = migrateDataToNewFormat(
data["TrashArtistList"]
);
} else {
trashSongList = data["trashSongList"];
trashArtistList = data["trashArtistList"];
}
if (oldFormat || !trashSongList || !trashArtistList) {
trashSongList = trashSongList || {};
trashArtistList = trashArtistList || {};
$.ajax({
url: `https:${jsonBinURL}`,
method: "PUT",
contentType: "application/json",
data: JSON.stringify({
trashSongList,
trashArtistList,
}),
error: (err) => {
console.error(err);
},
});
}
},
error: (err) => {
console.error(err);
},
});
} else {
trashSongList = JSON.parse(
Spicetify.LocalStorage.get("TrashSongList")
);
trashArtistList = JSON.parse(
Spicetify.LocalStorage.get("TrashArtistList")
);
if (dataIsInOldFormat(trashSongList))
trashSongList = migrateDataToNewFormat(trashSongList);
if (dataIsInOldFormat(trashArtistList))
trashArtistList = migrateDataToNewFormat(trashArtistList);
if (!trashSongList) {
Spicetify.LocalStorage.set("TrashSongList", "{}");
trashSongList = {};
}
if (!trashArtistList) {
Spicetify.LocalStorage.set("TrashArtistList", "{}");
trashArtistList = {};
}
}
$(".track-text-item").append(TRASH_BUTTON);
trashIcon = $("#trashbin-icon");
trashIcon.on("click", () => {
banSong();
if (!trashSongList[Spicetify.Player.data.track.uri]) {
trashSongList[Spicetify.Player.data.track.uri] = true;
Spicetify.Player.next();
} else {
delete trashSongList[Spicetify.Player.data.track.uri];
}
updateIconColor();
storeList();
});
// Tracking when users hit previous button.
// By doing that, user can return to threw song to take it out of trashbin.
$("#player-button-previous").on("click", () => {
userHitBack = true;
});
updateIconPosition();
updateIconColor();
Spicetify.Player.addEventListener("songchange", watchChange);
watchArtistPage();
}
//Observe artist page
function watchArtistPage() {
const target = $("iframe#app-artist");
if (!target.length) {
setTimeout(watchArtistPage, 1000);
return;
}
function appendArtistTrashbin() {
const headers = $("iframe#app-artist")
.contents()
.find(".glue-page-header__buttons");
if (!headers.length) {
setTimeout(appendArtistTrashbin, 100);
return;
}
const artistUri = `spotify:artist:${
$("iframe#app-artist")
.attr("data-app-uri")
.split(":")[3]
}`;
const throwButton = headers.find(".throw-artist");
if (!throwButton.length) {
headers.each(function() {
$(this).append(TRASH_ARTIST_BUTTON);
const trashButton = $(this).find(".throw-artist");
trashButton.attr("artist-uri", artistUri);
trashButton.on("click", () => {
if (!trashArtistList[artistUri]) {
trashArtistList[artistUri] = true;
} else {
delete trashArtistList[artistUri];
}
storeList();
updateIconColor_Artist();
});
});
updateIconColor_Artist();
} else if (throwButton.attr("artist-uri") !== artistUri) {
setTimeout(appendArtistTrashbin, 100);
return;
}
}
appendArtistTrashbin();
const artistObserver = new MutationObserver(appendArtistTrashbin);
artistObserver.observe(target[0], {
attributes: true,
attributeFilter: ["data-app-uri"],
});
}
function storeList() {
if (jsonBinURL) {
$.ajax({
url: `https:${jsonBinURL}`,
method: "PUT",
contentType: "application/json",
data: JSON.stringify({ trashSongList, trashArtistList }),
error: (err) => {
console.error(err);
},
});
} else {
Spicetify.LocalStorage.set(
"TrashSongList",
JSON.stringify(trashSongList)
);
Spicetify.LocalStorage.set(
"TrashArtistList",
JSON.stringify(trashArtistList)
);
}
}
function watchChange() {
updateIconPosition();
updateIconColor();
if (userHitBack) {
userHitBack = false;
return;
}
if (trashSongList[Spicetify.Player.data.track.uri]) {
Spicetify.Player.next();
return;
}
let uriIndex = 0;
let artistUri = Spicetify.Player.data.track.metadata[baseArtistUri];
while (artistUri) {
if (trashArtistList[artistUri]) {
Spicetify.Player.next();
return;
}
uriIndex++;
artistUri =
Spicetify.Player.data.track.metadata[
baseArtistUri + ":" + uriIndex
];
}
}
// Change trash icon position based on playlist context
// In normal playlists, track-text-item has one icon and its padding-left
// is 32px, just enough for one icon. By appending two-icons class, its
// padding-left is expanded to 64px.
// In Discovery Weekly playlist, track-text-item has two icons: heart and ban.
// Ban functionality is the kind of the same as our so instead of crowding
// that tiny zone with 3 icons, I hide Spotify's Ban button and replace it with
// trash icon. Nevertheless, I still activate Ban button context menu whenever
// user clicks at trash icon.
function updateIconPosition() {
const trackContainer = $(".track-text-item");
if (!trackContainer.hasClass("two-icons")) {
trackContainer.addClass("two-icons");
trashIcon.css("right", "24px");
return;
}
const banButton = $(".track-text-item .nowplaying-ban-button");
if (banButton.css("display") !== "none") {
banButton.css("visibility", "hidden");
trashIcon.css("right", "0px");
banSong = () => banButton.trigger("click");
} else {
banSong = () => {
return;
};
}
}
function updateIconColor() {
if (Spicetify.Player.data.track.metadata["is_advertisement"] === "true") {
trashIcon.attr("disabled", true);
return;
}
trashIcon.removeAttr("disabled");
if (trashSongList[Spicetify.Player.data.track.uri]) {
trashIcon.addClass("active");
trashIcon.attr("data-tooltip-text", UNTHROW_TEXT);
} else {
trashIcon.removeClass("active");
trashIcon.attr("data-tooltip-text", THROW_TEXT);
}
}
function updateIconColor_Artist() {
const buttons = $("iframe#app-artist")
.contents()
.find(".throw-artist");
buttons.each(function() {
const inner = $(this).find("button");
if (trashArtistList[$(this).attr("artist-uri")]) {
inner.addClass("contextmenu-active");
inner.attr("data-tooltip", UNTHROW_TEXT);
} else {
inner.removeClass("contextmenu-active");
inner.attr("data-tooltip", THROW_TEXT);
}
});
}
function dataIsInOldFormat(data) {
if (!data) {
return false;
}
return (
Array.isArray(data) ||
(!Array.isArray(data) &&
(!!data["TrashSongList"] || !!data["TrashArtistList"]))
);
}
function migrateDataToNewFormat(data) {
const newDataFormat = {};
if (data && data.length)
data.forEach((item) => (newDataFormat[item] = true));
return newDataFormat;
}
init();
})();