| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- ---
- import Win98Window from "./win98window.astro";
- const layout = {
- mobile: { left: "2%", top: "60%", width: "96%", height: "auto" },
- desktop: { left: "66%", top: "8%", width: "320px", height: "auto" },
- };
- ---
- <Win98Window title="Now Playing" layout={layout}, shown=true>
- <div class="np" id="nowplaying">
- <div class="np-row">
- <div class="np-art" aria-hidden="true">
- <div class="np-art-inner" id="np-art-inner"></div>
- </div>
- <div class="np-meta">
- <div class="np-title" id="np-title">Not connected</div>
- <div class="np-artist" id="np-artist">Spotify</div>
- <div class="np-album" id="np-album"></div>
- </div>
- </div>
- <div class="np-progress">
- <div class="np-time" id="np-time">--:-- / --:--</div>
- <div class="np-bar" role="progressbar" aria-label="track progress">
- <div class="np-bar-fill" id="np-bar-fill"></div>
- </div>
- </div>
- <div class="np-controls">
- <button id="np-open" disabled>Open</button>
- <button id="np-refresh">Refresh</button>
- </div>
- </div>
- </Win98Window>
- <style>
- .np {
- width: 100%;
- max-width: 100%;
- }
- .np-row {
- display: flex;
- gap: 10px;
- align-items: center;
- }
- .np-art {
- width: 56px;
- height: 56px;
- background: silver;
- box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey,
- inset 2px 2px #dfdfdf;
- padding: 3px;
- flex: 0 0 auto;
- }
- .np-art-inner {
- width: 100%;
- height: 100%;
- background: #000;
- background-size: cover;
- background-position: center;
- }
- .np-meta {
- min-width: 0;
- flex: 1 1 auto;
- }
- .np-title {
- font-weight: 700;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .np-artist,
- .np-album {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: #222;
- }
- .np-album {
- color: #444;
- }
- .np-progress {
- margin-top: 10px;
- }
- .np-time {
- font-size: 11px;
- margin-bottom: 4px;
- }
- .np-bar {
- height: 14px;
- background: white;
- box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf,
- inset 2px 2px grey;
- padding: 2px;
- }
- .np-bar-fill {
- height: 100%;
- width: 0%;
- background: navy;
- }
- .np-controls {
- margin-top: 10px;
- display: flex;
- gap: 8px;
- }
- .np-controls button {
- min-width: 80px;
- }
- .np-hint {
- margin-top: 10px;
- font-size: 11px;
- color: #333;
- }
- </style>
- <script>
- const apiUrl = "/api/spotify/now-playing.json";
- const pollMs = 8000;
- const elTitle = document.getElementById("np-title");
- const elArtist = document.getElementById("np-artist");
- const elAlbum = document.getElementById("np-album");
- const elTime = document.getElementById("np-time");
- const elFill = document.getElementById("np-bar-fill");
- const elArt = document.getElementById("np-art-inner");
- const elHint = document.getElementById("np-hint");
- const btnOpen = document.getElementById("np-open");
- const btnRefresh = document.getElementById("np-refresh");
- let lastTrackUrl = null;
- let timer = null;
- let tickTimer = null;
- let lastKnown = {
- title: null,
- artist: null,
- album: null,
- albumArtUrl: null,
- trackUrl: null,
- durationMs: null,
- stoppedAtMs: null,
- stoppedProgressMs: null,
- };
- let lastProgressMs = null;
- let lastDurationMs = null;
- let lastIsPlaying = false;
- let lastTrackKey = null;
- let lastSyncAt = 0;
- function fmtTime(ms) {
- if (typeof ms !== "number" || !Number.isFinite(ms)) return "--:--";
- const s = Math.max(0, Math.floor(ms / 1000));
- const m = Math.floor(s / 60);
- const r = s % 60;
- return `${m}:${String(r).padStart(2, "0")}`;
- }
- function fmtRelative(tsMs) {
- if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "";
- const diffMs = Date.now() - tsMs;
- if (!Number.isFinite(diffMs) || diffMs < 0) return "";
- const sec = Math.floor(diffMs / 1000);
- const min = Math.floor(sec / 60);
- const hr = Math.floor(min / 60);
- try {
- const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
- if (sec < 60) return rtf.format(-sec, "second");
- if (min < 60) return rtf.format(-min, "minute");
- return rtf.format(-hr, "hour");
- } catch {
- if (sec < 60) return `${sec}s ago`;
- if (min < 60) return `${min}m ago`;
- return `${hr}h ago`;
- }
- }
- function setProgress(progressMs, durationMs) {
- if (typeof progressMs !== "number" || typeof durationMs !== "number" || durationMs <= 0) {
- elFill.style.width = "0%";
- elTime.textContent = "--:-- / --:--";
- return;
- }
- const pct = Math.max(0, Math.min(100, (progressMs / durationMs) * 100));
- elFill.style.width = `${pct.toFixed(1)}%`;
- elTime.textContent = `${fmtTime(progressMs)} / ${fmtTime(durationMs)}`;
- }
- function startTicking() {
- if (tickTimer) return;
- tickTimer = setInterval(() => {
- if (!lastIsPlaying) return;
- if (typeof lastProgressMs !== "number" || typeof lastDurationMs !== "number") return;
- const elapsed = Date.now() - lastSyncAt;
- const est = lastProgressMs + elapsed;
- setProgress(est, lastDurationMs);
- }, 250);
- }
- function stopTicking() {
- if (tickTimer) {
- clearInterval(tickTimer);
- tickTimer = null;
- }
- }
- function setArt(url) {
- if (!url) {
- elArt.style.backgroundImage = "none";
- return;
- }
- elArt.style.backgroundImage = `url(${url})`;
- }
- async function refresh() {
- try {
- const res = await fetch(apiUrl, { cache: "no-store" });
- const data = await res.json();
- if (!data || data.isPlaying === false || !data.title) {
- if (lastKnown.title) {
- elTitle.textContent = `Last played: ${lastKnown.title}`;
- elArtist.textContent = lastKnown.artist || "";
- const when = fmtRelative(lastKnown.stoppedAtMs);
- elAlbum.textContent = when
- ? `${lastKnown.album || ""} (${when})`
- : lastKnown.album || "";
- setArt(lastKnown.albumArtUrl);
- setProgress(lastKnown.stoppedProgressMs, lastKnown.durationMs);
- lastTrackUrl = lastKnown.trackUrl;
- if (btnOpen instanceof HTMLButtonElement) btnOpen.disabled = !lastTrackUrl;
- } else {
- elTitle.textContent = "(Nothing playing)";
- elArtist.textContent = "Spotify";
- elAlbum.textContent = "";
- setArt(null);
- setProgress(null, null);
- if (btnOpen instanceof HTMLButtonElement) btnOpen.disabled = true;
- lastTrackUrl = null;
- }
- stopTicking();
- if (lastIsPlaying && lastKnown.title && !lastKnown.stoppedAtMs) {
- lastKnown.stoppedAtMs = Date.now();
- if (typeof lastProgressMs === "number") lastKnown.stoppedProgressMs = lastProgressMs;
- }
- lastIsPlaying = false;
- lastProgressMs = null;
- lastDurationMs = null;
- lastTrackKey = null;
- lastSyncAt = 0;
- elHint.textContent = data?.error ? `Status: ${data.error}` : "";
- return;
- }
- elTitle.textContent = data.title;
- elArtist.textContent = data.artist || "";
- elAlbum.textContent = data.album || "";
- setArt(data.albumArtUrl);
- lastKnown = {
- title: data.title ?? null,
- artist: data.artist ?? null,
- album: data.album ?? null,
- albumArtUrl: data.albumArtUrl ?? null,
- trackUrl: data.trackUrl ?? null,
- durationMs: typeof data.durationMs === "number" ? data.durationMs : null,
- stoppedAtMs: null,
- stoppedProgressMs: null,
- };
- const trackKey = `${data.title}::${data.artist}::${data.album}`;
- const durationMs = typeof data.durationMs === "number" ? data.durationMs : null;
- const progressMs = typeof data.progressMs === "number" ? data.progressMs : null;
- const isPlaying = Boolean(data.isPlaying);
- if (lastTrackKey && trackKey !== lastTrackKey) {
- lastProgressMs = null;
- lastSyncAt = 0;
- }
- lastTrackKey = trackKey;
- lastDurationMs = durationMs;
- lastProgressMs = progressMs;
- lastIsPlaying = isPlaying;
- lastSyncAt = Date.now();
- setProgress(progressMs, durationMs);
- if (isPlaying) startTicking();
- else stopTicking();
- lastTrackUrl = data.trackUrl || null;
- if (btnOpen instanceof HTMLButtonElement) btnOpen.disabled = !lastTrackUrl;
- elHint.textContent = "";
- } catch (e) {
- elHint.textContent = "Unable to load now playing.";
- }
- }
- btnOpen?.addEventListener("click", () => {
- if (lastTrackUrl) window.open(lastTrackUrl, "_blank", "noopener,noreferrer");
- });
- btnRefresh?.addEventListener("click", () => refresh());
- refresh();
- timer = setInterval(refresh, pollMs);
- window.addEventListener("beforeunload", () => {
- if (timer) clearInterval(timer);
- if (tickTimer) clearInterval(tickTimer);
- });
- </script>
|