// Ari was here uwu // Professional boob lover // girls kissing,,, console.log(` ⣿⣿⣿⠏⣴⣿⣿⣿⣿⡿⠟⢹⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡉⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⢇⣾⣿⣿⣿⡿⢋⢀⣴⣿⣿⡿⠋⠀⠘⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣀⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⡏⣼⣿⣿⣿⠏⣴⢃⣾⣿⡿⢋⣴⠟⣠⣾⣿⣿⣿⠏⢁⣼⣿⣿⣿⣿⣿⣿⠟⣿⣿⣿⠟⠂⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠟⠛⠀⠘⠛⠛⣛⣛⣋⢉⣉⣉⣛⡛⠻⠿⣿⣿ ⡟⣸⣿⣿⣿⡏⡸⢡⣾⣿⢋⣤⡿⢡⣾⣿⣿⣿⠟⠁⣰⣿⠟⣹⡿⢿⣿⠋⢀⣾⣿⣿⠏⡄⢻⡆⢀⠙⣿⣿⡿⠟⢋⣩⣤⣶⣾⣿⣿⣿⠟⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶ ⢠⣿⣿⣿⣿⠃⢡⣿⡟⣡⣾⠏⣰⣿⣿⣿⡿⠋⢀⣾⡿⢁⣼⠟⢠⠞⠁⣰⣿⣿⡿⢣⣾⡇⢸⣿⣾⠆⠙⠁⣰⣾⣿⣿⣿⣿⣿⣿⣿⠏⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⡏⢠⣿⠏⣴⣿⠟⣰⣿⣿⣿⡿⣡⢃⣾⠟⢀⡞⠁⣴⢋⠄⣼⣿⣿⠏⣰⣿⡟⢀⡼⠋⣠⡶⠀⣴⣿⢿⣿⣿⢿⠏⣸⣿⡏⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣧⣿⠏⣼⡿⠿⢠⣿⣿⣿⡟⣰⢃⣾⠏⢀⠞⠀⡼⢡⠟⣼⣿⡟⣡⣾⣿⠏⠀⠄⠀⣾⡿⠁⣼⡿⢃⡜⣽⡏⠈⣰⡟⠀⠁⣾⣿⣿⣿⣿⣿⣿⡇⢹⣿⣿⣿⣿⣿⡇⢻ ⣿⣿⣿⣿⣿⡏⣸⣿⠃⠀⣼⣿⣿⡟⣴⢃⣾⠏⠀⢀⢀⡾⢡⡟⣰⣿⢋⣴⡿⠋⠁⣀⠀⠀⣰⡿⠁⡌⡸⢁⣾⣿⡟⠀⢠⣿⡇⠀⣸⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⣿⡇⢸ ⣿⣿⣿⣿⡿⢰⣿⠃⡆⢀⣿⣿⡟⣸⠏⣾⠏⣴⠂⠂⣾⢡⡟⣰⡟⣡⠞⣩⠄⢀⣼⠁⠀⣼⣿⠇⡼⠠⠃⣼⣿⡿⠁⢀⣾⠿⠀⠀⡏⢸⣿⣿⣿⠃⣿⣿⠇⢸⣿⣿⣿⣿⣿⡇⢸ ⣿⣿⡇⢸⠇⣾⡏⣸⠃⢸⣿⡿⢱⡟⣸⡟⣼⠃⠂⣼⢃⠏⢠⠏⠰⠋⠀⢁⣠⣾⠇⠀⣼⣿⡟⠀⠁⣦⣾⣿⡿⠀⣴⡾⠀⠀⠀⠠⠁⣿⡿⢹⠉⢠⣿⣿⠀⢸⣿⣿⣿⣿⣿⡇⢸ ⣿⣿⠀⣼⢰⣿⡀⡿⢀⢸⣿⢡⣿⢡⡿⠰⡏⠀⢰⠇⡞⠀⠀⣾⠞⠀⢴⣿⣿⣷⠀⣼⣿⣿⠃⠀⣸⣿⡿⡿⠀⢀⣿⠇⠀⠀⠀⠐⢀⣿⠇⡌⢠⣸⣿⡇⠀⢸⣿⣿⣿⣿⣿⠃⣸ ⣿⡟⠀⡇⣸⣿⢠⡇⣏⣾⡏⣼⡇⡼⠁⠘⠁⠀⡟⡸⠀⠐⠚⢁⣦⣶⣿⣿⣿⡇⠀⣿⠇⡟⠀⠀⣿⡿⠁⠀⠀⢸⡟⠀⢰⠆⠀⡄⢸⡏⢠⠃⣿⡟⢹⡇⠀⣿⣿⣿⣿⣿⡿⠀⡏ ⣿⡇⠀⡇⣿⣿⢸⡇⡟⣿⢰⣿⢡⠃⠀⠀⣰⠃⢡⠁⠈⠀⣴⣿⣿⣿⣿⣿⠟⡁⡀⢻⠀⡇⠀⢀⣿⠃⠀⠀⡄⢸⠃⠀⣿⡇⠀⠀⣼⡇⢸⢀⣿⡇⢸⠀⢀⣿⣿⣿⣿⣿⠇⢸⠁ ⣿⢃⡆⡇⢿⣿⢸⣷⡇⠏⣼⡏⡌⠀⠀⠀⡏⢀⣼⡘⢀⣤⡈⠛⢿⣿⣿⣧⣾⡇⣇⠘⠀⠁⠀⢸⡏⠀⠀⣼⠃⡏⠀⢸⣿⡇⠀⠀⣿⠀⣾⣼⣿⠀⡟⠀⢸⣿⣿⣿⣿⡟⢀⡟⢠ ⣿⣿⡇⡇⢸⣿⢸⠛⡇⠀⣿⠇⠁⠀⠏⣼⣷⢸⡿⢃⣾⣿⣷⣄⠀⠈⠛⢿⣿⡇⣿⡀⠀⠀⠀⠈⠀⠀⣼⣿⠀⠀⠀⣀⠙⢧⠀⠀⣿⠀⣿⣿⣿⠀⡇⠀⣼⣿⣿⣿⣿⠃⣼⠃⣾ ⣿⣿⡇⠁⢸⣿⠘⠀⡇⠀⣿⠀⠀⠀⢰⣿⣿⡆⠃⣼⣿⣿⣿⣿⣷⣤⣄⣤⣽⣇⢹⡇⠀⣦⡄⠀⠀⢸⣿⡟⠀⠀⢠⣿⣷⣄⠀⠀⣿⠀⣿⣿⡇⢰⠁⠀⣿⣿⣿⣿⡟⢠⡏⢰⡿ ⣿⠻⣷⠀⢸⣿⡄⠀⣷⣾⣧⠀⠀⠀⠈⣿⣿⠇⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣷⣦⡘⡷⠄⠀⣿⣿⡇⠀⣰⣾⣿⣿⣿⡇⠀⠈⠀⣿⣿⠁⠈⠀⢸⣿⣿⣿⡿⢀⣾⣧⣿⠃ ⣿⡆⣿⠀⢸⣿⣇⠀⣿⣿⣿⣷⠀⢀⠀⣿⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠻⠷⢂⣤⣼⣿⣿⣇⢀⣿⣿⣿⣿⣿⣷⠀⠀⠀⣿⣿⠀⠀⠀⣼⣿⣿⣿⠃⣸⣿⣿⠃⠀ ⣿⣧⢸⡆⠘⣿⣿⠀⢻⣿⣿⡇⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣶⣶⣿⣿⣿⣿⣿⣿⣿⠘⠛⣻⣿⣿⣿⣿⡀⠀⠀⣿⡏⢠⠀⠀⣾⣿⣿⠇⠠⢿⢻⠏⠀⠀ ⢹⣿⡌⣧⠀⠻⣿⣷⣾⣿⣿⡇⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠻⣿⣿⣿⣿⣷⡆⠀⢿⠃⠀⠀⠀⣿⣿⠏⠀⠀⠆⠀⠀⠀⠀ ⡌⢿⣷⢹⡆⠈⢿⣿⣿⣿⣿⠧⣿⡟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣄⡈⠛⠿⣿⣿⣷⠀⠸⠀⠀⡄⢸⣿⡏⢠⠂⠘⠀⠀⠀⠀⠀ ⣷⠘⣿⡆⢿⣧⡈⠻⢿⣿⣿⠀⣿⣧⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⣡⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣾⣿⡆⠀⠀⢠⠁⣸⡟⢀⠎⠀⠀⠀⠀⠀⠀⢠ ⣿⣧⢹⣿⡘⣿⣷⣀⠈⣿⣿⠀⣿⣿⣧⡹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⠀⠟⠁⡼⠀⠀⠀⠀⠀⠀⢀⣾ ⣿⣿⣆⢻⣷⡘⣿⣿⡀⠘⣿⡆⢹⣿⣿⣷⡌⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠗⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⢸⠀⠀⢰⠁⠀⠀⢀⠀⠀⢀⣿⣿ ⢿⢿⣿⣦⠹⣷⠸⣿⣷⠀⠹⡇⠘⣿⣿⠿⢿⣦⣙⣿⣿⣿⣿⣿⣿⣿⣿⠟⣡⣄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠸⠀⠀⠌⠀⠀⢀⠆⠀⢀⣿⣿⣿ ⠀⠈⢿⣿⣷⡙⢧⠹⣿⣇⢧⠉⠀⣿⠏⣰⣶⣤⣍⡛⠿⣿⣿⣿⣿⠟⣡⣾⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠈⠀⢠⣾⣿⡿⠟ ⣀⠀⠈⠻⣿⣿⣌⠣⠙⣿⡌⢧⠀⠁⣼⣿⣿⣿⣿⣿⣷⣶⣬⣭⣥⣾⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠗⠀⠀⠀⠀⠀⠀⣰⣿⡟⠁⠀⠀ ⣿⣿⣶⠀⠈⠛⢿⣷⡄⠈⢿⡌⣇⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⠸⣟⡀⣀⠀⢠⣼⣿⣿⣷⣿⠀⠀ ⣿⣿⣿⣿⣷⣤⡀⠉⠛⢦⣀⣿⡘⡄⢹⣬⡙⣿⣿⣿⠟⣹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣤⣙⠻⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⡙⠛⠠⠶⠃⣸⣿⣿⣿⣿⠀⠀ ⣿⣿⣿⣿⣿⣿⣧⡀⠀⠰⣿⣿⣷⠸⡄⠙⣷⣼⣧⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣦⣭⣭⡉⣙⡛⠛⠿⣿⣿⣿⣿⣿⡇⠐⠄⢀⢂⡀⢘⣿⣿⣿⣿⣿⣷⡄ ⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⢹⣿⣿⣧⢹⡄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢇⣿⢿⣷⡄⠘⣿⣿⣿⣿⠇⣾⣿⣦⣤⡀⢸⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣷⣤⣈⡙⠻⢿⡇⠀⢿⣇⢻⡆⢿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣧⡀⡜⢁⣤⡘⣿⣿⡿⢠⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⡀⠘⣿⣄⢻⡘⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣸⣿⡀⠀⣷⡘⣿⠇⣼⣿⣿⣿⡿⢸⣿⣿⣿⣿⣿⣿⣿⣿`); // mmmmmmmmmmmmmmmmm girls kissing,,,,, (function music() { "use strict"; // ---- config ------------------------------------------------------------- const DISCORD_ID = "1464890289922641993"; const LFM_USER = "Real_AlexTLM"; const LFM_KEY = "768e8bd0d366f4d6c7874740ca6610ad"; const LFM_OK = !!(LFM_USER && LFM_KEY); // LRCLIB-compatible instances, tried in order; falls through on any failure. const LRCLIB_HOSTS = [ "https://lrclib.schuh.wtf", "https://lyrics.lanyard.cafe", "https://lyrics.kie.ac", "https://api.assumi.ng/lyrics", "https://lyrics.aureal.dev", "https://lrclib.net", ]; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // ---- tiny helpers ------------------------------------------------------- const $ = (sel) => document.querySelector(sel); function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); } function mmss(ms) { if (!isFinite(ms) || ms < 0) ms = 0; const s = Math.floor(ms / 1000); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); } function clamp(n, lo, hi) { return n < lo ? lo : n > hi ? hi : n; } // ---- album art → Catppuccin accent (same maths as now-playing.js) ------- const ACCENT_VARS = [ "rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "saphire", "blue", "lavender", ]; function hexToRgb(hex) { hex = hex.trim().replace("#", ""); if (hex.length === 3) hex = hex.split("").map((c) => c + c).join(""); const n = parseInt(hex, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } function themePalette() { const cs = getComputedStyle(document.documentElement); const pal = []; for (const name of ACCENT_VARS) { const v = cs.getPropertyValue("--" + name).trim(); if (v.startsWith("#")) { const [r, g, b] = hexToRgb(v); pal.push({ r, g, b }); } } return pal; } function nearestAccent(r, g, b) { const pal = themePalette(); let best = null, bestD = Infinity; for (const c of pal) { const rm = (r + c.r) / 2, dr = r - c.r, dg = g - c.g, db = b - c.b; const d = (2 + rm / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rm) / 256) * db * db; if (d < bestD) { bestD = d; best = c; } } return best; } let lastArtUrl = null; function applyAccent(url) { if (!url) { resetAccent(); return; } if (url === lastArtUrl) return; lastArtUrl = url; const img = new Image(); img.crossOrigin = "anonymous"; img.referrerPolicy = "no-referrer"; img.onload = () => { try { const c = document.createElement("canvas"); c.width = c.height = 16; const ctx = c.getContext("2d", { willReadFrequently: true }); ctx.drawImage(img, 0, 0, 16, 16); const { data } = ctx.getImageData(0, 0, 16, 16); let r = 0, g = 0, b = 0, count = 0; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 125) continue; const lum = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]; if (lum < 24 || lum > 235) continue; r += data[i]; g += data[i + 1]; b += data[i + 2]; count++; } if (!count) { resetAccent(); return; } r = Math.round(r / count); g = Math.round(g / count); b = Math.round(b / count); const near = nearestAccent(r, g, b); const rgb = near ? `${near.r}, ${near.g}, ${near.b}` : `${r}, ${g}, ${b}`; document.documentElement.style.setProperty("--accent-rgb", rgb); } catch (e) { resetAccent(); } }; img.onerror = resetAccent; img.src = url; } function resetAccent() { lastArtUrl = null; document.documentElement.style.removeProperty("--accent-rgb"); } // ---- DOM refs ----------------------------------------------------------- const stage = $("#music"); if (!stage) return; const npArt = $("#np-art"); const npState = $("#np-state"); const npTitle = $("#np-title"); const npArtist = $("#np-artist"); const npAlbum = $("#np-album"); const npLink = $("#np-link"); const barFill = $("#np-fill"); const barCur = $("#np-cur"); const barDur = $("#np-dur"); const progress = $("#np-progress"); const lyricsBox = $("#lyrics"); const lockBtn = $("#ly-lock"); const recentBox = $("#recent"); const topBox = $("#top"); // ---- state -------------------------------------------------------------- // track: { song, artist, album, art, trackId, url, start, end, duration, live } let track = null; // what's on the hero right now let lyrics = null; // { synced:[{t,text}], plain, instrumental, key } let lyricsReq = 0; // race guard for async lyric fetches let activeLine = -1; const lyricsCache = new Map(); // trackKey → normalized lyrics (instant on repeat) let locked = true; // follow the current line; released when the user scrolls function trackKey(t) { return t ? [t.song, t.artist, t.album].map((x) => (x || "").toLowerCase()).join("\u241F") : ""; } // ======================================================================= // NOW PLAYING (hero) // ======================================================================= function paintHero() { if (!track) { stage.classList.add("is-idle"); npState.textContent = "Not listening right now"; npTitle.textContent = "—"; npArtist.textContent = ""; npAlbum.textContent = ""; npArt.removeAttribute("src"); npArt.classList.remove("has-art"); npLink.removeAttribute("href"); progress.hidden = true; resetAccent(); return; } stage.classList.toggle("is-idle", false); stage.classList.toggle("is-live", !!track.live); npState.textContent = track.live ? "Listening now" : "Last played"; npTitle.textContent = track.song || "Unknown track"; npArtist.textContent = track.artist || ""; npAlbum.textContent = track.album || ""; if (track.art) { npArt.src = track.art; npArt.classList.add("has-art"); } else { npArt.removeAttribute("src"); npArt.classList.remove("has-art"); } if (track.url) npLink.href = track.url; else npLink.removeAttribute("href"); // progress bar only makes sense for a live track with real timestamps progress.hidden = !(track.live && track.start && track.end); if (!progress.hidden) barDur.textContent = mmss(track.duration); applyAccent(track.art); } function setTrack(next) { const changed = trackKey(next) !== trackKey(track); track = next; paintHero(); if (changed) { activeLine = -1; loadLyrics(next); } } // ======================================================================= // LYRICS (LRCLIB) // ======================================================================= function parseLRC(text) { if (!text) return []; const out = []; const tag = /\[(\d{1,2}):(\d{1,2}(?:[.:]\d{1,3})?)\]/g; text.split(/\r?\n/).forEach((line) => { tag.lastIndex = 0; const stamps = []; let m, last = 0; while ((m = tag.exec(line))) { const mins = parseInt(m[1], 10); const secs = parseFloat(m[2].replace(":", ".")); stamps.push((mins * 60 + secs) * 1000); last = tag.lastIndex; } if (!stamps.length) return; const words = line.slice(last).trim(); stamps.forEach((t) => out.push({ t, text: words })); }); out.sort((a, b) => a.t - b.t); return out; } function lyricsLoading() { lyricsBox.className = "lyrics is-loading"; lyricsBox.innerHTML = '

