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

feat: restructure last.fm recent track display

Index 31128336 a9f32a89

+2 -1
.env.example
···
-
PUBLIC_ACTOR_DID=""
+
PUBLIC_ACTOR_DID=""
+
LASTFM_API_KEY=""
+7 -1
astro.config.mjs
···
import react from "@astrojs/react";
+
import node from "@astrojs/node";
+
// https://astro.build/config
export default defineConfig({
integrations: [react()],
-
});
+
+
adapter: node({
+
mode: "standalone",
+
}),
+
});
+169
package-lock.json
···
"name": "static",
"version": "0.0.1",
"dependencies": {
+
"@astrojs/node": "^9.2.2",
"@astrojs/react": "^4.3.0",
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
···
"unist-util-visit": "^5.0.0",
"unist-util-visit-parents": "^6.0.1",
"vfile": "^6.0.3"
+
}
+
},
+
"node_modules/@astrojs/node": {
+
"version": "9.2.2",
+
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.2.2.tgz",
+
"integrity": "sha512-PtLPuuojmcl9O3CEvXqL/D+wB4x5DlbrGOvP0MeTAh/VfKFprYAzgw1+45xsnTO+QvPWb26l1cT+ZQvvohmvMw==",
+
"dependencies": {
+
"@astrojs/internal-helpers": "0.6.1",
+
"send": "^1.2.0",
+
"server-destroy": "^1.0.1"
+
},
+
"peerDependencies": {
+
"astro": "^5.3.0"
}
},
"node_modules/@astrojs/prism": {
···
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
+
"node_modules/depd": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
···
"node": ">=4"
},
+
"node_modules/ee-first": {
+
"version": "1.1.1",
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+
},
"node_modules/electron-to-chromium": {
"version": "1.5.165",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
···
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="
},
+
"node_modules/encodeurl": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/entities": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
···
"node": ">=6"
},
+
"node_modules/escape-html": {
+
"version": "1.0.3",
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
···
"@types/estree": "^1.0.0"
},
+
"node_modules/etag": {
+
"version": "1.8.1",
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+
"engines": {
+
"node": ">= 0.6"
+
}
+
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
···
"unicode-trie": "^2.0.0"
},
+
"node_modules/fresh": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
···
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
+
"node_modules/http-errors": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+
"dependencies": {
+
"depd": "2.0.0",
+
"inherits": "2.0.4",
+
"setprototypeof": "1.2.0",
+
"statuses": "2.0.1",
+
"toidentifier": "1.0.1"
+
},
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
+
"node_modules/http-errors/node_modules/statuses": {
+
"version": "2.0.1",
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
···
"type": "github",
"url": "https://github.com/sponsors/wooorm"
+
},
+
"node_modules/inherits": {
+
"version": "2.0.4",
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/iron-webcrypto": {
"version": "1.2.1",
···
},
+
"node_modules/mime-db": {
+
"version": "1.54.0",
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+
"engines": {
+
"node": ">= 0.6"
+
}
+
},
+
"node_modules/mime-types": {
+
"version": "3.0.1",
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+
"dependencies": {
+
"mime-db": "^1.54.0"
+
},
+
"engines": {
+
"node": ">= 0.6"
+
}
+
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
···
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="
},
+
"node_modules/on-finished": {
+
"version": "2.4.1",
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+
"dependencies": {
+
"ee-first": "1.1.1"
+
},
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
···
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="
+
},
+
"node_modules/range-parser": {
+
"version": "1.2.1",
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+
"engines": {
+
"node": ">= 0.6"
+
}
},
"node_modules/react": {
"version": "19.1.0",
···
"node": ">=10"
},
+
"node_modules/send": {
+
"version": "1.2.0",
+
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+
"dependencies": {
+
"debug": "^4.3.5",
+
"encodeurl": "^2.0.0",
+
"escape-html": "^1.0.3",
+
"etag": "^1.8.1",
+
"fresh": "^2.0.0",
+
"http-errors": "^2.0.0",
+
"mime-types": "^3.0.1",
+
"ms": "^2.1.3",
+
"on-finished": "^2.4.1",
+
"range-parser": "^1.2.1",
+
"statuses": "^2.0.1"
+
},
+
"engines": {
+
"node": ">= 18"
+
}
+
},
+
"node_modules/server-destroy": {
+
"version": "1.0.1",
+
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="
+
},
+
"node_modules/setprototypeof": {
+
"version": "1.2.0",
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
···
"url": "https://github.com/sponsors/wooorm"
},
+
"node_modules/statuses": {
+
"version": "2.0.2",
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+
"engines": {
+
"node": ">= 0.8"
+
}
+
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
···
"integrity": "sha512-XGhStWuOlBA5D8QnyN2xtgB2cUOdJ3ztisne1DYVWMcVH29qh8eQIpRmP3HnuJLdgyzG0HpdGzRMu1lm/Oictw==",
"bin": {
"tlds": "bin.js"
+
}
+
},
+
"node_modules/toidentifier": {
+
"version": "1.0.1",
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+
"engines": {
+
"node": ">=0.6"
},
"node_modules/tr46": {
+1
package.json
···
"astro": "astro"
},
"dependencies": {
+
"@astrojs/node": "^9.2.2",
"@astrojs/react": "^4.3.0",
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
-2
robots.txt
···
-
User-agent: *
-
Disallow:
+8 -18
src/components/NowPlaying.jsx
···
useEffect(() => {
const fetchNowPlaying = async () => {
try {
-
const res = await fetch(
-
"https://lastfm-last-played.biancarosa.com.br/Index_Card/latest-song",
-
);
+
const res = await fetch("/api/now-playing");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
-
const track = json.track;
-
const isNowPlaying = track["@attr"]?.nowplaying === "true";
-
const status = {
-
song: track.name,
-
artist: track.artist["#text"],
-
album: track.album["#text"],
-
createdAt: track.date
-
? new Date(track.date.uts * 1000).toISOString()
-
: null,
-
link: track.url,
-
nowPlaying: isNowPlaying,
-
albumArt: track.image.find((img) =>
-
img.size === "medium"
-
)["#text"],
+
song: json.track,
+
artist: json.artist,
+
albumArt: json.cover,
+
createdAt: json.createdAt,
+
link: json.url,
+
nowPlaying: !json.createdAt,
};
setData(status);
} catch (err) {
···
}, []);
if (error) return <span>Error: {error}</span>;
-
if (!data) return <span>Loading now playing...</span>;
+
if (!data) return null;
let timeAgo = "";
let oldStatusClasses = "";
+4
src/components/SocialLinks.astro
···
{
name: 'GitHub',
href: 'https://github.com/indexxing/'
+
},
+
{
+
name: 'last.fm',
+
href: 'https://last.fm/user/Index_Card'
}
];
---
+20 -2
src/components/Status.jsx
···
fetchStatus();
}, [actorDid]);
-
if (error) return <span>Error: {error}</span>;
-
if (!data) return <span>Loading status...</span>;
+
if (error) {
+
return (
+
<span
+
className="badge bg-dark"
+
style={{ color: "#595959 !important" }}
+
>
+
Error: {error}
+
</span>
+
);
+
}
+
if (!data) {
+
return (
+
<span
+
className="badge bg-dark"
+
style={{ color: "#595959 !important" }}
+
>
+
Loading status...
+
</span>
+
);
+
}
const date = new Date(data.createdAt);
const now = new Date();
+76
src/pages/api/now-playing.ts
···
+
import type { APIRoute } from "astro";
+
const lastfmAPIKey = import.meta.env.LASTFM_API_KEY;
+
+
if (!lastfmAPIKey) {
+
console.error(
+
"LASTFM_API_KEY is not defined. Please check your .env file.",
+
);
+
}
+
+
interface CacheItem {
+
data: any;
+
timestamp: number;
+
}
+
+
let cache: CacheItem | null = null;
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
+
+
export const prerender = false;
+
+
export const GET: APIRoute = async () => {
+
const now = Date.now();
+
+
if (cache && (now - cache.timestamp) < CACHE_DURATION) {
+
return Response.json(cache.data, {
+
status: 200,
+
headers: {
+
"X-Index-API-Cached": "true", // Debug
+
"Cache-Control": "max-age=300", // 5 minutes
+
},
+
});
+
}
+
+
try {
+
const apiRes = await fetch(
+
`http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=Index_Card&api_key=${lastfmAPIKey}&format=json`,
+
);
+
+
if (!apiRes.ok) {
+
return Response.json({}, {
+
status: 502,
+
});
+
}
+
+
const apiData: LastFmApiResponse = await apiRes.json();
+
const recentTrack = apiData.recenttracks.track[0];
+
+
const data = {
+
track: recentTrack.name,
+
artist: recentTrack.artist["#text"],
+
cover: recentTrack.image.find((img) =>
+
img.size == "medium"
+
)!["#text"],
+
url: recentTrack.url,
+
createdAt: recentTrack.date
+
? new Date(parseInt(recentTrack.date.uts) * 1000).toISOString()
+
: null,
+
};
+
+
cache = {
+
data,
+
timestamp: Date.now(),
+
};
+
+
return Response.json(data, {
+
status: 200,
+
headers: {
+
"X-Index-API-Cached": "true", // Debug
+
"Cache-Control": "max-age=300", // 5 minutes
+
},
+
});
+
} catch (error) {
+
return Response.json({}, {
+
status: 500,
+
});
+
}
+
};
+52
types/lastfm.d.ts
···
+
interface LastFmImage {
+
size: "small" | "medium" | "large" | "extralarge";
+
"#text": string;
+
}
+
+
interface LastFmArtist {
+
mbid: string;
+
"#text": string;
+
}
+
+
interface LastFmAlbum {
+
mbid: string;
+
"#text": string;
+
}
+
+
interface LastFmDate {
+
uts: string;
+
"#text": string;
+
}
+
+
interface LastFmTrackAttr {
+
nowplaying?: boolean;
+
}
+
+
interface LastFmTrack {
+
artist: LastFmArtist;
+
streamable: boolean;
+
image: LastFmImage[];
+
mbid: string;
+
album: LastFmAlbum;
+
name: string;
+
url: string;
+
date: LastFmDate;
+
"@attr"?: LastFmTrackAttr;
+
}
+
+
interface LastFmRecentTracksAttr {
+
user: string;
+
totalPages: string;
+
page: string;
+
perPage: string;
+
total: string;
+
}
+
+
interface LastFmRecentTracks {
+
track: LastFmTrack[];
+
"@attr": LastFmRecentTracksAttr;
+
}
+
+
interface LastFmApiResponse {
+
recenttracks: LastFmRecentTracks;
+
}