My personal site hosted @ https://indexx.dev
1import { useEffect, useState } from "react";
2import AudioVisualizer from "./AudioVisualizer";
3
4export default function Tealfm() {
5 const [data, setData] = useState(null);
6 const [error, setError] = useState(null);
7 const [isCurrentlyPlaying, setIsCurrentlyPlaying] = useState(false);
8
9 useEffect(() => {
10 const fetchStatus = async () => {
11 const repo = "did:plc:sfjxpxxyvewb2zlxwoz2vduw";
12 const statusCollection = "fm.teal.alpha.actor.status";
13 const lastPlayedCollection = "fm.teal.alpha.feed.play";
14
15 try {
16 const statusUrl =
17 `https://pds.indexx.dev/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${statusCollection}&rkey=self`;
18 const statusRes = await fetch(statusUrl);
19
20 if (!statusRes.ok) {
21 throw new Error(`HTTP ${statusRes.status} on status fetch`);
22 }
23
24 const statusData = await statusRes.json();
25 const statusValue = statusData?.value;
26 const nowTimestamp = Math.floor(Date.now() / 1000);
27
28 const isExpired = statusValue?.expiry &&
29 nowTimestamp > parseInt(statusValue.expiry, 10);
30 const isItemEmpty = !statusValue?.item?.trackName;
31
32 let status;
33
34 if (statusValue && !isExpired && !isItemEmpty) {
35 const latest = statusValue.item;
36
37 status = {
38 song: latest.trackName,
39 artist: latest.artists.map((artist) =>
40 artist.artistName
41 ).join(", "),
42 createdAt: parseInt(statusValue.time, 10) * 1000,
43 link: latest.originUrl ?? "",
44 };
45 setData(status);
46 setIsCurrentlyPlaying(true);
47 return;
48 }
49
50 console.log(
51 "Status expired or empty. Falling back to last played record.",
52 );
53
54 const lastPlayedUrl =
55 `https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=${repo}&collection=${lastPlayedCollection}&limit=1&reverse=false`;
56 const lastPlayedRes = await fetch(lastPlayedUrl);
57
58 if (!lastPlayedRes.ok) {
59 throw new Error(
60 `HTTP ${lastPlayedRes.status} on listRecords fetch`,
61 );
62 }
63
64 const lastPlayedData = await lastPlayedRes.json();
65
66 if (
67 lastPlayedData.records && lastPlayedData.records.length > 0
68 ) {
69 const latest = lastPlayedData.records[0].value;
70
71 status = {
72 song: latest.trackName,
73 artist: latest.artists.map((artist) =>
74 artist.artistName
75 ).join(", "),
76 albumArt:
77 `https://coverartarchive.org/release/${latest.releaseMbId}/front-500`,
78 createdAt: latest.playedTime,
79 link: latest.originUrl ?? "",
80 };
81 setData(status);
82 setIsCurrentlyPlaying(false);
83 } else {
84 console.log("No records found in last played collection.");
85 setIsCurrentlyPlaying(false);
86 }
87 } catch (err) {
88 console.error("Fetch failed:", err);
89 setError(err.message);
90 setIsCurrentlyPlaying(false);
91 }
92 };
93
94 fetchStatus();
95 }, []);
96
97 if (error) return <span>Error: {error}</span>;
98 if (!data) return null;
99
100 let timeAgo = "";
101 let oldStatusClasses = "";
102
103 const date = new Date(data.createdAt);
104 const now = new Date();
105 const diff = now.getTime() - date.getTime();
106
107 const minutes = Math.floor(diff / 60000);
108 const hours = Math.floor(minutes / 60);
109 const days = Math.floor(hours / 24);
110
111 if (days > 0) timeAgo = `${days} days ago`;
112 else if (hours > 0) timeAgo = `${hours} hours ago`;
113 else if (minutes > 0) timeAgo = `${minutes} minutes ago`;
114 else timeAgo = "just now";
115
116 oldStatusClasses = days > 3
117 ? "opacity-75 text-decoration-line-through"
118 : "";
119
120 return (
121 <a
122 id="now-playing"
123 href={data.link}
124 target="_blank"
125 className={oldStatusClasses}
126 >
127 <AudioVisualizer isSilent={!isCurrentlyPlaying} />
128 <div
129 style={{
130 display: "flex",
131 flexDirection: "column",
132 gap: "2px",
133 width: "100%",
134 }}
135 >
136 <div style={{ fontWeight: "bold" }}>{data.song}</div>
137 <div style={{ fontSize: "0.9em", marginTop: "-5px" }}>
138 {data.artist}
139 </div>
140 <div
141 style={{
142 fontSize: "0.8em",
143 opacity: 0.7,
144 marginTop: "-5px",
145 }}
146 >
147 <small style={{ fontSize: "0.7rem" }}>
148 ^ what I'm listening (or last listened) to
149 </small>
150 </div>
151 </div>
152 </a>
153 );
154}