My personal site hosted @ https://indexx.dev

feat: switch to tealfm for music widget

Changed files
+118 -38
src
+28 -13
src/components/jsx/AudioVisualizer.jsx
···
"0.4s",
];
-
export default function AudioVisualizer() {
+
export default function AudioVisualizer({ isSilent = false }) {
+
// Define the base animation style
+
const activeAnimation = "wave 2.5s ease-in-out infinite";
+
+
// Define the style for the silent state (dots)
+
const silentStyle = {
+
animation: "none",
+
height: "5px",
+
};
+
return (
<div className="visualizer">
-
{Array(BAR_COUNT).fill(0).map((_, index) => (
-
<div
-
key={index}
-
className="bar"
-
style={{ animationDelay: BAR_ANIMATION_DELAYS[index] }}
-
>
-
</div>
-
))}
+
{Array(BAR_COUNT).fill(0).map((_, index) => {
+
// Determine the specific style for each bar
+
const barStyle = isSilent
+
? silentStyle // If silent, use the fixed dot style
+
: { // If not silent, use the full animation with its unique delay
+
animation: activeAnimation,
+
animationDelay: BAR_ANIMATION_DELAYS[index],
+
};
+
+
return (
+
<div
+
key={index}
+
className="bar"
+
style={barStyle} // Apply the determined style object
+
>
+
</div>
+
);
+
})}
<style jsx>
{`
···
.bar {
width: 2px;
-
/*background: linear-gradient(to top, #1b1bff, #8585fe, #1b1bff);*/
background: rgb(0, 151, 188);
border-radius: 1px;
-
animation: wave 2.5s ease-in-out infinite;
}
@keyframes wave {
0%, 100% {
-
/* Start/End Height */
height: 5px;
}
50% {
-
/* Peak Height */
height: 30px;
}
}
+1 -1
src/components/jsx/Lastfm.jsx
···
target="_blank"
className={oldStatusClasses}
>
-
<AudioVisualizer />
+
<AudioVisualizer isSilent={!data.nowPlaying} />
<div
style={{
display: "flex",
+87 -22
src/components/jsx/Tealfm.jsx
···
import { useEffect, useState } from "react";
+
import AudioVisualizer from "./AudioVisualizer";
export default function Tealfm() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
-
const fetchTealfmData = async () => {
+
const fetchStatus = async () => {
+
const repo = "did:plc:sfjxpxxyvewb2zlxwoz2vduw";
+
const statusCollection = "fm.teal.alpha.actor.status";
+
const lastPlayedCollection = "fm.teal.alpha.feed.play";
+
try {
-
const res = await fetch(
-
"https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Asfjxpxxyvewb2zlxwoz2vduw&collection=fm.teal.alpha.feed.play&limit=1&reverse=false",
+
// --- 1. Attempt to fetch the actor status record ---
+
const statusUrl =
+
`https://pds.indexx.dev/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${statusCollection}&rkey=self`;
+
const statusRes = await fetch(statusUrl);
+
+
if (!statusRes.ok) {
+
throw new Error(`HTTP ${statusRes.status} on status fetch`);
+
}
+
+
const statusData = await statusRes.json();
+
const statusValue = statusData?.value;
+
const nowTimestamp = Math.floor(Date.now() / 1000); // Current time in seconds
+
+
const isExpired = statusValue?.expiry &&
+
nowTimestamp > parseInt(statusValue.expiry, 10);
+
const isItemEmpty = !statusValue?.item?.trackName;
+
+
let status;
+
+
// Check if the status is valid (not expired and not empty)
+
if (statusValue && !isExpired && !isItemEmpty) {
+
// Status is valid, use it
+
const latest = statusValue.item;
+
+
status = {
+
song: latest.trackName,
+
artist: latest.artists.map((artist) =>
+
artist.artistName
+
).join(", "),
+
albumArt: latest.releaseMbId
+
? `https://coverartarchive.org/release/${latest.releaseMbId}/front-500`
+
: "/default-album-art.png", // Fallback image needed if not present
+
createdAt: parseInt(statusValue.time, 10) * 1000, // Convert seconds to milliseconds
+
link: latest.originUrl ?? "",
+
};
+
setData(status);
+
return; // Exit if a valid status is found
+
}
+
+
// --- 2. If status is expired or empty, fetch the last played record (Original Logic) ---
+
console.log(
+
"Status expired or empty. Falling back to last played record.",
);
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+
const lastPlayedUrl =
+
`https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=${repo}&collection=${lastPlayedCollection}&limit=1&reverse=false`;
+
const lastPlayedRes = await fetch(lastPlayedUrl);
-
const data = await res.json();
-
const latest = data.records[0].value;
+
if (!lastPlayedRes.ok) {
+
throw new Error(
+
`HTTP ${lastPlayedRes.status} on listRecords fetch`,
+
);
+
}
+
+
const lastPlayedData = await lastPlayedRes.json();
+
+
if (
+
lastPlayedData.records && lastPlayedData.records.length > 0
+
) {
+
const latest = lastPlayedData.records[0].value;
-
const status = {
-
song: latest.trackName,
-
artist: latest.artists.map((artist) => artist.artistName)
-
.join(", "),
-
albumArt:
-
`https://coverartarchive.org/release/${latest.releaseMbId}/front-500`,
-
createdAt: latest.playedTime,
-
link: latest.originUrl ?? "",
-
};
-
setData(status);
+
status = {
+
song: latest.trackName,
+
artist: latest.artists.map((artist) =>
+
artist.artistName
+
).join(", "),
+
albumArt:
+
`https://coverartarchive.org/release/${latest.releaseMbId}/front-500`,
+
createdAt: latest.playedTime,
+
link: latest.originUrl ?? "",
+
};
+
setData(status);
+
} else {
+
console.log("No records found in last played collection.");
+
// Optionally set a specific state for 'no data found'
+
}
} catch (err) {
console.error("Fetch failed:", err);
setError(err.message);
}
};
-
fetchTealfmData();
+
fetchStatus();
}, []);
if (error) return <span>Error: {error}</span>;
if (!data) return null;
+
// --- Time Ago and Styling Logic (Unchanged) ---
let timeAgo = "";
let oldStatusClasses = "";
+
// data.createdAt is either a string (from listRecords) or a number (from getRecord)
const date = new Date(data.createdAt);
const now = new Date();
const diff = now.getTime() - date.getTime();
···
oldStatusClasses = days > 3
? "opacity-75 text-decoration-line-through"
: "";
+
// --- End Time Ago and Styling Logic ---
return (
<a
···
target="_blank"
className={oldStatusClasses}
>
-
<img
-
src={data.albumArt}
-
alt={`${data.album} cover`}
-
/>
+
<AudioVisualizer isSilent={!data.nowPlaying} />
<div
style={{
display: "flex",
···
marginTop: "-5px",
}}
>
-
{timeAgo}
+
<small style={{ fontSize: "0.7rem" }}>
+
^ what I'm listening (or last listened) to
+
</small>
</div>
</div>
</a>
+2 -2
src/pages/index.astro
···
import ProjectsPane from "../components/jsx/ProjectsPane.jsx";
import Status from "../components/jsx/Status.jsx";
-
import Lastfm from "../components/jsx/Lastfm.jsx";
+
import Tealfm from "../components/jsx/Tealfm";
---
<Layout title="hello" description="insert wave emoji">
<main id="page" class="text-center">
<Header />
<Status client:load />
-
<Lastfm client:load />
+
<Tealfm client:load />
<SocialLinks />
<!--
<img