Skip to content

Instantly share code, notes, and snippets.

@andraaspar
Last active March 16, 2023 15:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andraaspar/ac57cafd9a0d772d6c15d29b0f7a965e to your computer and use it in GitHub Desktop.
Save andraaspar/ac57cafd9a0d772d6c15d29b0f7a965e to your computer and use it in GitHub Desktop.
Spotify ratings
// ==UserScript==
// @name Spotify ratings
// @namespace 50edea63-8324-74b3-4019-81e0bdcd316a
// @version 0.2.0
// @description Loads my ratings for an album.
// @author AP
// @match https://open.spotify.com/*
// @run-at document-idle
// ==/UserScript==
(() => {
/** @type {typeof document['createElement']} */
const $ = document.createElement.bind(document);
const ratingFileNames = [
"albums-0-of-3.json",
"albums-1-of-3.json",
"albums-2-of-3.json",
"albums-3-of-3.json",
];
let ratingLists = [];
// let timeoutId;
const style = $("style");
style.innerHTML = `
.pir-rating {
color:#bf0000;
}
.pir-buttons {
display: flex !important;
flex-flow: row wrap;
padding: 1px;
gap: 1px;
}
.pir-buttons * {
font-size: 14px;
line-height: 1;
}
.pir-buttons > a, .pir-buttons > button {
display: flex;
flex-flow: row wrap;
align-items: center;
background: black;
color: white;
border: 1px solid gray;
padding: 0 1em;
cursor: pointer;
}
.pir-buttons input {
background: black;
color: white;
border: 1px solid gray;
border-radius: 3px;
}
`;
document.head.append(style);
const buttons = $("div");
buttons.className = "pir-buttons";
document.body.prepend(buttons);
const reloadRatingsButton = $("button");
buttons.append(reloadRatingsButton);
reloadRatingsButton.innerText = `Reload Ratings`;
reloadRatingsButton.addEventListener("click", async (e) => {
try {
// clearTimeout(timeoutId);
for (const existingRating of document.querySelectorAll(`.pir-rating`)) {
existingRating.remove();
}
for (const ratedItem of document.querySelectorAll(
`[data-pir-rating-for]`
)) {
delete ratedItem.dataset.pirRatingFor;
}
const res = await fetch(
`https://api.github.com/gists/9c9728852c2a187b8187b6753c12210e`
);
if (!res.ok) {
throw new Error(`Could not load ratings!`);
}
let info = await res.json();
const responses = await Promise.all(
ratingFileNames.map((ratingFileName, i) =>
fetch(info.files[ratingFileName].raw_url)
)
);
const lists = [];
for (const response of responses) {
if (response.ok) {
lists.push(await response.json());
} else {
lists.push([]);
}
}
ratingLists = lists;
showRatings();
} catch (e) {
window.alert(e);
}
});
const updateRatingsButton = $("button");
buttons.append(updateRatingsButton);
updateRatingsButton.innerText = `Update Ratings`;
updateRatingsButton.addEventListener("click", async (e) => {
try {
showRatings();
} catch (e) {
window.alert(e);
}
});
const copyAlbumJsonButton = $("button");
buttons.append(copyAlbumJsonButton);
copyAlbumJsonButton.innerText = `Copy Album JSON`;
copyAlbumJsonButton.addEventListener("click", async (e) => {
const albumInfo = await getAlbumInfoFromAlbumPage(location.href);
navigator.clipboard.writeText(
`\n` + indent(JSON.stringify(albumInfo, undefined, "\t")) + `,`
);
});
const copySongJsonButton = $("button");
buttons.append(copySongJsonButton);
copySongJsonButton.innerText = `Copy Song JSON`;
copySongJsonButton.addEventListener("click", async (e) => {
try {
const albumHref = document.querySelector(
`a[data-testid="context-link"]`
).href;
if (typeof albumHref !== "string") {
throw new Error(`Album link not found!`);
}
const albumInfo = await getAlbumInfoFromAlbumPage(albumHref);
navigator.clipboard.writeText(
`\n` +
indent(
JSON.stringify(
{
...albumInfo,
trackTitle: document.querySelector(
`a[data-testid="context-item-link"]`
).innerText,
},
undefined,
"\t"
)
) +
`,`
);
} catch (e) {
window.alert(e);
}
});
const dashboardLink = $("a");
dashboardLink.href = `https://developer.spotify.com/dashboard/`;
dashboardLink.target = `_blank`;
buttons.append(dashboardLink);
dashboardLink.innerText = "Spotify Developer Dashboard";
const clientIdInputLabel = $("label");
buttons.append(clientIdInputLabel);
clientIdInputLabel.innerText = " Client ID: ";
const clientIdInput = $("input");
clientIdInputLabel.append(clientIdInput);
clientIdInput.size = 32;
clientIdInput.value = localStorage["rrm9yz"] ?? "";
clientIdInput.addEventListener("input", (e) => {
localStorage["rrm9yz"] = e.currentTarget.value;
});
const clientSecretInputLabel = $("label");
buttons.append(clientSecretInputLabel);
clientSecretInputLabel.innerText = " Client Secret: ";
const clientSecretInput = $("input");
clientSecretInputLabel.append(clientSecretInput);
clientSecretInput.size = 32;
clientSecretInput.type = "password";
clientSecretInput.value = localStorage["rrma0u"] ?? "";
clientSecretInput.addEventListener("input", (e) => {
localStorage["rrma0u"] = e.currentTarget.value;
});
async function showRatings() {
if (!ratingLists.length) return;
/** @type {NodeListOf<HTMLElement>} **/
const albumTitles = document.querySelectorAll(
`a[href^="/album/"]:not([data-pir-rating-for])`
);
let token = "";
try {
token = await oauth();
} catch (e) {
console.error(e);
}
for (const albumTitle of albumTitles) {
if (!document.contains(albumTitle)) break;
const albumHref = albumTitle.href;
const albumInfo = await (token
? getAlbumInfoFromApi(token, albumHref)
: getAlbumInfoFromAlbumPage(albumHref));
let rating = `???`;
let ratingJson = `{}`;
let ratingListIndex = -1;
findRating: for (const ratingList of ratingLists) {
ratingListIndex++;
for (const ratedAlbumInfo of ratingList) {
if (
ratedAlbumInfo.albumTitle === albumInfo.albumTitle &&
ratedAlbumInfo.artistName === albumInfo.artistName
) {
ratingJson = JSON.stringify(ratedAlbumInfo);
rating = "";
for (let j = 0; j < ratingListIndex; j++) rating += "&#x2605;";
while (rating.length < 8 * 3) rating += "&#x2606;";
break findRating;
}
}
}
const ratingElem = $("span");
ratingElem.className = "pir-rating";
ratingElem.innerHTML = "&nbsp;" + rating;
albumTitle.append(ratingElem);
albumTitle.dataset.pirRatingFor = ratingJson;
}
// timeoutId = setTimeout(showRatings, 2000);
}
/**
* @param {string} albumHref
* @returns {{artistName:string,albumTitle:string}}
*/
async function getAlbumInfoFromAlbumPage(albumHref) {
const albumRes = await fetch(albumHref);
if (!albumRes.ok) {
throw new Error(`Could not load album info!`);
}
const albumHtml = await albumRes.text();
const artistLink = albumHtml.replace(
/.*<meta name="music:musician" content="(.*?)".*/,
"$1"
);
const artistRes = await fetch(artistLink);
if (!artistRes.ok) {
throw new Error(`Could not load artist info!`);
}
const artistHtml = await artistRes.text();
return {
artistName: JSON.parse(
artistHtml.replace(
/.*<script type="application\/ld\+json">(.*?)<\/script>.*/is,
"$1"
)
).name,
albumTitle: JSON.parse(
albumHtml.replace(
/.*<script type="application\/ld\+json">(.*?)<\/script>.*/is,
"$1"
)
).name,
};
}
/**
* @returns {Promise<string>}
*/
async function oauth() {
const data = new URLSearchParams();
data.append("grant_type", "client_credentials");
const res = await fetch(`https://accounts.spotify.com/api/token`, {
method: "POST",
headers: {
Authorization:
"Basic " +
btoa(localStorage["rrm9yz"] + ":" + localStorage["rrma0u"]),
"Content-Type": "application/x-www-form-urlencoded",
},
body: data,
});
if (!res.ok) throw new Error(`[rrm8el] Could not authenticate.`);
const response = await res.json();
return response.access_token;
}
/**
* @param {string} token
* @param {string} albumHref
* @returns {{artistName:string,albumTitle:string}}
*/
async function getAlbumInfoFromApi(token, albumHref) {
const id = new URL(albumHref).pathname.replace(/.*\//, "");
const res = await fetch(
`https://api.spotify.com/v1/albums/${encodeURIComponent(id)}`,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
);
if (!res.ok)
throw new Error(
`[rrm8w1] Could not load album info for href: ${albumHref}`
);
const album = await res.json();
return {
artistName: album.artists.reduce(
(all, it) => (all ? all + ", " + it.name : it.name),
""
),
albumTitle: album.name,
};
}
/**
* @param {string} s
* @returns {string}
*/
function indent(s) {
return s.replace(/^/gm, "\t");
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment