Yeet gifs and use pngs
|
|
@ -73,48 +73,48 @@
|
|||
<div class="button-page">
|
||||
<main class="button-wall" aria-label="88x31 buttons">
|
||||
<!-- coding / web -->
|
||||
<a href="https://doughmination.co.uk" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/doughmination.gif" alt="Doughmination" loading="eager"></a>
|
||||
<a href="https://doughmination.co.uk" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/doughmination.png" alt="Doughmination" loading="eager"></a>
|
||||
<a href="https://git.gay/doughmination" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/gitgay.png" alt="Git Gay" loading="lazy"></a>
|
||||
<a href="https://code.visualstudio.com" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/vscbutton.gif" alt="Made with Visual Studio Code" loading="lazy"></a>
|
||||
<img src="/assets/88x31/htmldream.gif" alt="I dream in HTML" loading="lazy">
|
||||
<a href="https://validator.w3.org/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/valid-html5.gif" alt="Valid HTML5" loading="lazy"></a>
|
||||
<a href="https://jigsaw.w3.org/css-validator/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/valid-css.gif" alt="Valid CSS" loading="lazy"></a>
|
||||
<a href="https://yesterweb.org/no-to-web3/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/noweb32.gif" alt="Keep the web free" loading="lazy"></a>
|
||||
<img src="/assets/88x31/nft.gif" alt="No NFTs, no thanks" loading="lazy">
|
||||
<img src="/assets/88x31/nowebp.gif" alt="No WEBp" loading="lazy">
|
||||
<a href="https://code.visualstudio.com" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/vscbutton.png" alt="Made with Visual Studio Code" loading="lazy"></a>
|
||||
<img src="/assets/88x31/htmldream.png" alt="I dream in HTML" loading="lazy">
|
||||
<a href="https://validator.w3.org/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/valid-html5.png" alt="Valid HTML5" loading="lazy"></a>
|
||||
<a href="https://jigsaw.w3.org/css-validator/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/valid-css.png" alt="Valid CSS" loading="lazy"></a>
|
||||
<a href="https://yesterweb.org/no-to-web3/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/noweb32.png" alt="Keep the web free" loading="lazy"></a>
|
||||
<img src="/assets/88x31/nft.png" alt="No NFTs, no thanks" loading="lazy">
|
||||
<img src="/assets/88x31/nowebp.png" alt="No WEBp" loading="lazy">
|
||||
<!-- software / os -->
|
||||
<a href="https://www.linux.org/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/linux.gif" alt="Made on GNU/Linux" loading="lazy"></a>
|
||||
<a href="https://www.mozilla.org/firefox/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/firefox.gif" alt="Firefox" loading="lazy"></a>
|
||||
<img src="/assets/88x31/no-chrome.gif" alt="Anything but Chrome" loading="lazy">
|
||||
<a href="https://support.apple.com/en-gb/121552" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/macbutton.gif" alt="Made on a Mac" loading="lazy"></a>
|
||||
<a href="https://www.win-rar.com/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/winrar4.gif" alt="WinRAR" loading="lazy"></a>
|
||||
<a href="https://microslop.com/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/microslop.gif" alt="Stop Microsoft" loading="lazy"></a>
|
||||
<img src="/assets/88x31/dark-mode.gif" alt="Made for dark mode" loading="lazy">
|
||||
<a href="https://www.linux.org/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/linux.png" alt="Made on GNU/Linux" loading="lazy"></a>
|
||||
<a href="https://www.mozilla.org/firefox/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/firefox.png" alt="Firefox" loading="lazy"></a>
|
||||
<img src="/assets/88x31/no-chrome.png" alt="Anything but Chrome" loading="lazy">
|
||||
<a href="https://support.apple.com/en-gb/121552" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/macbutton.png" alt="Made on a Mac" loading="lazy"></a>
|
||||
<a href="https://www.win-rar.com/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/winrar4.png" alt="WinRAR" loading="lazy"></a>
|
||||
<a href="https://microslop.com/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/microslop.png" alt="Stop Microsoft" loading="lazy"></a>
|
||||
<img src="/assets/88x31/dark-mode.png" alt="Made for dark mode" loading="lazy">
|
||||
<!-- pride / identity -->
|
||||
<a href="https://valerie.vg/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/estrogen.gif" alt="Powered by estrogen" loading="lazy"></a>
|
||||
<img src="/assets/88x31/transnow.gif" alt="Trans rights now" loading="lazy">
|
||||
<img src="/assets/88x31/queerpride.gif" alt="Queer pride" loading="lazy">
|
||||
<a href="https://valerie.vg/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/estrogen.png" alt="Powered by estrogen" loading="lazy"></a>
|
||||
<img src="/assets/88x31/transnow.png" alt="Trans rights now" loading="lazy">
|
||||
<img src="/assets/88x31/queerpride.png" alt="Queer pride" loading="lazy">
|
||||
<img src="/assets/88x31/girlsnow.png" alt="Girls Now" loading="lazy">
|
||||
<img src="/assets/88x31/skirt.gif" alt="Let boys wear skirts" loading="lazy">
|
||||
<img src="/assets/88x31/cutesocks.gif" alt="I wear cute socks!" loading="lazy">
|
||||
<img src="/assets/88x31/skirt.png" alt="Let boys wear skirts" loading="lazy">
|
||||
<img src="/assets/88x31/cutesocks.png" alt="I wear cute socks!" loading="lazy">
|
||||
<!-- causes -->
|
||||
<a href="https://archive.org/details/zines-anti-fascism" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/antifa.gif" alt="No fascism, no bigotry" loading="lazy"></a>
|
||||
<a href="https://www.youtube.com/watch?v=7AQbhes-Ntw" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/meltice.gif" alt="Melt ICE" loading="lazy"></a>
|
||||
<a href="https://www.map.org.uk/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/palestine.gif" alt="Free Palestine" loading="lazy"></a>
|
||||
<a href="https://u24.gov.ua/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/ukraine.gif" alt="Slava Ukraini" loading="lazy"></a>
|
||||
<a href="https://archive.org/details/zines-anti-fascism" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/antifa.png" alt="No fascism, no bigotry" loading="lazy"></a>
|
||||
<a href="https://www.youtube.com/watch?v=7AQbhes-Ntw" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/meltice.png" alt="Melt ICE" loading="lazy"></a>
|
||||
<a href="https://www.map.org.uk/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/palestine.png" alt="Free Palestine" loading="lazy"></a>
|
||||
<a href="https://u24.gov.ua/" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/ukraine.png" alt="Slava Ukraini" loading="lazy"></a>
|
||||
<!-- misc -->
|
||||
<a href="/discord"><img src="/assets/88x31/discord.gif" alt="Discord" loading="lazy"></a>
|
||||
<img src="/assets/88x31/bestvieweddesktop.gif" alt="Best viewed on desktop" loading="lazy">
|
||||
<img src="/assets/88x31/killmenow.gif" alt="Kill me now" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/no.gif" alt="Don't click here, no!" loading="lazy"></a>
|
||||
<a href="https://www.minecraft.net" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/minecraft.gif" alt="Minecraft" loading="lazy"></a>
|
||||
<a href="/discord"><img src="/assets/88x31/discord.png" alt="Discord" loading="lazy"></a>
|
||||
<img src="/assets/88x31/bestvieweddesktop.png" alt="Best viewed on desktop" loading="lazy">
|
||||
<img src="/assets/88x31/killmenow.png" alt="Kill me now" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/no.png" alt="Don't click here, no!" loading="lazy"></a>
|
||||
<a href="https://www.minecraft.net" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/minecraft.png" alt="Minecraft" loading="lazy"></a>
|
||||
<!-- anime -->
|
||||
<img src="/assets/88x31/pokemon.gif" alt="Pokémon" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=VEj0cuqVJ-I" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/caramelldansen.gif" alt="Caramelldansen" loading="lazy"></a>
|
||||
<img src="/assets/88x31/blink.gif" alt="Anime blink" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=_-2dIuV34cs" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/miku.gif" alt="This site is Miku approved" loading="lazy"></a>
|
||||
<img src="/assets/88x31/tummy.gif" alt="Anime tummy supporter" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=9lNZ_Rnr7Jc" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/badapple.gif" alt="Bad Apple!!" loading="lazy"></a>
|
||||
<img src="/assets/88x31/pokemon.png" alt="Pokémon" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=VEj0cuqVJ-I" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/caramelldansen.png" alt="Caramelldansen" loading="lazy"></a>
|
||||
<img src="/assets/88x31/blink.png" alt="Anime blink" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=_-2dIuV34cs" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/miku.png" alt="This site is Miku approved" loading="lazy"></a>
|
||||
<img src="/assets/88x31/tummy.png" alt="Anime tummy supporter" loading="lazy">
|
||||
<a href="https://www.youtube.com/watch?v=9lNZ_Rnr7Jc" target="_blank" rel="noopener noreferrer"><img src="/assets/88x31/badapple.png" alt="Bad Apple!!" loading="lazy"></a>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 640 KiB |
|
After Width: | Height: | Size: 768 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 554 B |
|
After Width: | Height: | Size: 725 B |
|
Before Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 6.5 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 724 B |
|
Before Width: | Height: | Size: 925 B |
|
After Width: | Height: | Size: 1007 B |
|
Before Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 724 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 644 B |
|
Before Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
116
js/music.js
|
|
@ -517,31 +517,95 @@
|
|||
}
|
||||
|
||||
// Last.fm stopped serving artist images — every artist returns the same
|
||||
// placeholder star — so we pull them from Deezer. Deezer's REST API blocks
|
||||
// CORS, but its JSONP mode (output=jsonp) doesn't. Resolves to a picture URL
|
||||
// (or "" when there's no match / it fails).
|
||||
function deezerArtistImg(name) {
|
||||
return new Promise(function (resolve) {
|
||||
if (!name) { resolve(""); return; }
|
||||
var cb = "__dz_" + Math.random().toString(36).slice(2);
|
||||
var s = document.createElement("script");
|
||||
var done = false;
|
||||
function finish(url) {
|
||||
if (done) return;
|
||||
done = true;
|
||||
try { delete window[cb]; s.remove(); } catch (e) { /* no-op */ }
|
||||
resolve(url || "");
|
||||
// placeholder star. Spotify has the photos, but its catalog API needs an
|
||||
// OAuth token (and a client secret we can't ship in a static page). So we
|
||||
// take the keyless route: MusicBrainz maps an artist name → their linked
|
||||
// Spotify page, then Spotify's own oEmbed hands back that page's picture.
|
||||
// No API key, no token, no third-party cookies. Resolves to a picture URL
|
||||
// (or "" when there's no confident match). Results are cached in
|
||||
// localStorage, so repeat visits are instant and we stay gentle on the APIs.
|
||||
const MB_ROOT = "https://musicbrainz.org/ws/2";
|
||||
const ART_CACHE_PREFIX = "cstupidcat:artimg:";
|
||||
const ART_TTL_HIT = 30 * 864e5; // remember a found photo for 30 days
|
||||
const ART_TTL_MISS = 3 * 864e5; // retry a miss after 3 days
|
||||
|
||||
// MusicBrainz asks anonymous clients for ~1 request/second, so funnel every
|
||||
// MB call through a one-at-a-time queue spaced ~1.1s apart.
|
||||
let mbChain = Promise.resolve();
|
||||
function mbFetch(url) {
|
||||
const run = mbChain.then(() =>
|
||||
fetch(url, { headers: { Accept: "application/json" } })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null)
|
||||
);
|
||||
const gap = () => new Promise((r) => setTimeout(r, 1100));
|
||||
mbChain = run.then(gap, gap); // pace the next call whether this one won or lost
|
||||
return run;
|
||||
}
|
||||
window[cb] = function (data) {
|
||||
var a = data && data.data && data.data[0];
|
||||
finish(a ? (a.picture_medium || a.picture || "") : "");
|
||||
};
|
||||
s.onerror = function () { finish(""); };
|
||||
s.src = "https://api.deezer.com/search/artist?q=" + encodeURIComponent(name) +
|
||||
"&limit=1&output=jsonp&callback=" + cb;
|
||||
document.head.appendChild(s);
|
||||
setTimeout(function () { finish(""); }, 6000);
|
||||
});
|
||||
|
||||
function artCacheGet(name) {
|
||||
try {
|
||||
const raw = localStorage.getItem(ART_CACHE_PREFIX + name.toLowerCase());
|
||||
if (!raw) return undefined;
|
||||
const hit = JSON.parse(raw);
|
||||
const ttl = hit.url ? ART_TTL_HIT : ART_TTL_MISS;
|
||||
if (Date.now() - hit.ts > ttl) return undefined; // stale → re-resolve
|
||||
return hit.url || "";
|
||||
} catch (e) { return undefined; }
|
||||
}
|
||||
function artCacheSet(name, url) {
|
||||
try {
|
||||
localStorage.setItem(ART_CACHE_PREFIX + name.toLowerCase(),
|
||||
JSON.stringify({ url: url || "", ts: Date.now() }));
|
||||
} catch (e) { /* storage full / disabled — just skip caching */ }
|
||||
}
|
||||
|
||||
// name → MusicBrainz artist MBID, but only when we're confident it's the
|
||||
// right act: an exact (case-insensitive) name match or a strong search score.
|
||||
async function mbArtistId(name) {
|
||||
const q = encodeURIComponent('artist:"' + name.replace(/"/g, " ") + '"');
|
||||
const data = await mbFetch(MB_ROOT + "/artist?query=" + q + "&limit=1&fmt=json");
|
||||
const a = data && data.artists && data.artists[0];
|
||||
if (!a) return "";
|
||||
const same = (a.name || "").toLowerCase() === name.toLowerCase();
|
||||
return (same || a.score >= 90) ? a.id : "";
|
||||
}
|
||||
// MBID → the artist's linked Spotify page (via MusicBrainz URL relationships)
|
||||
async function mbSpotifyUrl(mbid) {
|
||||
const data = await mbFetch(MB_ROOT + "/artist/" + mbid + "?inc=url-rels&fmt=json");
|
||||
const rels = (data && data.relations) || [];
|
||||
for (const r of rels) {
|
||||
const u = r && r.url && r.url.resource;
|
||||
if (u && u.indexOf("open.spotify.com/artist") !== -1) return u;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// Spotify page → its photo, via Spotify's public (keyless, CORS-open) oEmbed
|
||||
async function spotifyOembedImg(spotifyUrl) {
|
||||
try {
|
||||
const res = await fetch("https://open.spotify.com/oembed?url=" +
|
||||
encodeURIComponent(spotifyUrl));
|
||||
if (!res.ok) return "";
|
||||
const data = await res.json();
|
||||
return data.thumbnail_url || "";
|
||||
} catch (e) { return ""; }
|
||||
}
|
||||
|
||||
// Public resolver used by loadTop(): artist name → picture URL ("" on miss).
|
||||
async function artistImg(name) {
|
||||
if (!name) return "";
|
||||
const cached = artCacheGet(name);
|
||||
if (cached !== undefined) return cached; // fresh hit or fresh miss
|
||||
let url = "";
|
||||
try {
|
||||
const mbid = await mbArtistId(name);
|
||||
if (mbid) {
|
||||
const sp = await mbSpotifyUrl(mbid);
|
||||
if (sp) url = await spotifyOembedImg(sp);
|
||||
}
|
||||
} catch (e) { url = ""; }
|
||||
artCacheSet(name, url);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function loadTop() {
|
||||
|
|
@ -561,10 +625,10 @@
|
|||
'<span class="top-plays">' + esc(a.playcount) + " plays</span>" +
|
||||
"</span>" +
|
||||
"</a></li>").join("") + "</ol>";
|
||||
// chips are already visible — fill in the artist images as Deezer answers
|
||||
// chips are already visible — fill in the artist images as they resolve
|
||||
const chips = topBox.querySelectorAll(".top-chip");
|
||||
arr.forEach((a, i) => {
|
||||
deezerArtistImg(a.name).then((url) => {
|
||||
artistImg(a.name).then((url) => {
|
||||
if (!url) return;
|
||||
const slot = chips[i] && chips[i].querySelector(".top-art");
|
||||
if (!slot) return;
|
||||
|
|
|
|||