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.title} + +

+

{track.artist}

+
+
+ + {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 []; + } +}