Finding lyrics…

'; } function lyricsEmpty(msg) { lyricsBox.className = "lyrics is-empty"; lyricsBox.innerHTML = '

' + esc(msg) + "

"; } function renderLyrics(data) { lyrics = data; activeLine = -1; locked = true; // every new track starts in follow mode const synced = data && data.synced && data.synced.length; updateLock(synced); if (!data) { lyricsEmpty("No track playing."); return; } if (data.instrumental) { lyricsBox.className = "lyrics is-instrumental"; lyricsBox.innerHTML = '

♪ instrumental ♪

'; return; } if (synced) { lyricsBox.className = "lyrics is-synced"; lyricsBox.innerHTML = data.synced .map((l, i) => '

' + (esc(l.text) || " ") + "

") .join(""); lyricsBox.scrollTop = 0; return; } if (data.plain) { lyricsBox.className = "lyrics is-plain"; lyricsBox.innerHTML = data.plain.split(/\r?\n/) .map((l) => '

' + (esc(l) || " ") + "

").join(""); return; } lyricsEmpty("No lyrics found for this one."); } // ---- lock / follow controls -------------------------------------------- let selfScroll = false; // true while WE are scrolling, so we don't self-release function centerLine(i, smooth) { const el = lyricsBox.children[i]; if (!el) return; const top = el.offsetTop - lyricsBox.clientHeight / 2 + el.clientHeight / 2; selfScroll = true; lyricsBox.scrollTo({ top, behavior: smooth && !reduceMotion ? "smooth" : "auto" }); setTimeout(() => { selfScroll = false; }, smooth && !reduceMotion ? 600 : 50); } function updateLock(show) { if (!lockBtn) return; lockBtn.hidden = !show; lockBtn.classList.toggle("is-locked", locked); lockBtn.setAttribute("aria-pressed", String(locked)); let label = lockBtn.querySelector(".ly-lock-label"); if (!label) { lockBtn.innerHTML = '' + ''; label = lockBtn.querySelector(".ly-lock-label"); } label.textContent = locked ? "Synced" : "Sync"; } function release() { // user took over the scroll if (!locked) return; locked = false; if (lockBtn) updateLock(!lockBtn.hidden); } function reLock() { // jump back to the current line and follow again locked = true; updateLock(true); if (activeLine >= 0) centerLine(activeLine, true); } if (lockBtn) { lockBtn.addEventListener("click", () => (locked ? release() : reLock())); } // user-driven scroll intent releases the lock (programmatic scrolls don't) ["wheel", "touchmove"].forEach((ev) => lyricsBox.addEventListener(ev, () => { if (!selfScroll) release(); }, { passive: true })); lyricsBox.addEventListener("keydown", (e) => { if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(e.key)) release(); }); // LRCLIB lookups, narrowest match first. Walks the instance list until one // returns a hit; a 404/non-OK just means "this mirror doesn't have it" → next. async function lrclibGet(params) { const qs = new URLSearchParams(params).toString(); for (const host of LRCLIB_HOSTS) { try { const res = await fetch(host + "/api/get?" + qs, { headers: { "X-User-Agent": "clove.is-a.dev music (https://clove.is-a.dev)" }, }); if (res.ok) return res.json(); } catch (e) { /* network/CORS error → try the next instance */ } } return null; } async function lrclibSearch(track_name, artist_name) { const qs = new URLSearchParams({ track_name, artist_name }).toString(); for (const host of LRCLIB_HOSTS) { try { const res = await fetch(host + "/api/search?" + qs); if (!res.ok) continue; const arr = await res.json(); if (!Array.isArray(arr) || !arr.length) continue; // prefer a result that actually has synced lyrics return arr.find((r) => r.syncedLyrics) || arr.find((r) => r.plainLyrics) || arr[0]; } catch (e) { /* next instance */ } } return null; } function normalize(rec) { if (!rec) return null; return { instrumental: !!rec.instrumental, synced: parseLRC(rec.syncedLyrics), plain: rec.plainLyrics || "", }; } async function loadLyrics(t) { const myReq = ++lyricsReq; if (!t) { renderLyrics(null); return; } const key = trackKey(t); if (lyricsCache.has(key)) { renderLyrics(lyricsCache.get(key)); return; } // instant lyricsLoading(); let rec = null; try { if (t.live && t.duration) { // exact-ish match using duration (±2s tolerance handled by LRCLIB) rec = await lrclibGet({ track_name: t.song, artist_name: t.artist, album_name: t.album || "", duration: Math.round(t.duration / 1000), }); } if (!rec) { // drop album / no-duration → fall back to search rec = await lrclibSearch(t.song || "", t.artist || ""); } } catch (e) { rec = null; } if (myReq !== lyricsReq) return; // a newer track superseded us const data = normalize(rec); lyricsCache.set(key, data); renderLyrics(data); } // ======================================================================= // TICKER — sync the progress bar + active lyric line to playback // ======================================================================= function tick() { if (track && track.live && track.start && track.end) { const pos = clamp(Date.now() - track.start, 0, track.duration); // progress bar if (!progress.hidden) { barFill.style.width = (pos / track.duration) * 100 + "%"; barCur.textContent = mmss(pos); } // active synced line if (lyrics && lyrics.synced && lyrics.synced.length) { let i = -1; for (let k = 0; k < lyrics.synced.length; k++) { if (lyrics.synced[k].t <= pos) i = k; else break; } if (i !== activeLine) { const lines = lyricsBox.children; if (activeLine >= 0 && lines[activeLine]) lines[activeLine].classList.remove("is-active"); activeLine = i; if (lines[i]) { lines[i].classList.add("is-active"); if (locked) centerLine(i, true); } } } } requestAnimationFrame(tick); } // ======================================================================= // LANYARD — live Discord presence (same socket as the card) // ======================================================================= let ws = null, heartbeat = null, retry = 0; function fromSpotify(s) { return { song: s.song, artist: s.artist, album: s.album, art: s.album_art_url || "", trackId: s.track_id || "", url: s.track_id ? "https://open.spotify.com/track/" + s.track_id : "", start: s.timestamps && s.timestamps.start, end: s.timestamps && s.timestamps.end, duration: s.timestamps ? s.timestamps.end - s.timestamps.start : 0, live: true, }; } function onPresence(d) { if (d && d.listening_to_spotify && d.spotify) { setTrack(fromSpotify(d.spotify)); } else if (track && track.live) { // they just stopped — fall back to the latest scrobble for the hero track = null; showIdle(); } else if (!track) { showIdle(); } } function connectLanyard() { try { ws = new WebSocket("wss://api.lanyard.rest/socket"); } catch (e) { return; } ws.onmessage = (ev) => { let msg; try { msg = JSON.parse(ev.data); } catch (e) { return; } if (msg.op === 1) { const interval = (msg.d && msg.d.heartbeat_interval) || 30000; if (heartbeat) clearInterval(heartbeat); heartbeat = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 3 })); }, interval); ws.send(JSON.stringify({ op: 2, d: { subscribe_to_id: DISCORD_ID } })); } else if (msg.op === 0 && (msg.t === "INIT_STATE" || msg.t === "PRESENCE_UPDATE")) { onPresence(msg.d); } }; ws.onclose = () => { if (heartbeat) { clearInterval(heartbeat); heartbeat = null; } retry = Math.min(retry + 1, 6); setTimeout(connectLanyard, 1000 * retry); }; ws.onerror = () => { if (ws) ws.close(); }; } // ======================================================================= // LAST.FM — recently played, top artists, and the idle fallback headline // ======================================================================= const LFM = "https://ws.audioscrobbler.com/2.0/"; const LFM_PLACEHOLDER = "2a96cbd8b46e442fc41c2b86b821562f"; // last.fm "no art" star function lfmImg(images) { if (!Array.isArray(images)) return ""; const big = images[images.length - 1] || images[0] || {}; const url = big["#text"] || ""; return url && url.indexOf(LFM_PLACEHOLDER) === -1 ? url : ""; } function timeAgo(uts) { const diff = Math.floor(Date.now() / 1000) - Number(uts); if (!isFinite(diff) || diff < 0) return ""; if (diff < 60) return "just now"; if (diff < 3600) return Math.floor(diff / 60) + " min ago"; if (diff < 86400) return Math.floor(diff / 3600) + " hr ago"; return Math.floor(diff / 86400) + " day" + (diff < 172800 ? "" : "s") + " ago"; } async function lfm(method, extra) { const qs = new URLSearchParams(Object.assign( { method, user: LFM_USER, api_key: LFM_KEY, format: "json" }, extra || {} )).toString(); const res = await fetch(LFM + "?" + qs); if (!res.ok) throw new Error("last.fm " + res.status); return res.json(); } async function showIdle() { // pull the latest scrobble so the page isn't a dead end when nobody's listening if (!LFM_OK) { paintHero(); return; } try { const data = await lfm("user.getrecenttracks", { limit: 1 }); if (track && track.live) return; // a live presence arrived while we waited const t = data && data.recenttracks && data.recenttracks.track; const last = Array.isArray(t) ? t[0] : t; if (last) { setTrack({ song: last.name, artist: last.artist && (last.artist["#text"] || last.artist.name), album: last.album && last.album["#text"], art: lfmImg(last.image), url: last.url || "", live: false, }); return; } } catch (e) { /* fall through */ } paintHero(); } async function loadRecent() { if (!LFM_OK) { recentBox.innerHTML = '
  • Add your Last.fm username + key to the ' + 'music.js script tag to show recent plays.
  • '; return; } try { const data = await lfm("user.getrecenttracks", { limit: 12 }); const arr = (data && data.recenttracks && data.recenttracks.track) || []; const list = Array.isArray(arr) ? arr : [arr]; if (!list.length) { recentBox.innerHTML = '
  • No recent scrobbles.
  • '; return; } recentBox.innerHTML = list.map((t) => { const now = t["@attr"] && t["@attr"].nowplaying === "true"; const art = lfmImg(t.image); const when = now ? 'scrobbling now' : '' + esc(timeAgo(t.date && t.date.uts)) + ""; const artist = t.artist && (t.artist["#text"] || t.artist.name) || ""; return '
  • ' + '' + (art ? '' : '') + '' + '' + esc(t.name) + "" + '' + esc(artist) + "" + "" + when + "
  • "; }).join(""); } catch (e) { recentBox.innerHTML = '
  • Couldn’t reach Last.fm just now.
  • '; } } async function loadTop() { if (!LFM_OK || !topBox) { if (topBox) topBox.hidden = true; return; } try { const data = await lfm("user.gettopartists", { period: "7day", limit: 8 }); const arr = (data && data.topartists && data.topartists.artist) || []; if (!arr.length) { topBox.hidden = true; return; } topBox.hidden = false; topBox.innerHTML = '

    Top artists · last 7 days

    ' + '
      ' + arr.map((a, i) => '
    1. ' + '' + (i + 1) + "" + '' + esc(a.name) + "" + '' + esc(a.playcount) + " plays" + "
    2. ").join("") + "
    "; } catch (e) { topBox.hidden = true; } } // ======================================================================= // boot // ======================================================================= paintHero(); showIdle(); // headline + lyrics before the socket warms up connectLanyard(); // takes over the hero the moment a presence arrives loadRecent(); loadTop(); requestAnimationFrame(tick); if (LFM_OK) setInterval(loadRecent, 45000); })();