diff --git a/src/components/plyr-tracks.tsx b/src/components/plyr-tracks.tsx
new file mode 100644
index 0000000..b78c611
--- /dev/null
+++ b/src/components/plyr-tracks.tsx
@@ -0,0 +1,61 @@
+import { ProfileArtifact } from "~/components/profile-artifact";
+import type { PlyrTrack } from "~/lib/providers/plyr";
+
+export function PlyrTrackArtifact({ track }: { track: PlyrTrack }) {
+ const duration = formatDuration(track.duration);
+
+ return (
+
+
+ {track.imageUrl ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {track.description && (
+ {track.description}
+ )}
+
+ {duration && {duration}
}
+
+ );
+}
+
+function formatDuration(duration: number): string | null {
+ if (!Number.isFinite(duration)) {
+ return null;
+ }
+
+ const totalSeconds = Math.max(0, Math.round(duration));
+ const seconds = totalSeconds % 60;
+ const minutes = Math.floor(totalSeconds / 60) % 60;
+ const hours = Math.floor(totalSeconds / 3600);
+
+ if (hours > 0) {
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+ }
+
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
+}
diff --git a/src/components/profile-sections.tsx b/src/components/profile-sections.tsx
index 77c6ed5..0c03956 100644
--- a/src/components/profile-sections.tsx
+++ b/src/components/profile-sections.tsx
@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
import { GrainPhotoArtifact } from "~/components/grain-photos";
+import { PlyrTrackArtifact } from "~/components/plyr-tracks";
import { ProfileArtifact } from "~/components/profile-artifact";
import { RockskyScrobble } from "~/components/rocksky-scrobble-list";
import { StandardDocumentArtifact } from "~/components/standard-documents";
import { TangledRepoArtifact } from "~/components/tangled-repo-list";
import { getDidSectionOrder } from "~/lib/layout/did-section-order";
import { getRecentGrainPhotos } from "~/lib/providers/grain-photo";
+import { getRecentPlyrTracks } from "~/lib/providers/plyr";
import { getRecentRocksky } from "~/lib/providers/rocksky";
import { getMostStarredTangledRepos } from "~/lib/providers/tangled";
import { getRecentStandardDocuments } from "~/lib/providers/standard-site";
@@ -19,6 +21,7 @@ type ProfileSectionsProps = {
type ProfileData = {
photos: Awaited>;
+ plyrTracks: Awaited>;
scrobbles: Awaited>;
documents: Awaited>;
tangledRepos: Awaited>;
@@ -31,15 +34,17 @@ export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) {
let isCurrent = true;
async function loadProfileData() {
- const [photos, scrobbles, documents, tangledRepos] = await Promise.all([
- getRecentGrainPhotos(pds, did),
- getRecentRocksky(pds, did),
- getRecentStandardDocuments(pds, did),
- getMostStarredTangledRepos(pds, did, handle),
- ]);
+ const [photos, plyrTracks, scrobbles, documents, tangledRepos] =
+ await Promise.all([
+ getRecentGrainPhotos(pds, did),
+ getRecentPlyrTracks(pds, did),
+ getRecentRocksky(pds, did),
+ getRecentStandardDocuments(pds, did),
+ getMostStarredTangledRepos(pds, did, handle),
+ ]);
if (isCurrent) {
- setData({ photos, scrobbles, documents, tangledRepos });
+ setData({ photos, plyrTracks, scrobbles, documents, tangledRepos });
}
}
@@ -60,6 +65,12 @@ export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) {
}
const artifacts = [
+ ...data.plyrTracks.map((track) => (
+
+ )),
...data.scrobbles.map((scrobble) => (
;
+};
+
+const PLYR_TRACK_COLLECTION = "fm.plyr.track";
+const MAX_TRACK_COUNT = 4;
+
+export async function getRecentPlyrTracks(
+ pds: string,
+ did: string,
+): Promise {
+ try {
+ const params = new URLSearchParams({
+ repo: did,
+ collection: PLYR_TRACK_COLLECTION,
+ limit: String(MAX_TRACK_COUNT),
+ reverse: "true",
+ });
+ const response = await fetch(
+ `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.listRecords?${params}`,
+ { cache: "no-store" },
+ );
+
+ if (!response.ok) {
+ return [];
+ }
+
+ const data = (await response.json()) as ListRecordsResponse;
+
+ return data.records
+ .map(({ value }) => value)
+ .filter((track) => track.audioUrl && track.title && track.artist)
+ .sort(
+ (firstTrack, secondTrack) =>
+ Date.parse(secondTrack.createdAt) - Date.parse(firstTrack.createdAt),
+ );
+ } catch {
+ return [];
+ }
+}