Add recently played to profiles

A play is recorded when a new track starts (pausing and resuming doesn't
count; returning after 10+ minutes does). Profiles show the last 10,
excluding the track playing now; 50 are kept per user. Recent plays are
free on every plan; the paid flag is renamed to fullHistory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 16:02:38 -04:00
co-authored by Claude Opus 5.5
parent 87353ccca3
commit ece9c4fd64
14 changed files with 1147 additions and 11 deletions
+6 -1
View File
@@ -7,6 +7,7 @@ import useSWR from 'swr';
import type { ProfileDTO } from '@musicshare/shared';
import { Avatar } from '@/components/Avatar';
import { NowPlayingCard } from '@/components/NowPlayingCard';
import { RecentPlays } from '@/components/RecentPlays';
import { TrackCard } from '@/components/TrackCard';
import { api, fetcher, useMe } from '@/lib/api';
import { useRealtime } from '@/lib/useRealtime';
@@ -20,7 +21,9 @@ export default function ProfilePage() {
// Live updates arrive for people you follow (and yourself)
useRealtime(Boolean(me), (msg) => {
if (msg.type === 'now_playing' && profile && msg.data.user.id === profile.user.id) {
mutate({ ...profile, nowPlaying: msg.data }, { revalidate: false });
// A new track moves the previous one into "Recently played", so refetch; otherwise patch in place.
const newTrack = msg.data.track.id !== profile.nowPlaying?.track.id;
mutate({ ...profile, nowPlaying: msg.data }, { revalidate: newTrack });
}
});
@@ -94,6 +97,8 @@ export default function ProfilePage() {
) : (
<p className="muted">Nothing playing right now.</p>
)}
<RecentPlays plays={profile.recent} preferred={preferred} />
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
'use client';
import { SERVICE_LABELS, openUrl, timeAgo, type RecentPlayDTO, type StreamingService } from '@musicshare/shared';
export function RecentPlays({ plays, preferred }: { plays: RecentPlayDTO[]; preferred: StreamingService | null | undefined }) {
if (plays.length === 0) return null;
return (
<section className="card">
<h2>Recently played</h2>
<ul className="recent">
{plays.map((p, i) => {
const target = preferred ? openUrl(p.track, preferred) : null;
const row = (
<>
{p.track.artworkUrl ? <img className="artwork-sm" src={p.track.artworkUrl} alt="" /> : <div className="artwork-sm artwork-empty">♪</div>}
<span className="recent-text">
<span className="recent-title">{p.track.title}</span>
<span className="muted">{p.track.artist}</span>
</span>
<span className="muted recent-time">{timeAgo(p.playedAt)}</span>
</>
);
return (
<li key={`${p.playedAt}-${i}`}>
{target ? (
<a
className="recent-row"
href={target.url}
target="_blank"
rel="noreferrer"
title={`${target.exact ? 'Open' : 'Search'} in ${SERVICE_LABELS[preferred!]}`}
>
{row}
</a>
) : (
<div className="recent-row">{row}</div>
)}
</li>
);
})}
</ul>
</section>
);
}